使用自定义的token认证过滤器替换security的认证功能

场景

spring-boot-starter-security不够灵活, 项目需要二次开发。下面展示一个替换security认证功能的替代方案。

实现

  1. 项目实现生成accessToken以及checkAccessToken的功能
  2. 创建一个token过滤器,该过滤器有3部分组成:
    2.1 校验token是否有效
    2.2 根据token生成用户实体 生成Authentication
    2.3 SecurityContextHolder.getContext().setAuthentication(authentication); 将给定的 Authentication 对象设置为当前线程的安全上下文中,表示当前用户已经通过身份验证
  3. 将token认证过滤器加入到UsernamePasswordAuthenticationFilter过滤器之前

认证过滤器具体实现

@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    private final SecurityProperties securityProperties;

    private final GlobalExceptionHandler globalExceptionHandler;

    private final OAuth2TokenApi oAuth2TokenApi;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader());

        if (StrUtil.isNotBlank(token)) {
            Integer userType = SecurityFrameworkUtils.getLoginUserType(request);

            try {
                // 基于token构建登录用户
                LoginUser loginUser = buildLoginUserByToken(token, userType);

                // 设置当前用户
                if (loginUser != null) {
                    SecurityFrameworkUtils.setLoginUser(loginUser, request);
                }

            } catch (Throwable e) {
                CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, e);
                ServletUtils.writeJSON(response, result);
                return;
            }
        }

        filterChain.doFilter(request, response);
    }

    /**
     * 根据token构建用户
     */
    private LoginUser buildLoginUserByToken(String token, Integer userType) {
        //  校验token
        OAuth2AccessTokenCheckRespDTO tokenCheckRespDTO = oAuth2TokenApi.checkAccessToken(token);

        if (tokenCheckRespDTO == null) {
            return null;
        }

        // 校验用户类型
        if (ObjectUtil.notEqual(tokenCheckRespDTO.getUserType(), userType)) {
            throw new AccessDeniedException("用户类型错误");
        }

        //  构建登录用户
        return LoginUser.builder()
                .userType(userType)
                .id(tokenCheckRespDTO.getUserId())
                .tenantId(tokenCheckRespDTO.getTenantId())
                .scopes(tokenCheckRespDTO.getScopes())
                .build();
    }
}

public class SecurityFrameworkUtils {

    public final static String AUTHORIZATION_BEARER = "Bearer";
    private static final String REQUEST_ATTRIBUTE_LOGIN_USER_TYPE = "login_user_type";


    private SecurityFrameworkUtils() {
    }

    /**
     * 从header中提取token
     */
    public static String obtainAuthorization(HttpServletRequest request, String header) {
        String authorization = request.getHeader(header);
        if (StrUtil.isEmpty(authorization)) {
            return null;
        }

        if (!StrUtil.startWith(authorization, AUTHORIZATION_BEARER + " ")) {
            return null;
        }

        return authorization.substring(AUTHORIZATION_BEARER.length() + 1).trim();
    }

    /**
     * 获取当前用户类型
     */
    public static Integer getLoginUserType(HttpServletRequest request) {
        if (request == null) {
            return null;
        }

        Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE);
        if (userType != null) {
            return userType;
        }

        return UserTypeEnum.ADMIN.getValue();
    }

    /**
     * 设置用户
     */
    public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {
        // 创建 Authentication,并设置到上下文
        /**
         * SecurityContextHolder.getContext().setAuthentication(authentication) 是 Spring Security 中用于设置当前用户认证信息的方法。
         * 它的作用是将给定的 Authentication 对象设置为当前线程的安全上下文中,表示当前用户已经通过身份验证。
         * */
        Authentication authentication = buildAuthentication(loginUser,request);
        SecurityContextHolder.getContext().setAuthentication(authentication);

    }

    /**
     * 构建Authentication
     * */
    private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {
        /**
         * 创建 UsernamePasswordAuthenticationToken 对象
         * public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
         *         Collection<? extends GrantedAuthority> authorities);
         * principal: 表示身份验证的主体,通常是用户名或用户实体。
         * credentials: 表示凭证,通常是密码或其他用于验证身份的凭据。
         * authorities: 表示用户的权限集合,通常是一组 GrantedAuthority 对象。
         * 举个例子
         * // 定义用户信息
         * String username = "user";
         * String password = "password";
         *
         * // 定义用户权限
         * Collection<? extends GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
         *
         * // 创建 UsernamePasswordAuthenticationToken 实例
         * UsernamePasswordAuthenticationToken authenticationToken =
         *         new UsernamePasswordAuthenticationToken(username, password, authorities);
         * */
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList());
        // 建WebAuthenticationDetails对象并将其设置到Authentication对象中
        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

        return authentication;
    }
}
详细说说替换security认证功能的实现
  1. SecurityContextHolder.getContext().setAuthentication(authentication); 是 Spring Security 中用于设置当前用户认证信息的方法。它的作用是将给定的 Authentication 对象设置为当前线程的安全上下文中,表示当前用户已经通过身份验证
  2. Authentication应该怎么生成呢? 生成一个UsernamePasswordAuthenticationToken 实例就行, public UsernamePasswordAuthenticationToken(Object principal, Object credentials,Collection<? extends GrantedAuthority> authorities);
    2.1 principal: 表示身份验证的主体,通常是用户名或用户实体。
    2.2 credentials: 表示凭证,通常是密码或其他用于验证身份的凭据。
    2.3 authorities: 表示用户的权限集合,通常是一组 GrantedAuthority 对象。下面举个简单的例子
     String username = "user";
      String password = "password";

    // 定义用户权限
      Collection<? extends GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));

       // 创建 UsernamePasswordAuthenticationToken 实例
       UsernamePasswordAuthenticationToken authenticationToken =
       new UsernamePasswordAuthenticationToken(username, password, authorities);

认证过滤器放到Spring Security UsernamePasswordAuthenticationFilter过滤器之前

/**
 * @version V1.0
 * @author: carsonlius
 * @date: 2023/12/20 15:38
 * @company
 * @description 自定义的 Spring Security 配置适配器实现
 */
@AutoConfiguration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class DemoWebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    @Resource
    private TokenAuthenticationFilter tokenAuthenticationFilter;

    /**
     * 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入
     * 通过覆写父类的该方法,添加 @Bean 注解,解决该问题
     */
    @Bean
    public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authentication) throws Exception {
        return authentication.getAuthenticationManager();
    }

    /**
     * 配置 URL 的安全配置
     * <p>
     * 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().disable() // csrf禁用,因为不使用session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // STATELESS(无状态): 表示应用程序是无状态的,不会创建会话。这意味着每个请求都是独立的,不依赖于之前的请求。适用于 RESTful 风格的应用。
                .and().headers().frameOptions().disable()
                .and().exceptionHandling().authenticationEntryPoint(authenticationEntryPoint) // 身份未认证时响应
                .accessDeniedHandler(accessDeniedHandler); // 身份已经认证(登录),但是没有权限的情况的响应


        // 设置具体请求的权限
        httpSecurity.authorizeRequests()
                .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() // 静态资源无需认证
                .antMatchers("/websocket/message").permitAll() // websocket无需认证
                .antMatchers("/system/auth/login").permitAll()
                .and().authorizeRequests().anyRequest().authenticated(); // 其他请求必须认证

        httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return httpSecurity.build();
    }

}



  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值