簡體   English   中英

處理 Spring Boot Resource Server 中的安全異常

[英]Handle Security exceptions in Spring Boot Resource Server

如何讓我的自定義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來自定義OAuth2ExceptionRenderer驗證服務器的OAuth2ExceptionRenderer 但是我找不到將它連接到資源服務器的安全鏈的位置。

我們唯一的配置/設置是這樣的:

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

如之前的評論中所述,請求在到達 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類中擴展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”標頭。

您無法使用 Spring MVC 異常處理程序注釋(例如@ControllerAdvice因為 Spring 安全過濾器在 Spring MVC 之前就開始使用了。

如果您使用類似於在 Spring Security Oauth2 中使用 RemoteTokenServices 配置資源服務器的配置使用令牌驗證 URL,它會在未經授權的情況下返回 HTTP 狀態 401:

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

如其他答案 ( https://stackoverflow.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>

OAuth2ExceptionRenderer 用於授權服務器。 正確答案可能會像這篇文章中詳述的那樣處理它(即,忽略它是 oauth 並將其視為任何其他 spring 安全身份驗證機制): https : //stackoverflow.com/a/26502321/5639571

當然,這將捕獲與 oauth 相關的異常(在到達資源端點之前拋出),但是資源端點內發生的任何異常仍然需要 @ExceptionHandler 方法。

我們可以使用這個安全處理程序將處理程序傳遞給 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定義異常,以便我們可以在一個地方管理全局異常處理程序。

這個有可能。 由於最初的問題是針對需要返回自定義 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 是另一個包含“MyError”列表的 POJO。

第二步:將上面創建的Handler注入到Security配置中

@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);
    }
}
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
  @Override
  public void commence(HttpServletRequest request, HttpServletResponse res,
        AuthenticationException authException) throws IOException, ServletException {
      ApiException ex = new ApiException(HttpStatus.FORBIDDEN, "Invalid Token", authException);

      ObjectMapper mapper = new ObjectMapper();
      res.setContentType("application/json;charset=UTF-8");
      res.setStatus(403);
      res.getWriter().write(mapper.writeValueAsString(ex));
  }
}

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);
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM