处理 Spring Boot 资源服务器中的安全异常

Posted

技术标签:

【中文标题】处理 Spring Boot 资源服务器中的安全异常【英文标题】:Handle Security exceptions in Spring Boot Resource Server 【发布时间】:2017-04-29 16:03:40 【问题描述】:

如何让我的自定义 ResponseEntityExceptionHandlerOAuth2ExceptionRenderer 在纯资源服务器上处理 Spring 安全性引发的异常?

我们实现了一个

@ControllerAdvice
@RestController
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler 

所以每当资源服务器上出现错误时,我们希望它回答


  "message": "...",
  "type": "...",
  "status": 400

资源服务器使用 application.properties 设置:

security.oauth2.resource.userInfoUri: http://localhost:9999/auth/user

对我们的身份验证服务器的请求进行身份验证和授权。

然而,任何 spring 安全错误都会绕过我们的异常处理程序

    @ExceptionHandler(InvalidTokenException.class)
    public ResponseEntity<Map<String, Object>> handleInvalidTokenException(InvalidTokenException e) 
        return createErrorResponseAndLog(e, 401);
    

并产生任一


  "timestamp": "2016-12-14T10:40:34.122Z",
  "status": 403,
  "error": "Forbidden",
  "message": "Access Denied",
  "path": "/api/templates/585004226f793042a094d3a9/schema"


  "error": "invalid_token",
  "error_description": "5d7e4ab5-4a88-4571-b4a4-042bce0a076b"

那么如何配置资源服务器的安全异常处理呢?我所找到的只是有关如何通过实现自定义OAuth2ExceptionRenderer 来自定义身份验证服务器的示例。但我找不到将它连接到资源服务器安全链的位置。

我们唯一的配置/设置是这样的:

@SpringBootApplication
@Configuration
@ComponentScan(basePackages = "our.packages")
@EnableAutoConfiguration
@EnableResourceServer

【问题讨论】:

http://***.com/questions/19767267/handle-spring-security-authentication-exceptions-with-exceptionhandler?noredirect=1&lq=1有同样的问题 对于 OAuth2 异常,请看这个问题:***.com/q/45985310/2387977 【参考方案1】:

如之前的 cmets 所述,请求在到达 MVC 层之前被安全框架拒绝,因此@ControllerAdvice 不是这里的选项。

Spring Security 框架中有 3 个接口可能在这里感兴趣:

org.springframework.security.web.authentication.AuthenticationSuccessHandler org.springframework.security.web.authentication.AuthenticationFailureHandler org.springframework.security.web.access.AccessDeniedHandler

您可以创建每个接口的实现,以便自定义为各种事件发送的响应:登录成功、登录失败、尝试访问权限不足的受保护资源。

以下将在登录尝试失败时返回 JSON 响应:

@Component
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler

  @Override
  public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException ex) throws IOException, ServletException
  
    response.setStatus(HttpStatus.FORBIDDEN.value());
    
    Map<String, Object> data = new HashMap<>();
    data.put("timestamp", new Date());
    data.put("status",HttpStatus.FORBIDDEN.value());
    data.put("message", "Access Denied");
    data.put("path", request.getRequestURL().toString());
    
    OutputStream out = response.getOutputStream();
    com.fasterxml.jackson.databind.ObjectMapper mapper = new ObjectMapper();
    mapper.writeValue(out, data);
    out.flush();
  

您还需要向安全框架注册您的实现。在 Java 配置中,如下所示:

@Configuration
@EnableWebSecurity
@ComponentScan("...")
public class SecurityConfiguration extends WebSecurityConfigurerAdapter

  @Override
  public void configure(HttpSecurity http) throws Exception
  
    http
       .addFilterBefore(corsFilter(), ChannelProcessingFilter.class)
       .logout()
       .deleteCookies("JESSIONID")
       .logoutUrl("/api/logout")
       .logoutSuccessHandler(logoutSuccessHandler())
       .and()
       .formLogin()
       .loginPage("/login")
       .loginProcessingUrl("/api/login")
       .failureHandler(authenticationFailureHandler())
       .successHandler(authenticationSuccessHandler())
       .and()
       .csrf()
       .disable()
       .exceptionHandling()
       .authenticationEntryPoint(authenticationEntryPoint())
       .accessDeniedHandler(accessDeniedHandler());
  

  /**
   * @return Custom @link AuthenticationFailureHandler to send suitable response to REST clients in the event of a
   *         failed authentication attempt.
   */
  @Bean
  public AuthenticationFailureHandler authenticationFailureHandler()
  
    return new RestAuthenticationFailureHandler();
  

  /**
   * @return Custom @link AuthenticationSuccessHandler to send suitable response to REST clients in the event of a
   *         successful authentication attempt.
   */
  @Bean
  public AuthenticationSuccessHandler authenticationSuccessHandler()
  
    return new RestAuthenticationSuccessHandler();
  

  /**
   * @return Custom @link AccessDeniedHandler to send suitable response to REST clients in the event of an attempt to
   *         access resources to which the user has insufficient privileges.
   */
  @Bean
  public AccessDeniedHandler accessDeniedHandler()
  
    return new RestAccessDeniedHandler();
  

