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

文章目录

      • 场景
      • 实现
      • 认证过滤器具体实现
      • 认证过滤器放到Spring Security UsernamePasswordAuthenticationFilter过滤器之前

场景

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;@Overrideprotected 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) {//  校验tokenOAuth2AccessTokenCheckRespDTO 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());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));return authentication;}
}

认证过滤器放到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 {@Autowiredprivate AuthenticationEntryPoint authenticationEntryPoint;@Autowiredprivate AccessDeniedHandler accessDeniedHandler;@Resourceprivate TokenAuthenticationFilter tokenAuthenticationFilter;/*** 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入* 通过覆写父类的该方法,添加 @Bean 注解,解决该问题*/@Beanpublic 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       |   用户登录后可访问*/@Beanprotected 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();}}