【问题标题】:Spring REST Api respond with empty body + 403 Forbidden on runtime exceptionsSpring REST Api 响应,正文为空 + 403 Forbidden on runtime exceptions
【发布时间】:2020-12-27 18:24:44
【问题描述】:

我创建了一个带有自定义 JWT 身份验证的 Spring Boot Rest Api。 我的问题是,当我发送例如带有过期或无效 JWT 令牌的请求时,我会收到如下异常:

com.auth0.jwt.exceptions.SignatureVerificationException: The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA512

这显然没问题,但是响应正文是空的,因此客户端不知道为什么会出现 403 错误。

问题与 Spring 的 BadCredentials Exception 等相同...

如何将这些异常转换为自定义错误响应而不是“403 禁止”?

Spring Web 配置:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    private final UserDetailsServiceImpl userDetailsService;

    @Autowired
    public WebSecurityConfig(UserDetailsServiceImpl userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

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

        JWTAuthenticationFilter filter = new JWTAuthenticationFilter(authenticationManager());
        filter.setFilterProcessesUrl(AUTH_URL);

        http.cors().and().csrf().disable().authorizeRequests()
                .antMatchers(HttpMethod.POST, SIGN_UP_URL).permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilter(filter)
                .addFilter(new JWTAuthorizationFilter(authenticationManager()))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }
}

JWTAuthenticationFilter

    private final AuthenticationManager authenticationManager;

    @Autowired
    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req,
                                                HttpServletResponse res) throws AuthenticationException {
        try {

            String decoded = new String(Base64.getDecoder().decode(new String(req.getInputStream().readAllBytes())));

            AuthenticationDetails details = new Gson().fromJson(decoded, AuthenticationDetails.class);

            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            details.getUsername(),
                            details.getPassword(),
                            new ArrayList<>()));
        } catch (TokenExpiredException e) {
            req.setAttribute("expired", e.getMessage());
            throw new TokenExpiredException(e.getMessage());
        } catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest req,
                                            HttpServletResponse res,
                                            FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {

        String token = JWT.create()
                .withSubject(((User) auth.getPrincipal()).getUsername())
                .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .sign(Algorithm.HMAC512(SECRET.getBytes()));
        res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
    }
}

JWTAuthorizationFilter


    public JWTAuthorizationFilter(AuthenticationManager authManager) {
        super(authManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse res,
                                    FilterChain chain) throws IOException, ServletException {
        String header = req.getHeader(HEADER_STRING);

        if (header == null || !header.startsWith(TOKEN_PREFIX)) {
            chain.doFilter(req, res);
            return;
        }

        UsernamePasswordAuthenticationToken authentication = getAuthentication(req);

        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(req, res);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        String token = request.getHeader(HEADER_STRING);
        if (token != null) {

            String user = JWT.require(Algorithm.HMAC512(SECRET.getBytes()))
                    .build()
                    .verify(token.replace(TOKEN_PREFIX, ""))
                    .getSubject();

            if (user != null) {
                return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
            }
            return null;
        }
        return null;
    }
}

【问题讨论】:

  • 能否请您发布在身份验证过滤器中生成的令牌??一切看起来都很好,但我想确认标头包含正确的算法并且它是正确生成的。在 jwt.io 的帮助下,您可以检查它。
  • 感谢您的宝贵时间!如果我使用正确的令牌,它就可以工作,没问题。但是当它过期或不正确,或者登录详细信息不正确时,我只会得到一个“403 Forbidden”响应,而不是例如。带有“不正确的登录详细信息”的 json 响应正文
  • 为什么不在验证令牌时捕获异常并抛出你自己的一个???
  • 我认为,Spring Security 完成了这项工作(验证等)并抛出异常。我应该在哪里抓到那些?还是我错了?

标签: java spring spring-boot jwt


【解决方案1】:

如果您从AbstractAuthenticationProcessingFilter 扩展您的JWTAuthenticationFilter,您可以覆盖unsuccessfulAuthentication,如下所示:

@Override
  protected void unsuccessfulAuthentication(
      HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
      throws IOException, ServletException {
    SecurityContextHolder.clearContext();
    failureHandler.onAuthenticationFailure(request, response, failed);
  }

现在,如您所见,我已将故障处理委托给我的 failureHandler,它的类型为 org.springframework.security.web.authentication.AuthenticationFailureHandler

为此,您需要注册您的自定义故障处理程序。您可以通过从org.springframework.security.web.authentication.AuthenticationFailureHandler 实现您的处理程序并覆盖onAuthenticationFailure 来做到这一点,并检查从JWTAuthenticationFilter 抛出的异常实例,如下所示:

@Component
public class MyAuthFailureHandler implements AuthenticationFailureHandler {
  private final ObjectMapper mapper;

  @Autowired
  public MyAuthFailureHandler(ObjectMapper mapper) {
    this.mapper = mapper;
  }

  @Override
  public void onAuthenticationFailure(
      HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
      throws IOException, ServletException {

    response.setStatus(HttpStatus.UNAUTHORIZED.value());
    response.setContentType(MediaType.APPLICATION_JSON_VALUE);

    if (e instanceof BadCredentialsException) {
      mapper.writeValue(
          response.getWriter(),
          ErrorResponse.of(
              "Invalid username or password", ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
    } else if (e instanceof JwtExpiredTokenException) {
      mapper.writeValue(
          response.getWriter(),
          ErrorResponse.of(
              "Token has expired", ErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED));
    } else if (e instanceof AuthMethodNotSupportedException) {
      mapper.writeValue(
          response.getWriter(),
          ErrorResponse.of(e.getMessage(), ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
    } else if (e instanceof TokenEncryptionException) {
      mapper.writeValue(
          response.getWriter(),
          ErrorResponse.of(e.getMessage(), ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
    } else if (e instanceof InvalidJwtAuthenticationTokenException) {
      mapper.writeValue(
          response.getWriter(),
          ErrorResponse.of(e.getMessage(), ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
    }

    mapper.writeValue(
        response.getWriter(),
        ErrorResponse.of(
            "Authentication failed", ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
  }

【讨论】:

  • 要添加到我的答案中,您还可以创建自定义异常并将其从 failure-handler 抛出,然后将其捕获到您的 global-exception-handler 类(从 org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler 扩展)。您可以搜索 spring boot 异常处理程序,您可以获得配置它的方法。这可能会解决您的多个业务异常
  • 感谢您的回答。我的 JWTAuthenticationFilter 是从 UsernamePasswordAuthenticationFilter 扩展而来的,我该怎么办?抱歉,我不是真正的专家。
  • 没关系,UsernamePasswordAuthenticationFilter 也是从org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter 扩展而来的。只需覆盖unsuccessfulAuthentication
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-11-16
  • 2015-09-19
  • 1970-01-01
  • 2022-10-04
  • 1970-01-01
  • 2019-09-07
  • 2013-11-01
相关资源
最近更新 更多