在Spring Security过滤器中返回自定义错误

3

我正在开发一个使用JSON Web Tokens的Spring Boot和Spring Security应用程序。

我有一个Spring Security过滤器,检查现有JWT的存在,并且如果存在,则注入一个UsernamePasswordAuthenticationToken:

public class AuthenticationTokenFilter extends UsernamePasswordAuthenticationFilter {

    @Value("${api.token.header}")
    String tokenHeader;

    @Autowired
    TokenUtility tokenUtility;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;

        String incomingToken = httpRequest.getHeader(tokenHeader);

        if (SecurityContextHolder.getContext().getAuthentication() == null && incomingToken != null) {

            UserDetails userDetails = null;

            try {

                userDetails = tokenUtility.validateToken(incomingToken);

            } catch (TokenExpiredException e) {

                throw new ServletException("Token has expired", e);
            }

            if (userDetails != null) {

                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));

                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }
}

此过滤器注入的方式如下:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserDetailsService userDetailsService;

    @Autowired
    EntryPointUnauthorizedHandler unauthorizedHandler;

    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {

        authenticationManagerBuilder
                            .userDetailsService(userDetailsService)
                            .passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {

        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManager() throws Exception {

        return super.authenticationManager();
    }

    @Bean
    public AuthenticationTokenFilter authenticationTokenFilter() throws Exception {

        AuthenticationTokenFilter authenticationTokenFilter = new AuthenticationTokenFilter();
        authenticationTokenFilter.setAuthenticationManager(authenticationManager());

        return authenticationTokenFilter;
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {

        httpSecurity
            .csrf()
                .disable()
            .exceptionHandling()
                .authenticationEntryPoint(unauthorizedHandler)
                .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
            .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .antMatchers("/auth/**").permitAll()
                .anyRequest().authenticated();

        // filter injected here
        httpSecurity.addFilterBefore(authenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

如果用户传递了一个已过期的令牌,他们将收到以下错误提示:
{
    "timestamp":1496424964894,
    "status":500,
    "error":"Internal Server Error",
    "exception":"com.app.exceptions.TokenExpiredException",
    "message":"javax.servlet.ServletException: Token has expired",
    "path":"/orders"
}

我知道Spring Security会在请求到达控制器层之前拦截请求,因此我不能使用现有的@ControllerAdvice来处理这些异常。
我的问题是,如何自定义返回的错误消息/对象?在其他地方,我使用JSON序列化的POJO返回错误消息,我希望保持一致。我也不想让用户看到javax.servlet.ServletException。
2个回答

3
首先,修改JWTTokenProvider类以使用setAttribute()方法向Http Servlet请求添加自定义标头。
   public boolean validateToken(String token,HttpServletRequest httpServletRequest){
    try {
        Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
        return true;
    }catch (SignatureException ex){
        System.out.println("Invalid JWT Signature");
    }catch (MalformedJwtException ex){
        System.out.println("Invalid JWT token");
    }catch (ExpiredJwtException ex){
        System.out.println("Expired JWT token");
        httpServletRequest.setAttribute("expired",ex.getMessage());
    }catch (UnsupportedJwtException ex){
        System.out.println("Unsupported JWT exception");
    }catch (IllegalArgumentException ex){
        System.out.println("Jwt claims string is empty");
    }
    return false;

接下来修改JwtAuthenticationEntryPoint类中的commence方法,检查我们上面添加的http servlet请求标头中的过期标头。

@Override
public void commence(HttpServletRequest httpServletRequest,
                     HttpServletResponse httpServletResponse,
                     AuthenticationException e) throws IOException, ServletException {

    final String expired = (String) httpServletRequest.getAttribute("expired");
    System.out.println(expired);
    if (expired!=null){
        httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,expired);
    }else{
        httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,"Invalid Login details");
    }

更多细节请查看这篇文章。一个简单好用的解决方案。


3
欢迎提供解决方案的链接,但请确保您的回答即使没有链接也有用:在链接周围添加上下文,以便您的同行用户对其了解并知道为什么需要它,然后引用您链接页面中最相关的部分,以防目标页面不可用。 仅仅是一个链接的答案可能会被删除。 - Zoe stands with Ukraine
刚刚添加了解决方案。 - Sanjaya Senadheera

1
由于您使用了.exceptionHandling(),我相信您可以配置一个新的ExceptionHandler;
另一种方法是覆盖您想要不同的消息,像这样post

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接