处理Spring Boot Resource Server中的安全性异常

时间:2016-12-14 10:47:11

标签: java spring security spring-boot spring-security

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

我们实施了

@ControllerAdvice
@RestController
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

所以只要资源服务器上出现错误,我们就希望它用

回答
{
  "message": "...",
  "type": "...",
  "status": 400
}

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

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

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

但是,任何弹簧安全性错误都将始终绕过我们的异常处理程序

    @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来自定义Auth服务器的示例。但我无法找到将其连接到资源服务器安全链的位置。

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

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

9 个答案:

答案 0 :(得分:25)

如前面的评论中所述,安全框架在到达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();
  }
}

答案 1 :(得分:5)

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

这样的事情:

@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时会被部分重用。特别是,它增加了&#34; WWW-Authenticate&#34;标题包含与错误相关的详细信息。

答案 2 :(得分:3)

您无法使用Spring @ControllerAdvice之类的Spring MVC异常处理程序注释,因为Spring安全过滤器会在Spring MVC之前大量使用。

答案 3 :(得分:1)

OAuth2ExceptionRenderer用于授权服务器。正确的答案很可能像本文中详述的那样处理它(也就是说,忽略它是oauth并像对待任何其他Spring安全认证机制一样对待它):https://stackoverflow.com/a/26502321/5639571

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

答案 4 :(得分:0)

如果您使用的令牌验证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://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);
                }
            }
        });
}

因此,您需要在RestTemplate配置中设置自定义RemoteTokenServices,以处理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 :(得分:0)

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

答案 6 :(得分:0)

我们可以使用此安全处理程序将处理程序传递给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 :(得分:0)

这是可能的。由于最初的问题是针对需要返回自定义 JSON 响应的 REST 控制器,因此我将逐步编写一个对我有用的完整答案。首先,您似乎无法使用扩展 @ControllerAdviceControllResponseEntityExceptionHandler 处理此问题。您需要一个单独的处理程序来扩展 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's列表的POJO。

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

@Autowired
private VOMSAccessDeniedHandler accessDeniedHandler;  

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

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

使用步骤 2步骤 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 :(得分:-3)

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

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