spring security 使用过滤器认证登录时,抛出自定义异常

前情提要

最近在做项目的改造,涉及到新增用户的离职冻结状态,当被离职/冻结后,尝试登录系统,则抛出不同的异常代码给前端,前端依据不同的异常代码提示不同的文本。所以需要对项目的认证逻辑简单调整,增加按照不同的登录用户的状态(离职/冻结)判断,如果满足指定状态,则抛出对应的异常代码。

认证逻辑

使用 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)

如果不加上注释的代码,此时如果登录人账号状态不是正常状态,则会报错
自己抛出的异常 是 AccountExpiredException,但是定义的异常被过滤器链中记录并吞掉了, 修改为了 InsufficientAuthenticationException,并且啥类型的异常都会被改掉,并且往前端写的code 为 AUTHENTICATION_FAILED。

{
    "uuid": "DF2E00186E1F44ABB579C351F5E70463",
    "code": "AUTHENTICATION_FAILED",
    "message": null,
    "data": null
}

loadUserByUsername 中触发抛出异常的代码如下:

        if ("-1".equals(user.getUserState())) {
            throw new LockedException("用户被锁定");
        }
        if ("1".equals(user.getUserState()) || "3".equals(user.getUserState())) {
            throw new AccountExpiredException("用户被冻结或离职");
        }

原因:需要在触发异常后手动调用异常处理类 AuthenticationEntryPoint 结束调用。就是上面被注释的代码,customAuthenticationEntryPoint.commence(request, response, e);
此时再次登录测试,就能够看到返回的自定义的code了, 后台报错也是自定义的异常类型:

{
    "uuid": "A89936FC642A42688D2706BA5B74A21E",
    "code": "USER_NOT_FOUND_ERROR",
    "message": "离职异动",
    "data": null
}
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)

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
主要思路则是重新捕获被重写的异常信息,再抛出来一次

以上,就能解决该问题了~

posted @ 2024-05-17 14:54  charler。  阅读(660)  评论(0编辑  收藏  举报