init
This commit is contained in:
		@@ -0,0 +1,27 @@
 | 
			
		||||
package com.jeelowcode.tool.framework.operatelog.config;
 | 
			
		||||
 | 
			
		||||
import com.jeelowcode.tool.framework.operatelog.core.service.LogRecordServiceImpl;
 | 
			
		||||
import com.mzt.logapi.service.ILogRecordService;
 | 
			
		||||
import com.mzt.logapi.starter.annotation.EnableLogRecord;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
 | 
			
		||||
import org.springframework.context.annotation.Bean;
 | 
			
		||||
import org.springframework.context.annotation.Primary;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 操作日志配置类
 | 
			
		||||
 *
 | 
			
		||||
 * @author HUIHUI
 | 
			
		||||
 */
 | 
			
		||||
@EnableLogRecord(tenant = "") // 貌似用不上 tenant 这玩意给个空好啦
 | 
			
		||||
@AutoConfiguration
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class OperateLogV2Configuration {
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    @Primary
 | 
			
		||||
    public ILogRecordService iLogRecordServiceImpl() {
 | 
			
		||||
        return new LogRecordServiceImpl();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,4 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 占位,无特殊作用
 | 
			
		||||
 */
 | 
			
		||||
package com.jeelowcode.tool.framework.operatelog.core;
 | 
			
		||||
@@ -0,0 +1,89 @@
 | 
			
		||||
package com.jeelowcode.tool.framework.operatelog.core.service;
 | 
			
		||||
 | 
			
		||||
import com.jeelowcode.tool.framework.common.util.monitor.TracerUtils;
 | 
			
		||||
import com.jeelowcode.tool.framework.common.util.servlet.ServletUtils;
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.LoginUser;
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.util.SecurityFrameworkUtils;
 | 
			
		||||
import com.jeelowcode.service.system.api.IApiOperateLogApi;
 | 
			
		||||
import com.jeelowcode.service.system.dto.OperateLogV2CreateReqDTO;
 | 
			
		||||
import com.mzt.logapi.beans.LogRecord;
 | 
			
		||||
import com.mzt.logapi.service.ILogRecordService;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Resource;
 | 
			
		||||
import javax.servlet.http.HttpServletRequest;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 操作日志 ILogRecordService 实现类
 | 
			
		||||
 *
 | 
			
		||||
 * 基于 {@link IApiOperateLogApi} 实现,记录操作日志
 | 
			
		||||
 *
 | 
			
		||||
 * @author HUIHUI
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class LogRecordServiceImpl implements ILogRecordService {
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private IApiOperateLogApi apiOperateLogApi;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void record(LogRecord logRecord) {
 | 
			
		||||
        // 1. 补全通用字段
 | 
			
		||||
        OperateLogV2CreateReqDTO reqDTO = new OperateLogV2CreateReqDTO();
 | 
			
		||||
        reqDTO.setTraceId(TracerUtils.getTraceId());
 | 
			
		||||
        // 补充用户信息
 | 
			
		||||
        fillUserFields(reqDTO);
 | 
			
		||||
        // 补全模块信息
 | 
			
		||||
        fillModuleFields(reqDTO, logRecord);
 | 
			
		||||
        // 补全请求信息
 | 
			
		||||
        fillRequestFields(reqDTO);
 | 
			
		||||
 | 
			
		||||
        // 2. 异步记录日志
 | 
			
		||||
        apiOperateLogApi.createOperateLogV2(reqDTO);
 | 
			
		||||
        // TODO 测试结束删除或搞个开关
 | 
			
		||||
        log.info("操作日志 ===> {}", reqDTO);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void fillUserFields(OperateLogV2CreateReqDTO reqDTO) {
 | 
			
		||||
        // 使用 SecurityFrameworkUtils。因为要考虑,rpc、mq、job,它其实不是 web;
 | 
			
		||||
        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
 | 
			
		||||
        if (loginUser == null) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        reqDTO.setUserId(loginUser.getId());
 | 
			
		||||
        reqDTO.setUserType(loginUser.getUserType());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void fillModuleFields(OperateLogV2CreateReqDTO reqDTO, LogRecord logRecord) {
 | 
			
		||||
        reqDTO.setType(logRecord.getType()); // 大模块类型,例如:CRM 客户
 | 
			
		||||
        reqDTO.setSubType(logRecord.getSubType());// 操作名称,例如:转移客户
 | 
			
		||||
        reqDTO.setBizId(Long.parseLong(logRecord.getBizNo())); // 业务编号,例如:客户编号
 | 
			
		||||
        reqDTO.setAction(logRecord.getAction());// 操作内容,例如:修改编号为 1 的用户信息,将性别从男改成女,将姓名从芋道改成源码。
 | 
			
		||||
        reqDTO.setExtra(logRecord.getExtra()); // 拓展字段,有些复杂的业务,需要记录一些字段 ( JSON 格式 ),例如说,记录订单编号,{ orderId: "1"}
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void fillRequestFields(OperateLogV2CreateReqDTO reqDTO) {
 | 
			
		||||
        // 获得 Request 对象
 | 
			
		||||
        HttpServletRequest request = ServletUtils.getRequest();
 | 
			
		||||
        if (request == null) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        // 补全请求信息
 | 
			
		||||
        reqDTO.setRequestMethod(request.getMethod());
 | 
			
		||||
        reqDTO.setRequestUrl(request.getRequestURI());
 | 
			
		||||
        reqDTO.setUserIp(ServletUtils.getClientIP(request));
 | 
			
		||||
        reqDTO.setUserAgent(ServletUtils.getUserAgent(request));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<LogRecord> queryLog(String bizNo, String type) {
 | 
			
		||||
        throw new UnsupportedOperationException("使用 OperateLogApi 进行操作日志的查询");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<LogRecord> queryLogByBizNo(String bizNo, String type, String subType) {
 | 
			
		||||
        throw new UnsupportedOperationException("使用 OperateLogApi 进行操作日志的查询");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 基于 mzt-log 框架
 | 
			
		||||
 * 实现操作日志功能
 | 
			
		||||
 *
 | 
			
		||||
 * @author HUIHUI
 | 
			
		||||
 */
 | 
			
		||||
package com.jeelowcode.tool.framework.operatelog;
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
package com.jeelowcode.tool.framework.security.config;
 | 
			
		||||
 | 
			
		||||
import com.jeelowcode.tool.framework.web.config.WebProperties;
 | 
			
		||||
import org.springframework.core.Ordered;
 | 
			
		||||
import org.springframework.security.config.Customizer;
 | 
			
		||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 | 
			
		||||
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Resource;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 自定义的 URL 的安全配置
 | 
			
		||||
 * 目的:每个 Maven Module 可以自定义规则!
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
public abstract class AuthorizeRequestsCustomizer
 | 
			
		||||
        implements Customizer<ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry>, Ordered {
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private WebProperties webProperties;
 | 
			
		||||
 | 
			
		||||
    protected String buildAdminApi(String url) {
 | 
			
		||||
        return webProperties.getAdminApi().getPrefix() + url;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected String buildAppApi(String url) {
 | 
			
		||||
        return webProperties.getAppApi().getPrefix() + url;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getOrder() {
 | 
			
		||||
        return 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,102 @@
 | 
			
		||||
package com.jeelowcode.tool.framework.security.config;
 | 
			
		||||
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.aop.PreAuthenticatedAspect;
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy;
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.filter.TokenAuthenticationFilter;
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.handler.AccessDeniedHandlerImpl;
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.handler.AuthenticationEntryPointImpl;
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.service.SecurityFrameworkService;
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.service.SecurityFrameworkServiceImpl;
 | 
			
		||||
import com.jeelowcode.tool.framework.web.core.handler.GlobalExceptionHandler;
 | 
			
		||||
import com.jeelowcode.service.system.api.IApiOAuth2TokenApi;
 | 
			
		||||
import com.jeelowcode.service.system.api.IApiPermissionApi;
 | 
			
		||||
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
 | 
			
		||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
 | 
			
		||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
 | 
			
		||||
import org.springframework.context.annotation.Bean;
 | 
			
		||||
import org.springframework.security.core.context.SecurityContextHolder;
 | 
			
		||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 | 
			
		||||
import org.springframework.security.crypto.password.PasswordEncoder;
 | 
			
		||||
import org.springframework.security.web.AuthenticationEntryPoint;
 | 
			
		||||
import org.springframework.security.web.access.AccessDeniedHandler;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Resource;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Spring Security 自动配置类,主要用于相关组件的配置
 | 
			
		||||
 *
 | 
			
		||||
 * 注意,不能和 {@link WebSecurityConfigurerAdapter} 用一个,原因是会导致初始化报错。
 | 
			
		||||
 * 参见 https://stackoverflow.com/questions/53847050/spring-boot-delegatebuilder-cannot-be-null-on-autowiring-authenticationmanager 文档。
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@AutoConfiguration
 | 
			
		||||
@EnableConfigurationProperties(SecurityProperties.class)
 | 
			
		||||
public class SecurityAutoConfiguration {
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private SecurityProperties securityProperties;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 处理用户未登录拦截的切面的 Bean
 | 
			
		||||
     */
 | 
			
		||||
    @Bean
 | 
			
		||||
    public PreAuthenticatedAspect preAuthenticatedAspect() {
 | 
			
		||||
        return new PreAuthenticatedAspect();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 认证失败处理类 Bean
 | 
			
		||||
     */
 | 
			
		||||
    @Bean
 | 
			
		||||
    public AuthenticationEntryPoint authenticationEntryPoint() {
 | 
			
		||||
        return new AuthenticationEntryPointImpl();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 权限不够处理器 Bean
 | 
			
		||||
     */
 | 
			
		||||
    @Bean
 | 
			
		||||
    public AccessDeniedHandler accessDeniedHandler() {
 | 
			
		||||
        return new AccessDeniedHandlerImpl();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Spring Security 加密器
 | 
			
		||||
     * 考虑到安全性,这里采用 BCryptPasswordEncoder 加密器
 | 
			
		||||
     *
 | 
			
		||||
     * @see <a href="http://stackabuse.com/password-encoding-with-spring-security/">Password Encoding with Spring Security</a>
 | 
			
		||||
     */
 | 
			
		||||
    @Bean
 | 
			
		||||
    public PasswordEncoder passwordEncoder() {
 | 
			
		||||
        return new BCryptPasswordEncoder(securityProperties.getPasswordEncoderLength());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Token 认证过滤器 Bean
 | 
			
		||||
     */
 | 
			
		||||
    @Bean
 | 
			
		||||
    public TokenAuthenticationFilter authenticationTokenFilter(GlobalExceptionHandler globalExceptionHandler,
 | 
			
		||||
                                                               IApiOAuth2TokenApi oauth2TokenApi) {
 | 
			
		||||
        return new TokenAuthenticationFilter(securityProperties, globalExceptionHandler, oauth2TokenApi);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Bean("ss") // 使用 Spring Security 的缩写,方便使用
 | 
			
		||||
    public SecurityFrameworkService securityFrameworkService(IApiPermissionApi apiPermissionApi) {
 | 
			
		||||
        return new SecurityFrameworkServiceImpl(apiPermissionApi);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 声明调用 {@link SecurityContextHolder#setStrategyName(String)} 方法,
 | 
			
		||||
     * 设置使用 {@link TransmittableThreadLocalSecurityContextHolderStrategy} 作为 Security 的上下文策略
 | 
			
		||||
     */
 | 
			
		||||
    @Bean
 | 
			
		||||
    public MethodInvokingFactoryBean securityContextHolderMethodInvokingFactoryBean() {
 | 
			
		||||
        MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
 | 
			
		||||
        methodInvokingFactoryBean.setTargetClass(SecurityContextHolder.class);
 | 
			
		||||
        methodInvokingFactoryBean.setTargetMethod("setStrategyName");
 | 
			
		||||
        methodInvokingFactoryBean.setArguments(TransmittableThreadLocalSecurityContextHolderStrategy.class.getName());
 | 
			
		||||
        return methodInvokingFactoryBean;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,51 @@
 | 
			
		||||
package com.jeelowcode.tool.framework.security.config;
 | 
			
		||||
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import org.springframework.boot.context.properties.ConfigurationProperties;
 | 
			
		||||
import org.springframework.validation.annotation.Validated;
 | 
			
		||||
 | 
			
		||||
import javax.validation.constraints.NotEmpty;
 | 
			
		||||
import javax.validation.constraints.NotNull;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
@ConfigurationProperties(prefix = "jeelowcode.security")
 | 
			
		||||
@Validated
 | 
			
		||||
@Data
 | 
			
		||||
public class SecurityProperties {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * HTTP 请求时,访问令牌的请求 Header
 | 
			
		||||
     */
 | 
			
		||||
    @NotEmpty(message = "Token Header 不能为空")
 | 
			
		||||
    private String tokenHeader = "Authorization";
 | 
			
		||||
    /**
 | 
			
		||||
     * HTTP 请求时,访问令牌的请求参数
 | 
			
		||||
     *
 | 
			
		||||
     * 初始目的:解决 WebSocket 无法通过 header 传参,只能通过 token 参数拼接
 | 
			
		||||
     */
 | 
			
		||||
    @NotEmpty(message = "Token Parameter 不能为空")
 | 
			
		||||
    private String tokenParameter = "token";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * mock 模式的开关
 | 
			
		||||
     */
 | 
			
		||||
    @NotNull(message = "mock 模式的开关不能为空")
 | 
			
		||||
    private Boolean mockEnable = false;
 | 
			
		||||
    /**
 | 
			
		||||
     * mock 模式的密钥
 | 
			
		||||
     * 一定要配置密钥,保证安全性
 | 
			
		||||
     */
 | 
			
		||||
    @NotEmpty(message = "mock 模式的密钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。
 | 
			
		||||
    private String mockSecret = "test";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 免登录的 URL 列表
 | 
			
		||||
     */
 | 
			
		||||
    private List<String> permitAllUrls = Collections.emptyList();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * PasswordEncoder 加密复杂度,越高开销越大
 | 
			
		||||
     */
 | 
			
		||||
    private Integer passwordEncoderLength = 4;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,197 @@
 | 
			
		||||
package com.jeelowcode.tool.framework.security.config;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.collection.CollUtil;
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.filter.TokenAuthenticationFilter;
 | 
			
		||||
import com.jeelowcode.tool.framework.web.config.WebProperties;
 | 
			
		||||
import com.google.common.collect.HashMultimap;
 | 
			
		||||
import com.google.common.collect.Multimap;
 | 
			
		||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
 | 
			
		||||
import org.springframework.context.ApplicationContext;
 | 
			
		||||
import org.springframework.context.annotation.Bean;
 | 
			
		||||
import org.springframework.http.HttpMethod;
 | 
			
		||||
import org.springframework.security.authentication.AuthenticationManager;
 | 
			
		||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
 | 
			
		||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
 | 
			
		||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 | 
			
		||||
import org.springframework.security.config.http.SessionCreationPolicy;
 | 
			
		||||
import org.springframework.security.web.AuthenticationEntryPoint;
 | 
			
		||||
import org.springframework.security.web.SecurityFilterChain;
 | 
			
		||||
import org.springframework.security.web.access.AccessDeniedHandler;
 | 
			
		||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 | 
			
		||||
import org.springframework.web.bind.annotation.RequestMethod;
 | 
			
		||||
import org.springframework.web.method.HandlerMethod;
 | 
			
		||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
 | 
			
		||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Resource;
 | 
			
		||||
import javax.annotation.security.PermitAll;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 自定义的 Spring Security 配置适配器实现
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@AutoConfiguration
 | 
			
		||||
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
 | 
			
		||||
public class WebSecurityConfigurerAdapter {
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private WebProperties webProperties;
 | 
			
		||||
    @Resource
 | 
			
		||||
    private SecurityProperties securityProperties;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 认证失败处理类 Bean
 | 
			
		||||
     */
 | 
			
		||||
    @Resource
 | 
			
		||||
    private AuthenticationEntryPoint authenticationEntryPoint;
 | 
			
		||||
    /**
 | 
			
		||||
     * 权限不够处理器 Bean
 | 
			
		||||
     */
 | 
			
		||||
    @Resource
 | 
			
		||||
    private AccessDeniedHandler accessDeniedHandler;
 | 
			
		||||
    /**
 | 
			
		||||
     * Token 认证过滤器 Bean
 | 
			
		||||
     */
 | 
			
		||||
    @Resource
 | 
			
		||||
    private TokenAuthenticationFilter authenticationTokenFilter;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 自定义的权限映射 Bean 们
 | 
			
		||||
     *
 | 
			
		||||
     * @see #filterChain(HttpSecurity)
 | 
			
		||||
     */
 | 
			
		||||
    @Resource
 | 
			
		||||
    private List<AuthorizeRequestsCustomizer> authorizeRequestsCustomizers;
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private ApplicationContext applicationContext;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入
 | 
			
		||||
     * 通过覆写父类的该方法,添加 @Bean 注解,解决该问题
 | 
			
		||||
     */
 | 
			
		||||
    @Bean
 | 
			
		||||
    public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {
 | 
			
		||||
        return authenticationConfiguration.getAuthenticationManager();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 配置 URL 的安全配置
 | 
			
		||||
     *
 | 
			
		||||
     * anyRequest          |   匹配所有请求路径
 | 
			
		||||
     * access              |   SpringEl表达式结果为true时可以访问
 | 
			
		||||
     * anonymous           |   匿名可以访问
 | 
			
		||||
     * denyAll             |   用户不能访问
 | 
			
		||||
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
 | 
			
		||||
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
 | 
			
		||||
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
 | 
			
		||||
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
 | 
			
		||||
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
 | 
			
		||||
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
 | 
			
		||||
     * permitAll           |   用户可以任意访问
 | 
			
		||||
     * rememberMe          |   允许通过remember-me登录的用户访问
 | 
			
		||||
     * authenticated       |   用户登录后可访问
 | 
			
		||||
     */
 | 
			
		||||
    @Bean
 | 
			
		||||
    protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
 | 
			
		||||
        // 登出
 | 
			
		||||
        httpSecurity
 | 
			
		||||
                // 开启跨域
 | 
			
		||||
                .cors().and()
 | 
			
		||||
                // CSRF 禁用,因为不使用 Session
 | 
			
		||||
                .csrf().disable()
 | 
			
		||||
                // 基于 token 机制,所以不需要 Session
 | 
			
		||||
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
 | 
			
		||||
                .headers().frameOptions().disable().and()
 | 
			
		||||
                // 一堆自定义的 Spring Security 处理器
 | 
			
		||||
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
 | 
			
		||||
                .accessDeniedHandler(accessDeniedHandler);
 | 
			
		||||
        // 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高
 | 
			
		||||
 | 
			
		||||
        // 获得 @PermitAll 带来的 URL 列表,免登录
 | 
			
		||||
        Multimap<HttpMethod, String> permitAllUrls = getPermitAllUrlsFromAnnotations();
 | 
			
		||||
        // 设置每个请求的权限
 | 
			
		||||
        httpSecurity
 | 
			
		||||
                // ①:全局共享规则
 | 
			
		||||
                .authorizeRequests()
 | 
			
		||||
                // 1.1 静态资源,可匿名访问
 | 
			
		||||
                .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
 | 
			
		||||
                // 1.2 设置 @PermitAll 无需认证
 | 
			
		||||
                .antMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll()
 | 
			
		||||
                .antMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll()
 | 
			
		||||
                .antMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll()
 | 
			
		||||
                .antMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll()
 | 
			
		||||
                // 1.3 基于 jeelowcode.security.permit-all-urls 无需认证
 | 
			
		||||
                .antMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll()
 | 
			
		||||
                // 1.4 设置 App API 无需认证
 | 
			
		||||
                .antMatchers(buildAppApi("/**")).permitAll()
 | 
			
		||||
                // 1.5 验证码captcha 允许匿名访问
 | 
			
		||||
                .antMatchers("/captcha/get", "/captcha/check").permitAll()
 | 
			
		||||
                // ②:每个项目的自定义规则
 | 
			
		||||
                .and().authorizeRequests(registry -> // 下面,循环设置自定义规则
 | 
			
		||||
                        authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry)))
 | 
			
		||||
                // ③:兜底规则,必须认证
 | 
			
		||||
                .authorizeRequests()
 | 
			
		||||
                .anyRequest().authenticated()
 | 
			
		||||
        ;
 | 
			
		||||
 | 
			
		||||
        // 添加 Token Filter
 | 
			
		||||
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
 | 
			
		||||
        return httpSecurity.build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private String buildAppApi(String url) {
 | 
			
		||||
        return webProperties.getAppApi().getPrefix() + url;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Multimap<HttpMethod, String> getPermitAllUrlsFromAnnotations() {
 | 
			
		||||
        Multimap<HttpMethod, String> result = HashMultimap.create();
 | 
			
		||||
        // 获得接口对应的 HandlerMethod 集合
 | 
			
		||||
        RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping)
 | 
			
		||||
                applicationContext.getBean("requestMappingHandlerMapping");
 | 
			
		||||
        Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
 | 
			
		||||
        // 获得有 @PermitAll 注解的接口
 | 
			
		||||
        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethodMap.entrySet()) {
 | 
			
		||||
            HandlerMethod handlerMethod = entry.getValue();
 | 
			
		||||
            if (!handlerMethod.hasMethodAnnotation(PermitAll.class)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            if (entry.getKey().getPatternsCondition() == null) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            Set<String> urls = entry.getKey().getPatternsCondition().getPatterns();
 | 
			
		||||
            // 特殊:使用 @RequestMapping 注解,并且未写 method 属性,此时认为都需要免登录
 | 
			
		||||
            Set<RequestMethod> methods = entry.getKey().getMethodsCondition().getMethods();
 | 
			
		||||
            if (CollUtil.isEmpty(methods)) { //
 | 
			
		||||
                result.putAll(HttpMethod.GET, urls);
 | 
			
		||||
                result.putAll(HttpMethod.POST, urls);
 | 
			
		||||
                result.putAll(HttpMethod.PUT, urls);
 | 
			
		||||
                result.putAll(HttpMethod.DELETE, urls);
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            // 根据请求方法,添加到 result 结果
 | 
			
		||||
            entry.getKey().getMethodsCondition().getMethods().forEach(requestMethod -> {
 | 
			
		||||
                switch (requestMethod) {
 | 
			
		||||
                    case GET:
 | 
			
		||||
                        result.putAll(HttpMethod.GET, urls);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case POST:
 | 
			
		||||
                        result.putAll(HttpMethod.POST, urls);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case PUT:
 | 
			
		||||
                        result.putAll(HttpMethod.PUT, urls);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case DELETE:
 | 
			
		||||
                        result.putAll(HttpMethod.DELETE, urls);
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,68 @@
 | 
			
		||||
package com.jeelowcode.tool.framework.security.core;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.map.MapUtil;
 | 
			
		||||
import com.jeelowcode.tool.framework.common.enums.UserTypeEnum;
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonIgnore;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 登录用户信息
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Data
 | 
			
		||||
public class LoginUser {
 | 
			
		||||
 | 
			
		||||
    public static final String INFO_KEY_NICKNAME = "nickname";
 | 
			
		||||
    public static final String INFO_KEY_DEPT_ID = "deptId";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 用户编号
 | 
			
		||||
     */
 | 
			
		||||
    private Long id;
 | 
			
		||||
    /**
 | 
			
		||||
     * 用户类型
 | 
			
		||||
     *
 | 
			
		||||
     * 关联 {@link UserTypeEnum}
 | 
			
		||||
     */
 | 
			
		||||
    private Integer userType;
 | 
			
		||||
    /**
 | 
			
		||||
     * 租户编号
 | 
			
		||||
     */
 | 
			
		||||
    private Long tenantId;
 | 
			
		||||
    /**
 | 
			
		||||
     * 授权范围
 | 
			
		||||
     */
 | 
			
		||||
    private List<String> scopes;
 | 
			
		||||
 | 
			
		||||
    private Long loginDeptId;
 | 
			
		||||
 | 
			
		||||
    private Long loginRoleId;
 | 
			
		||||
 | 
			
		||||
    private boolean supAdminFlag;
 | 
			
		||||
 | 
			
		||||
    // ========== 上下文 ==========
 | 
			
		||||
    /**
 | 
			
		||||
     * 上下文字段,不进行持久化
 | 
			
		||||
     *
 | 
			
		||||
     * 1. 用于基于 LoginUser 维度的临时缓存
 | 
			
		||||
     */
 | 
			
		||||
    @JsonIgnore
 | 
			
		||||
    private Map<String, Object> context;
 | 
			
		||||
 | 
			
		||||
    public void setContext(String key, Object value) {
 | 
			
		||||
        if (context == null) {
 | 
			
		||||
            context = new HashMap<>();
 | 
			
		||||
        }
 | 
			
		||||
        context.put(key, value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public <T> T getContext(String key, Class<T> type) {
 | 
			
		||||
        return MapUtil.get(context, key, type);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
package com.jeelowcode.tool.framework.security.core.annotations;
 | 
			
		||||
 | 
			
		||||
import java.lang.annotation.*;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 声明用户需要登录
 | 
			
		||||
 *
 | 
			
		||||
 * 为什么不使用 {@link org.springframework.security.access.prepost.PreAuthorize} 注解,原因是不通过时,抛出的是认证不通过,而不是未登录
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Target({ElementType.METHOD})
 | 
			
		||||
@Retention(RetentionPolicy.RUNTIME)
 | 
			
		||||
@Inherited
 | 
			
		||||
@Documented
 | 
			
		||||
public @interface PreAuthenticated {
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
package com.jeelowcode.tool.framework.security.core.aop;
 | 
			
		||||
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.annotations.PreAuthenticated;
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.util.SecurityFrameworkUtils;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.aspectj.lang.ProceedingJoinPoint;
 | 
			
		||||
import org.aspectj.lang.annotation.Around;
 | 
			
		||||
import org.aspectj.lang.annotation.Aspect;
 | 
			
		||||
 | 
			
		||||
import static com.jeelowcode.tool.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED;
 | 
			
		||||
import static com.jeelowcode.tool.framework.common.exception.util.ServiceExceptionUtil.exception;
 | 
			
		||||
 | 
			
		||||
@Aspect
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class PreAuthenticatedAspect {
 | 
			
		||||
 | 
			
		||||
    @Around("@annotation(preAuthenticated)")
 | 
			
		||||
    public Object around(ProceedingJoinPoint joinPoint, PreAuthenticated preAuthenticated) throws Throwable {
 | 
			
		||||
        if (SecurityFrameworkUtils.getLoginUser() == null) {
 | 
			
		||||
            throw exception(UNAUTHORIZED);
 | 
			
		||||
        }
 | 
			
		||||
        return joinPoint.proceed();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,48 @@
 | 
			
		||||
package com.jeelowcode.tool.framework.security.core.context;
 | 
			
		||||
 | 
			
		||||
import com.alibaba.ttl.TransmittableThreadLocal;
 | 
			
		||||
import org.springframework.security.core.context.SecurityContext;
 | 
			
		||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
 | 
			
		||||
import org.springframework.security.core.context.SecurityContextImpl;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 基于 TransmittableThreadLocal 实现的 Security Context 持有者策略
 | 
			
		||||
 * 目的是,避免 @Async 等异步执行时,原生 ThreadLocal 的丢失问题
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
public class TransmittableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 使用 TransmittableThreadLocal 作为上下文
 | 
			
		||||
     */
 | 
			
		||||
    private static final ThreadLocal<SecurityContext> CONTEXT_HOLDER = new TransmittableThreadLocal<>();
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void clearContext() {
 | 
			
		||||
        CONTEXT_HOLDER.remove();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public SecurityContext getContext() {
 | 
			
		||||
        SecurityContext ctx = CONTEXT_HOLDER.get();
 | 
			
		||||
        if (ctx == null) {
 | 
			
		||||
            ctx = createEmptyContext();
 | 
			
		||||
            CONTEXT_HOLDER.set(ctx);
 | 
			
		||||
        }
 | 
			
		||||
        return ctx;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setContext(SecurityContext context) {
 | 
			
		||||
        Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
 | 
			
		||||
        CONTEXT_HOLDER.set(context);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public SecurityContext createEmptyContext() {
 | 
			
		||||
        return new SecurityContextImpl();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,117 @@
 | 
			
		||||
package com.jeelowcode.tool.framework.security.core.filter;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.util.ObjectUtil;
 | 
			
		||||
import cn.hutool.core.util.StrUtil;
 | 
			
		||||
import com.jeelowcode.tool.framework.common.exception.ServiceException;
 | 
			
		||||
import com.jeelowcode.tool.framework.common.pojo.CommonResult;
 | 
			
		||||
import com.jeelowcode.tool.framework.common.util.servlet.ServletUtils;
 | 
			
		||||
import com.jeelowcode.tool.framework.security.config.SecurityProperties;
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.LoginUser;
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.util.SecurityFrameworkUtils;
 | 
			
		||||
import com.jeelowcode.tool.framework.web.core.handler.GlobalExceptionHandler;
 | 
			
		||||
import com.jeelowcode.tool.framework.web.core.util.WebFrameworkUtils;
 | 
			
		||||
import com.jeelowcode.service.system.api.IApiOAuth2TokenApi;
 | 
			
		||||
import com.jeelowcode.service.system.dto.OAuth2AccessTokenCheckRespDTO;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import org.springframework.security.access.AccessDeniedException;
 | 
			
		||||
import org.springframework.web.filter.OncePerRequestFilter;
 | 
			
		||||
 | 
			
		||||
import javax.servlet.FilterChain;
 | 
			
		||||
import javax.servlet.ServletException;
 | 
			
		||||
import javax.servlet.http.HttpServletRequest;
 | 
			
		||||
import javax.servlet.http.HttpServletResponse;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Token 过滤器,验证 token 的有效性
 | 
			
		||||
 * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@RequiredArgsConstructor
 | 
			
		||||
public class TokenAuthenticationFilter extends OncePerRequestFilter {
 | 
			
		||||
 | 
			
		||||
    private final SecurityProperties securityProperties;
 | 
			
		||||
 | 
			
		||||
    private final GlobalExceptionHandler globalExceptionHandler;
 | 
			
		||||
 | 
			
		||||
    private final IApiOAuth2TokenApi oauth2TokenApi;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    @SuppressWarnings("NullableProblems")
 | 
			
		||||
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
 | 
			
		||||
            throws ServletException, IOException {
 | 
			
		||||
        String token = SecurityFrameworkUtils.obtainAuthorization(request,
 | 
			
		||||
                securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
 | 
			
		||||
        if (StrUtil.isNotEmpty(token)) {
 | 
			
		||||
            Integer userType = WebFrameworkUtils.getLoginUserType(request);
 | 
			
		||||
            try {
 | 
			
		||||
                // 1.1 基于 token 构建登录用户
 | 
			
		||||
                LoginUser loginUser = buildLoginUserByToken(token, userType);
 | 
			
		||||
                // 1.2 模拟 Login 功能,方便日常开发调试
 | 
			
		||||
                if (loginUser == null) {
 | 
			
		||||
                    loginUser = mockLoginUser(request, token, userType);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // 2. 设置当前用户
 | 
			
		||||
                if (loginUser != null) {
 | 
			
		||||
                    SecurityFrameworkUtils.setLoginUser(loginUser, request);
 | 
			
		||||
                }
 | 
			
		||||
            } catch (Throwable ex) {
 | 
			
		||||
                CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
 | 
			
		||||
                ServletUtils.writeJSON(response, result);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 继续过滤链
 | 
			
		||||
        chain.doFilter(request, response);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private LoginUser buildLoginUserByToken(String token, Integer userType) {
 | 
			
		||||
        try {
 | 
			
		||||
            OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token);
 | 
			
		||||
            if (accessToken == null) {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
            // 用户类型不匹配,无权限
 | 
			
		||||
            // 注意:只有 /admin-api/* 和 /app-api/* 有 userType,才需要比对用户类型
 | 
			
		||||
            // 类似 WebSocket 的 /ws/* 连接地址,是不需要比对用户类型的
 | 
			
		||||
            if (userType != null
 | 
			
		||||
                    && ObjectUtil.notEqual(accessToken.getUserType(), userType)) {
 | 
			
		||||
                throw new AccessDeniedException("错误的用户类型");
 | 
			
		||||
            }
 | 
			
		||||
            // 构建登录用户
 | 
			
		||||
            return new LoginUser().setId(accessToken.getUserId()).setLoginDeptId(accessToken.getLoginDeptId()).setLoginRoleId(accessToken.getLoginRoleId()).setSupAdminFlag(accessToken.isSupAdminFlag()).setUserType(accessToken.getUserType())
 | 
			
		||||
                    .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes());
 | 
			
		||||
        } catch (ServiceException serviceException) {
 | 
			
		||||
            // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 模拟登录用户,方便日常开发调试
 | 
			
		||||
     *
 | 
			
		||||
     * 注意,在线上环境下,一定要关闭该功能!!!
 | 
			
		||||
     *
 | 
			
		||||
     * @param request 请求
 | 
			
		||||
     * @param token 模拟的 token,格式为 {@link SecurityProperties#getMockSecret()} + 用户编号
 | 
			
		||||
     * @param userType 用户类型
 | 
			
		||||
     * @return 模拟的 LoginUser
 | 
			
		||||
     */
 | 
			
		||||
    private LoginUser mockLoginUser(HttpServletRequest request, String token, Integer userType) {
 | 
			
		||||
        if (!securityProperties.getMockEnable()) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        // 必须以 mockSecret 开头
 | 
			
		||||
        if (!token.startsWith(securityProperties.getMockSecret())) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        // 构建模拟用户
 | 
			
		||||
        Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length()));
 | 
			
		||||
        return new LoginUser().setId(userId).setUserType(userType)
 | 
			
		||||
                .setTenantId(WebFrameworkUtils.getTenantId(request));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,41 @@
 | 
			
		||||
package com.jeelowcode.tool.framework.security.core.handler;
 | 
			
		||||
 | 
			
		||||
import com.jeelowcode.tool.framework.common.exception.enums.GlobalErrorCodeConstants;
 | 
			
		||||
import com.jeelowcode.tool.framework.common.pojo.CommonResult;
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.util.SecurityFrameworkUtils;
 | 
			
		||||
import com.jeelowcode.tool.framework.common.util.servlet.ServletUtils;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.springframework.security.access.AccessDeniedException;
 | 
			
		||||
import org.springframework.security.web.access.AccessDeniedHandler;
 | 
			
		||||
import org.springframework.security.web.access.ExceptionTranslationFilter;
 | 
			
		||||
 | 
			
		||||
import javax.servlet.FilterChain;
 | 
			
		||||
import javax.servlet.ServletException;
 | 
			
		||||
import javax.servlet.http.HttpServletRequest;
 | 
			
		||||
import javax.servlet.http.HttpServletResponse;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
 | 
			
		||||
import static com.jeelowcode.tool.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。
 | 
			
		||||
 *
 | 
			
		||||
 * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
@SuppressWarnings("JavadocReference")
 | 
			
		||||
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
 | 
			
		||||
            throws IOException, ServletException {
 | 
			
		||||
        // 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏
 | 
			
		||||
        log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(),
 | 
			
		||||
                SecurityFrameworkUtils.getLoginUserId(), e);
 | 
			
		||||
        // 返回 403
 | 
			
		||||
        ServletUtils.writeJSON(response, CommonResult.error(FORBIDDEN));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,35 @@
 | 
			
		||||
package com.jeelowcode.tool.framework.security.core.handler;
 | 
			
		||||
 | 
			
		||||
import com.jeelowcode.tool.framework.common.exception.enums.GlobalErrorCodeConstants;
 | 
			
		||||
import com.jeelowcode.tool.framework.common.pojo.CommonResult;
 | 
			
		||||
import com.jeelowcode.tool.framework.common.util.servlet.ServletUtils;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.springframework.security.core.AuthenticationException;
 | 
			
		||||
import org.springframework.security.web.AuthenticationEntryPoint;
 | 
			
		||||
import org.springframework.security.web.access.ExceptionTranslationFilter;
 | 
			
		||||
 | 
			
		||||
import javax.servlet.FilterChain;
 | 
			
		||||
import javax.servlet.http.HttpServletRequest;
 | 
			
		||||
import javax.servlet.http.HttpServletResponse;
 | 
			
		||||
 | 
			
		||||
import static com.jeelowcode.tool.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页
 | 
			
		||||
 *
 | 
			
		||||
 * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类
 | 
			
		||||
 *
 | 
			
		||||
 * @author ruoyi
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
@SuppressWarnings("JavadocReference") // 忽略文档引用报错
 | 
			
		||||
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
 | 
			
		||||
        log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e);
 | 
			
		||||
        // 返回 401
 | 
			
		||||
        ServletUtils.writeJSON(response, CommonResult.error(UNAUTHORIZED));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,59 @@
 | 
			
		||||
package com.jeelowcode.tool.framework.security.core.service;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Security 框架 Service 接口,定义权限相关的校验操作
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
public interface SecurityFrameworkService {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 判断是否有权限
 | 
			
		||||
     *
 | 
			
		||||
     * @param permission 权限
 | 
			
		||||
     * @return 是否
 | 
			
		||||
     */
 | 
			
		||||
    boolean hasPermission(String permission);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 判断是否有权限,任一一个即可
 | 
			
		||||
     *
 | 
			
		||||
     * @param permissions 权限
 | 
			
		||||
     * @return 是否
 | 
			
		||||
     */
 | 
			
		||||
    boolean hasAnyPermissions(String... permissions);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 判断是否有角色
 | 
			
		||||
     *
 | 
			
		||||
     * 注意,角色使用的是 SysRoleDO 的 code 标识
 | 
			
		||||
     *
 | 
			
		||||
     * @param role 角色
 | 
			
		||||
     * @return 是否
 | 
			
		||||
     */
 | 
			
		||||
    boolean hasRole(String role);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 判断是否有角色,任一一个即可
 | 
			
		||||
     *
 | 
			
		||||
     * @param roles 角色数组
 | 
			
		||||
     * @return 是否
 | 
			
		||||
     */
 | 
			
		||||
    boolean hasAnyRoles(String... roles);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 判断是否有授权
 | 
			
		||||
     *
 | 
			
		||||
     * @param scope 授权
 | 
			
		||||
     * @return 是否
 | 
			
		||||
     */
 | 
			
		||||
    boolean hasScope(String scope);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 判断是否有授权范围,任一一个即可
 | 
			
		||||
     *
 | 
			
		||||
     * @param scope 授权范围数组
 | 
			
		||||
     * @return 是否
 | 
			
		||||
     */
 | 
			
		||||
    boolean hasAnyScopes(String... scope);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,57 @@
 | 
			
		||||
package com.jeelowcode.tool.framework.security.core.service;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.collection.CollUtil;
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.LoginUser;
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.util.SecurityFrameworkUtils;
 | 
			
		||||
import com.jeelowcode.service.system.api.IApiPermissionApi;
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
 | 
			
		||||
import static com.jeelowcode.tool.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 默认的 {@link SecurityFrameworkService} 实现类
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
@AllArgsConstructor
 | 
			
		||||
public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
 | 
			
		||||
 | 
			
		||||
    private final IApiPermissionApi apiPermissionApi;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean hasPermission(String permission) {
 | 
			
		||||
        return hasAnyPermissions(permission);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean hasAnyPermissions(String... permissions) {
 | 
			
		||||
        return apiPermissionApi.hasAnyPermissions(getLoginUserId(), permissions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean hasRole(String role) {
 | 
			
		||||
        return hasAnyRoles(role);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean hasAnyRoles(String... roles) {
 | 
			
		||||
        return apiPermissionApi.hasAnyRoles(getLoginUserId(), roles);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean hasScope(String scope) {
 | 
			
		||||
        return hasAnyScopes(scope);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean hasAnyScopes(String... scope) {
 | 
			
		||||
        LoginUser user = SecurityFrameworkUtils.getLoginUser();
 | 
			
		||||
        if (user == null) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        return CollUtil.containsAny(user.getScopes(), Arrays.asList(scope));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,133 @@
 | 
			
		||||
package com.jeelowcode.tool.framework.security.core.util;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.util.StrUtil;
 | 
			
		||||
import com.jeelowcode.tool.framework.security.core.LoginUser;
 | 
			
		||||
import com.jeelowcode.tool.framework.web.core.util.WebFrameworkUtils;
 | 
			
		||||
import org.springframework.lang.Nullable;
 | 
			
		||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 | 
			
		||||
import org.springframework.security.core.Authentication;
 | 
			
		||||
import org.springframework.security.core.context.SecurityContext;
 | 
			
		||||
import org.springframework.security.core.context.SecurityContextHolder;
 | 
			
		||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
 | 
			
		||||
import org.springframework.util.StringUtils;
 | 
			
		||||
 | 
			
		||||
import javax.servlet.http.HttpServletRequest;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 安全服务工具类
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
public class SecurityFrameworkUtils {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * HEADER 认证头 value 的前缀
 | 
			
		||||
     */
 | 
			
		||||
    public static final String AUTHORIZATION_BEARER = "Bearer";
 | 
			
		||||
 | 
			
		||||
    private SecurityFrameworkUtils() {}
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 从请求中,获得认证 Token
 | 
			
		||||
     *
 | 
			
		||||
     * @param request 请求
 | 
			
		||||
     * @param headerName 认证 Token 对应的 Header 名字
 | 
			
		||||
     * @param parameterName 认证 Token 对应的 Parameter 名字
 | 
			
		||||
     * @return 认证 Token
 | 
			
		||||
     */
 | 
			
		||||
    public static String obtainAuthorization(HttpServletRequest request,
 | 
			
		||||
                                             String headerName, String parameterName) {
 | 
			
		||||
        // 1. 获得 Token。优先级:Header > Parameter
 | 
			
		||||
        String token = request.getHeader(headerName);
 | 
			
		||||
        if (StrUtil.isEmpty(token)) {
 | 
			
		||||
            token = request.getParameter(parameterName);
 | 
			
		||||
        }
 | 
			
		||||
        if (!StringUtils.hasText(token)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        // 2. 去除 Token 中带的 Bearer
 | 
			
		||||
        int index = token.indexOf(AUTHORIZATION_BEARER + " ");
 | 
			
		||||
        return index >= 0 ? token.substring(index + 7).trim() : token;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获得当前认证信息
 | 
			
		||||
     *
 | 
			
		||||
     * @return 认证信息
 | 
			
		||||
     */
 | 
			
		||||
    public static Authentication getAuthentication() {
 | 
			
		||||
        SecurityContext context = SecurityContextHolder.getContext();
 | 
			
		||||
        if (context == null) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        return context.getAuthentication();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取当前用户
 | 
			
		||||
     *
 | 
			
		||||
     * @return 当前用户
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public static LoginUser getLoginUser() {
 | 
			
		||||
        Authentication authentication = getAuthentication();
 | 
			
		||||
        if (authentication == null) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获得当前用户的编号,从上下文中
 | 
			
		||||
     *
 | 
			
		||||
     * @return 用户编号
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public static Long getLoginUserId() {
 | 
			
		||||
        LoginUser loginUser = getLoginUser();
 | 
			
		||||
        return loginUser != null ? loginUser.getId() : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public static boolean isSupAdmin() {
 | 
			
		||||
        LoginUser loginUser = getLoginUser();
 | 
			
		||||
        return loginUser != null ? loginUser.isSupAdminFlag() : false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Long getLoginDeptId() {
 | 
			
		||||
        LoginUser loginUser = getLoginUser();
 | 
			
		||||
        return loginUser != null ? loginUser.getLoginDeptId() : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Long getLoginRoleId() {
 | 
			
		||||
        LoginUser loginUser = getLoginUser();
 | 
			
		||||
        return loginUser != null ? loginUser.getLoginRoleId() : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 设置当前用户
 | 
			
		||||
     *
 | 
			
		||||
     * @param loginUser 登录用户
 | 
			
		||||
     * @param request 请求
 | 
			
		||||
     */
 | 
			
		||||
    public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {
 | 
			
		||||
        // 创建 Authentication,并设置到上下文
 | 
			
		||||
        Authentication authentication = buildAuthentication(loginUser, request);
 | 
			
		||||
        SecurityContextHolder.getContext().setAuthentication(authentication);
 | 
			
		||||
 | 
			
		||||
        // 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号;
 | 
			
		||||
        // 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息
 | 
			
		||||
        WebFrameworkUtils.setLoginUserId(request, loginUser.getId());
 | 
			
		||||
        WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {
 | 
			
		||||
        // 创建 UsernamePasswordAuthenticationToken 对象
 | 
			
		||||
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
 | 
			
		||||
                loginUser, null, Collections.emptyList());
 | 
			
		||||
        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
 | 
			
		||||
        return authenticationToken;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 基于 Spring Security 框架
 | 
			
		||||
 * 实现安全认证功能
 | 
			
		||||
 *
 | 
			
		||||
 * @author 芋道源码
 | 
			
		||||
 */
 | 
			
		||||
package com.jeelowcode.tool.framework.security;
 | 
			
		||||
@@ -0,0 +1,3 @@
 | 
			
		||||
com.jeelowcode.tool.framework.security.config.SecurityAutoConfiguration
 | 
			
		||||
com.jeelowcode.tool.framework.security.config.WebSecurityConfigurerAdapter
 | 
			
		||||
com.jeelowcode.tool.framework.operatelog.config.OperateLogV2Configuration
 | 
			
		||||
		Reference in New Issue
	
	Block a user