【讨论】:

在纯@EnableResourceServer 中,我有一个@Configuration 类,我在其中连接http.exceptionHandling().accessDeniedHandler(restAccessDeniedHandler,但是这并没有捕获以下内容: "error": "invalid_token", "error_description": "5d7e4ab5-4a88-4571-b4a4-042bce0a076b" 你会怎么做? 在这个答案中,AuthenticationFailureHandler 是基于loginProcessingUrl 连接的。我不认为你会有一个@EnableResourceServer。你还能怎么把它连接进去? 我和你有同样的烦恼。你能找到最终的解决方案吗? @p 我遇到了问题。 @peterl 你找到解决方案了吗? 您还可以通过注入 HandlerExceptionResolver 然后在方法 onAuthenticationFailure 中执行 resolver.resolveException(request, response, null, exception); 来将异常重定向到 ControllerAdvice。这集中了您的异常处理。【参考方案2】:

如果您使用@EnableResourceServer,您可能会发现在@Configuration 类中扩展ResourceServerConfigurerAdapter 而不是WebSecurityConfigurerAdapter 很方便。通过这样做,您可以简单地通过覆盖configure(ResourceServerSecurityConfigurer resources) 并在方法中使用resources.authenticationEntryPoint(customAuthEntryPoint()) 来注册自定义AuthenticationEntryPoint

类似这样的:

@Configuration
@EnableResourceServer
public class CommonSecurityConfig extends ResourceServerConfigurerAdapter 

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception 
        resources.authenticationEntryPoint(customAuthEntryPoint());
    

    @Bean
    public AuthenticationEntryPoint customAuthEntryPoint()
        return new AuthFailureHandler();
    

还有一个不错的OAuth2AuthenticationEntryPoint 可以在实现自定义AuthenticationEntryPoint 时进行扩展(因为它不是最终版本)和部分重用。特别是,它添加了带有错误相关详细信息的“WWW-Authenticate”标头。

【讨论】:

安全框架返回 "Invalid_token, token expired (blabla...the token string here)" 对我不起作用。它只是捕获 401 的异常。【参考方案3】:

您无法使用 Spring MVC 异常处理程序注释,例如 @ControllerAdvice,因为 Spring 安全过滤器在 Spring MVC 之前就开始了。

【讨论】:

【参考方案4】:

如果您使用的令牌验证 URL 配置类似于 Configuring resource server with RemoteTokenServices in Spring Security Oauth2,在未经授权的情况下返回 HTTP 状态 401:

@Primary
@Bean
public RemoteTokenServices tokenService() 
    RemoteTokenServices tokenService = new RemoteTokenServices();
    tokenService.setCheckTokenEndpointUrl("https://token-validation-url.com");
    tokenService.setTokenName("token");
    return tokenService;

如其他答案 (https://***.com/a/44372313/5962766) 中所述实施自定义 authenticationEntryPoint 将不起作用,因为 RemoteTokenService 使用 400 状态并为其他状态(如 401)引发未处理的异常:

public RemoteTokenServices() 
        restTemplate = new RestTemplate();
        ((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() 
            @Override
            // Ignore 400
            public void handleError(ClientHttpResponse response) throws IOException 
                if (response.getRawStatusCode() != 400) 
                    super.handleError(response);
                
            
        );

所以你需要在RemoteTokenServices 配置中设置自定义RestTemplate 来处理401 而不会抛出异常:

@Primary
@Bean
public RemoteTokenServices tokenService() 
    RemoteTokenServices tokenService = new RemoteTokenServices();
    tokenService.setCheckTokenEndpointUrl("https://token-validation-url.com");
    tokenService.setTokenName("token");
    RestOperations restTemplate = new RestTemplate();
    restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
    ((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() 
            @Override
            // Ignore 400 and 401
            public void handleError(ClientHttpResponse response) throws IOException 
                if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) 
                    super.handleError(response);
                
            
        );
    
    tokenService.setRestTemplate(restTemplate);
    return tokenService;

并为HttpComponentsClientHttpRequestFactory添加依赖:

<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpclient</artifactId>
</dependency>

【讨论】:

【参考方案5】:

OAuth2ExceptionRenderer 用于授权服务器。正确的答案很可能会像这篇文章中详述的那样处理它(即忽略它是 oauth 并将其视为任何其他 Spring 安全身份验证机制):https://***.com/a/26502321/5639571

当然,这将捕获与 oauth 相关的异常(在您到达资源端点之前抛出),但是在您的资源端点内发生的任何异常仍然需要 @ExceptionHandler 方法。

【讨论】:

【参考方案6】:

我们可以使用这个安全处理程序将处理程序传递给spring mvc @ControllerAdvice

@Component
public class AuthExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler 

    private static final Logger LOG = LoggerFactory.getLogger(AuthExceptionHandler.class);

    private final HandlerExceptionResolver resolver;

    @Autowired
    public AuthExceptionHandler(@Qualifier("handlerExceptionResolver") final HandlerExceptionResolver resolver) 
        this.resolver = resolver;
    

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException 
        LOG.error("Responding with unauthorized error. Message - ", authException.getMessage());
        resolver.resolveException(request, response, null, authException);
    

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException 
        LOG.error("Responding with access denied error. Message - ", accessDeniedException.getMessage());
        resolver.resolveException(request, response, null, accessDeniedException);
    

然后使用@ControllerAdvice定义异常,这样我们就可以在一个地方管理全局异常处理程序了..

【讨论】:

【参考方案7】:

这是可能的。由于最初的问题是针对需要返回自定义 JSON 响应的 REST 控制器,因此我将逐步编写一个对我有用的完整答案。首先,您似乎无法使用扩展ControllResponseEntityExceptionHandler@ControllerAdvice 来处理这个问题。您需要一个扩展 AccessDeniedHandler 的单独处理程序。请按照以下步骤操作。

第 1 步:创建一个扩展 AccessDeniedHandler 的自定义处理程序类

@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler 

    private static final String JSON_TYPE = "application/json";
    
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException 
        MyErrorList errors = new MyErrorList();
        errors.addError(new MyError("", "You do not have permission to access this resource."));

        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.setContentType(JSON_TYPE);
        OutputStream output = response.getOutputStream();
        ObjectMapper mapper = new ObjectMapper();
        mapper.writeValue(output, errors);
        output.flush();
    

上面的'MyError'是一个简单的POJO来表示一个错误的json结构,而MyErrorList是另一个POJO,它包含一个'MyError's的列表。

第二步:将上面创建的 Handler 注入到安全配置中

@Autowired
private VOMSAccessDeniedHandler accessDeniedHandler;  

第 3 步:在您的配置方法中注册accessDeniedHandler

.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)

使用 Step 2Step 3,您的 SecurityConfiguration 应该如下所示(请注意,我省略了与此问题无关的代码以缩短这个答案的长度):

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

    @Autowired
    private MyAccessDeniedHandler accessDeniedHandler;

    // Other stuff

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.authenticationProvider(authenticationProvider());
    

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/register").permitAll()
                .antMatchers("/authenticate").permitAll()
                .antMatchers("/public").permitAll()
                .anyRequest().authenticated()
                .and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)
                .and().sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    

【讨论】:

【参考方案8】:

Spring 3.0 以后,您可以使用@ControllerAdvice(在类级别)并从CustomGlobalExceptionHandler 扩展org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler

@ExceptionHandler(com.test.CustomException1.class,com.test.CustomException2.class)
public final ResponseEntity<CustomErrorMessage> customExceptionHandler(RuntimeException ex)
     return new ResponseEntity<CustomErrorMessage>(new CustomErrorMessage(false,ex.getMessage(),404),HttpStatus.BAD_REQUEST);

【讨论】:

以上是关于处理 Spring Boot 资源服务器中的安全异常的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot静态资源处理

spring-boot-资源处理

Spring Boot JAR 安全加密运行工具:XJar快速上手

嵌入式 servlet 容器不处理 Spring Boot 中的 META-INF/资源

Spring Boot 静态资源处理

如何在 Spring Boot oauth2 资源服务器中使用自定义身份验证标头