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()
方法中来定义过滤器处理逻辑,首先,从请求头中获取 key
为 Authorization
的值,判断是否以 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