前情提要
最近在做项目的改造,涉及到新增用户的离职冻结状态,当被离职/冻结后,尝试登录系统,则抛出不同的异常代码给前端,前端依据不同的异常代码提示不同的文本。所以需要对项目的认证逻辑简单调整,增加按照不同的登录用户的状态(离职/冻结)判断,如果满足指定状态,则抛出对应的异常代码。
认证逻辑
使用 jwt 认证登录请求到 sso -> sso 认证成功携带 jwt token 进入登录的项目 -> 项目解析jwt的token,得到登录的用户id,用该id去取项目维护的用户表,不同用户状态抛出不同的错误代码
例如
用户离职,抛出 AccountExpiredException 并输出对应的错误代码给前端
用户冻结,抛出 LockedException 并输出对应的错误代码给前端
处理方式
一开始想要在userdetailService中抛出自定义的异常,但是他默认抛出的是UsernameNotFoundException 需要override 一下并修改抛出的异常为他的父类异常 AuthenticationException,这样就可以抛出来 集成了AuthenticationException的各种类型的自定义异常了,例如:
userdetailservice.loadUserByUsername:
if ("-1".equals(user.getUserState())) {
throw new LockedException("用户被锁定");
}
if ("1".equals(user.getUserState()) || "3".equals(user.getUserState())) {
throw new AccountExpiredException("用户被冻结或离职");
}
然后是在使用spring security 配置过滤器时,需要根据使用的不同的过滤器去调整过滤器逻辑:
比如使用的是 OncePerRequestFilter,在 doFilterInternal 方法中调用 loadUserByUsername去校验用户状态时,如果报错不进行处理,异常会被记录,并且覆盖掉,例如spring security 在 SecurityConfiguration 中这么配置:
http
.authorizeRequests()
.anyRequest()
.authenticated()
.and().addFilterBefore(new OncePerRequestFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = request.getHeader(tokenName);
DecodedJWT decodedJWT = verifierHMAC512.verify(jwt);
String decodedPayload = StringUtil.decode(decodedJWT.getPayload());
Map<String, String> map = objectMapper.readValue(decodedPayload, valueTypeRef);
String userId = map.get("iss");
ReqUser reqUser = userDetailsService.loadUserByUsername(userId);
reqUser.setToken(jwt);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(reqUser, null, reqUser.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
ReqContext.set(reqUser);
filterChain.doFilter(request, response);
}
/** catch (AuthenticationException e) {
log.error("登录异常", e);
customAuthenticationEntryPoint.commence(request, response, e);
} **/
finally {
ReqContext.clearContext();
SecurityContextHolder.clearContext();
}
}
}, UsernamePasswordAuthenticationFilter.class)
如果不加上注释的代码,此时如果登录人账号状态不是正常状态,则会报错
org.springframework.security.authentication.AccountExpiredException: 用户被冻结或离职
at com.hikvision.fiit.fpc.core.FpcUserDetailsServiceImpl.loadUserByUsername(FpcUserDetailsServiceImpl.java:65)
at com.hikvision.fiit.fpc.config.SecurityConfiguration$2.doFilterInternal(SecurityConfiguration.java:104)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
省略...
2024-05-17 14:20:49.902 ERROR 3300 --- [nio-8088-exec-1] c.h.f.f.c.CustomAuthenticationEntryPoint : 认证失败, DF2E00186E1F44ABB579C351F5E70463
org.springframework.security.authentication.InsufficientAuthenticationException: Full authentication is required to access this resource
at org.springframework.security.web.access.ExceptionTranslationFilter.handleSpringSecurityException(ExceptionTranslationFilter.java:189)
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:140)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
loadUserByUsername 中触发抛出异常的代码如下:
if ("-1".equals(user.getUserState())) {
throw new LockedException("用户被锁定");
}
if ("1".equals(user.getUserState()) || "3".equals(user.getUserState())) {
throw new AccountExpiredException("用户被冻结或离职");
}
可以看到,自己抛出的异常 是 AccountExpiredException,但是返回给前端的异常是, 自定义的异常被过滤器链中记录并吞掉了, 修改为了 InsufficientAuthenticationException,并且啥类型的异常都会被改掉。
{
"uuid": "DF2E00186E1F44ABB579C351F5E70463",
"code": "AUTHENTICATION_FAILED",
"message": null,
"data": null
}
查阅了相关的资料,需要在触发异常后手动调用异常处理类 AuthenticationEntryPoint ,就可以解决了。就是上面被注释的代码,customAuthenticationEntryPoint.commence(request, response, e);
此时再次登录,就能够返回自定义的异常信息了:
{
"uuid": "A89936FC642A42688D2706BA5B74A21E",
"code": "USER_NOT_FOUND_ERROR",
"message": "离职异动",
"data": null
}
CustomAuthenticationEntryPoint 对应的实现:
@Slf4j
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
public CustomAuthenticationEntryPoint(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
ApiResult<Object> apiResult = new ApiResult<>();
String uuid = StringUtil.uuid();
log.error("认证失败, {}", uuid, e);
apiResult.setUuid(uuid);
if (e instanceof LockedException) {
apiResult.setCode(ErrorCode.USER_LOCK_ERROR.name());
apiResult.setMessage(ErrorCode.USER_LOCK_ERROR.message());
} else if (e instanceof AccountExpiredException) {
apiResult.setCode(ErrorCode.USER_NOT_FOUND_ERROR.name());
apiResult.setMessage(ErrorCode.USER_NOT_FOUND_ERROR.message());
} else {
apiResult.setCode(ErrorCode.AUTHENTICATION_FAILED.name());
response.setStatus(HttpStatus.UNAUTHORIZED.value());
}
String json = objectMapper.writeValueAsString(apiResult);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(json);
}
}
也可以参考这个处理方式,将错误信息放到request中,最后取request中的message取判断哪一种错误,再返回给前端
https://blog.csdn.net/weixin_43120308/article/details/109744070
如果用的是UsernamePasswordAuthenticationFilter
那么可以参照下面的处理方法:
https://blog.csdn.net/wwang_dev/article/details/119108639
主要思路则是重新捕获被重写的异常信息,再抛出来一次
以上,就能解决该问题了~
标签:spring,apiResult,request,过滤器,new,security,异常,response From: https://www.cnblogs.com/charler/p/18197793