SpringSecurity配置 2

SpringSecurity配置 2

目前的现状,虽然是有了登录认证的接口,但是登录完成后,当我们访问受保护的接口时,即使将 Token 令牌携带与请求一起发送,依然是无法请求成功。另外,提示信息如下图所示,非常不友好,和之前定义好的统一返参格式也不一致。

1. 新建 Token 校验过滤器

首先,我们在 weblog-module-jwt 模块下的 /filter 包下,创建 TokenAuthenticationFilter 过滤器,用于专门校验 Token 令牌,代码如下:

java 复制代码
@Slf4j
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenHelper jwtTokenHelper;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 从请求头中获取 key 为 Authorization 的值
        String header = request.getHeader("Authorization");

        // 判断 value 值是否以 Bearer 开头
        if (StringUtils.startsWith(header, "Bearer")) {
            // 截取 Token 令牌
            String token = StringUtils.substring(header, 7);
            log.info("Token: {}", token);

            // 判空 Token
            if (StringUtils.isNotBlank(token)) {
                try {
                    // 校验 Token 是否可用, 若解析异常,针对不同异常做出不同的响应参数
                    jwtTokenHelper.validateToken(token);
                } catch (SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) {
                    // 抛出异常,统一让 AuthenticationEntryPoint 处理响应参数
                    authenticationEntryPoint.commence(request, response, new AuthenticationServiceException("Token 不可用"));
                    return;
                } catch (ExpiredJwtException e) {
                    authenticationEntryPoint.commence(request, response, new AuthenticationServiceException("Token 已失效"));
                    return;
                }

                // 从 Token 中解析出用户名
                String username = jwtTokenHelper.getUsernameByToken(token);
                
                if (StringUtils.isNotBlank(username)
                        && Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {
                    // 根据用户名获取用户详情信息
                    UserDetails userDetails = userDetailsService.loadUserByUsername(username);

                    // 将用户信息存入 authentication,方便后续校验
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, //用户详细信息,凭证,用户权限列表
                            userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    // 将 authentication 存入 ThreadLocal,方便后续获取用户信息
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }

        // 继续执行写一个过滤器
        filterChain.doFilter(request, response);
    }
}

上述代码中,我们自定义了一个 Spring Security 过滤器 TokenAuthenticationFilter,它继承了 OncePerRequestFilter,确保每个请求只被过滤一次。在重写的 doFilterInternal() 方法中来定义过滤器处理逻辑,首先,从请求头中获取 keyAuthorization 的值,判断是否以 Bearer 开头,若是,截取出 Token, 对其进行解析,并对可能抛出的异常做出不同的返参。最后,我们获取了用户详情信息,将用户信息存入 authentication,方便后续进行校验,同时将 authentication 存入 ThreadLocal 中,方便后面方便的获取用户信息。

2. JwtTokenHelper 新增方法

上述过滤器中,需要在 JwtTokenHelper 工具类中添加两个方法:

校验 Token 是否可用;

解析 Token 获取用户名;

java 复制代码
    /**
     * 校验 Token 是否可用
     * @param token
     * @return
     */
    public void validateToken(String token) {
        jwtParser.parseClaimsJws(token);
    }

    /**
     * 解析 Token 获取用户名
     * @param token
     * @return
     */
    public String getUsernameByToken(String token) {
        try {
            Claims claims = jwtParser.parseClaimsJws(token).getBody();
            String username = claims.getSubject();
            return username;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

3. 添加处理器

/handler 包下,新增 RestAuthenticationEntryPoint 处理器,它专门用来处理当用户未登录时,访问受保护的资源的情况:

java 复制代码
/**
 * @author: 犬小哈
 * @url: www.quanxiaoha.com
 * @date: 2023-08-27 17:27
 * @description: 用户未登录访问受保护的资源
 **/
@Slf4j
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        log.warn("用户未登录访问受保护的资源: ", authException);
        if (authException instanceof InsufficientAuthenticationException) {
            ResultUtil.fail(response, HttpStatus.UNAUTHORIZED.value(), Response.fail(ResponseCodeEnum.UNAUTHORIZED));
			return;
        }

        ResultUtil.fail(response, HttpStatus.UNAUTHORIZED.value(), Response.fail(authException.getMessage()));
    }
}

当请求相关受保护的接口时,请求头中未携带 Token 令牌,通过日志打印异常信息,你会发现对应的异常类型

对应的,你需要在 ResponseCodeEnum 枚举类中添加 UNAUTHORIZED 枚举值,代码如下:

java 复制代码
UNAUTHORIZED("20002", "无访问权限,请先登录!"),

4. RestAccessDeniedHandler

然后,在 /handler 包下,另外新增 RestAccessDeniedHandler 处理器,它主要用于处理当用户登录成功时,访问受保护的资源,但是权限不够的情况:

java 复制代码
/**
 * @author: 犬小哈
 * @url: www.quanxiaoha.com
 * @date: 2023-08-27 17:32
 * @description: 登录成功访问收保护的资源,但是权限不够
 **/
@Slf4j
@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        log.warn("登录成功访问收保护的资源,但是权限不够: ", accessDeniedException);
        // 预留,后面引入多角色时会用到
    }
}

5. 修改 Spring Security 配置

完成以上 Token 校验过滤器相关的开发后,接下来,需要将它们添加到 Spring Security 中,编辑 WebSecurityConfig 配置类,修改内容如下:

java 复制代码
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;
    @Autowired
    private RestAuthenticationEntryPoint authEntryPoint;
    @Autowired
    private RestAccessDeniedHandler deniedHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable(). // 禁用 csrf
                formLogin().disable() // 禁用表单登录
                .apply(jwtAuthenticationSecurityConfig) // 设置用户登录认证相关配置
             .and()
                .authorizeHttpRequests()
                .mvcMatchers("/admin/**").authenticated() // 认证所有以 /admin 为前缀的 URL 资源
                .anyRequest().permitAll() // 其他都需要放行,无需认证
             .and()
                .httpBasic().authenticationEntryPoint(authEntryPoint) // 处理用户未登录访问受保护的资源的情况
             .and()
                .exceptionHandling().accessDeniedHandler(deniedHandler) // 处理登录成功后访问受保护的资源,但是权限不够的情况
             .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 前后端分离,无需创建会话
             .and()
                .addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) // 将 Token 校验过滤器添加到用户认证过滤器之前
                ;
    }

    /**
     * Token 校验过滤器
     * @return
     */
    @Bean
    public TokenAuthenticationFilter tokenAuthenticationFilter() {
        return new TokenAuthenticationFilter();
    }

}

上述代码中,我们注入了前面写好的相关类,如过滤器、处理器等,主要改动代码如下:

  • .httpBasic().authenticationEntryPoint(authEntryPoint) : 处理用户未登录访问受保护的资源的情况;
  • .exceptionHandling().accessDeniedHandler(deniedHandler) : 处理登录成功后访问受保护的资源,但是权限不够的情况;
  • .addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) : 将 Token 校验过滤器添加到用户认证过滤器之前;

6. 相关变量提取到 application.yml 中

鉴权功能正常开发完毕后,你会发现已经开发好的代码中,还有一些写死的变量,如请求头的 key, 令牌的失效时间等,其实都是可以提取到 applicaiton.yml 配置文件中,方便后续统一维护的:

yaml 复制代码
jwt:
  # token 过期时间(单位:分钟) 24*60
  tokenExpireTime: 1440
  # token 请求头中的 key 值
  tokenHeaderKey: Authorization
  # token 请求头中的 value 值前缀
  tokenPrefix: Bearer