Spring Security Oauth - OAuth2Exceptions的自定义格式

时间:2017-07-10 07:39:41

标签: spring spring-security spring-security-oauth2

spring security oauth的错误格式符合OAuth规范,如下所示。

{
  "error":"insufficient_scope",
  "error_description":"Insufficient scope for this resource",
  "scope":"do.something"
}

特别是在资源服务器上,我发现为身份验证问题获取不同的错误格式有点奇怪。所以我想改变渲染异常的方式。

MDN

  

Authorization Server中的错误处理使用标准的Spring MVC   功能,即@ExceptionHandler方法

所以我尝试了这样的方法来自定义错误的格式:

@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MyErrorHandler {

    @ExceptionHandler(value = {InsufficientScopeException.class})
    ResponseEntity<MyErrorRepresentation> handle(RuntimeException ex, HttpServletRequest request) {
        return errorResponse(HttpStatus.FORBIDDEN,
                MyErrorRepresentation.builder()
                        .errorId("insufficient.scope")
                        .build(),
                request);
    }
}

但这不起作用。

查看代码,所有错误呈现似乎都在DefaultWebResponseExceptionTranslator#handleOAuth2Exception中完成。但实现自定义WebResponseExceptionTranslator不允许更改格式。

任何提示?

4 个答案:

答案 0 :(得分:3)

I found a similar question with answers that really helped my solving this - Handle spring security authentication exceptions with @ExceptionHandler

But my question is specifically about spring-security-oauth2 - so I think it is still worth stating the answer specific to spring-security-oauth2. My solution was picked from different answers to the question mentioned above.

My samples work for spring-security-oauth2 2.0.13

So the solution for me to achieve a different custom error structure for oauth2 errors on resource server resources was to register a custom OAuth2AuthenticationEntryPoint and OAuth2AccessDeniedHandler that I register using a ResourceServerConfigurerAdapter. It is worth mentioning that this is only changing the format for ResourceServer endpoints - and not the AuthorizationServer endpoints like the TokenEndpoint.

class MyCustomOauthErrorConversionConfigurerAdapter extends ResourceServerConfigurerAdapter {

  @Override
  public void configure(ResourceServerSecurityConfigurer configurer) throws Exception {
    configurer.authenticationEntryPoint(new MyCustomOauthErrorOAuth2AuthenticationEntryPoint());
    configurer.accessDeniedHandler(new MyCustomOauthErrorOAuth2AccessDeniedHandler());
  }
}

I could not reuse the functionality in OAuth2AuthenticationEntryPoint and OAuth2AccessDeniedHandler because the relevant methods translate the exception and flush it in the same method. So I needed to copy some code:

        public class MyCustomOauthErrorOAuth2AccessDeniedHandler extends OAuth2AccessDeniedHandler {

  private final MyCustomOauthErrorOAuth2SecurityExceptionHandler oAuth2SecurityExceptionHandler = new MyCustomOauthErrorOAuth2SecurityExceptionHandler();

  /**
  * Does exactly what OAuth2AccessDeniedHandler does only that the body is transformed to {@link MyCustomOauthError} before rendering the exception
  */
  @Override
  public void handle(HttpServletRequest request, HttpServletResponse response, org.springframework.security.access.AccessDeniedException authException)
  throws IOException, ServletException {
    oAuth2SecurityExceptionHandler.handle(request, response, authException, this::enhanceResponse);
  }
}

public class ExceptionMessageOAuth2AuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {

  private final MyCustomOauthErrorOAuth2SecurityExceptionHandler oAuth2SecurityExceptionHandler = new MyCustomOauthErrorOAuth2SecurityExceptionHandler();

  /**
  * Does exactly what OAuth2AuthenticationEntryPoint does only that the body is transformed to {@link MyCustomOauthError} before rendering the exception
  */
  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    oAuth2SecurityExceptionHandler.handle(request, response, authException, this::enhanceResponse);
  }
}

@RequiredArgsConstructor
public class MyCustomOauthErrorOAuth2SecurityExceptionHandler {

  private final WebResponseExceptionTranslator exceptionTranslator = new DefaultWebResponseExceptionTranslator();

  private final OAuth2ExceptionRenderer exceptionRenderer = new DefaultOAuth2ExceptionRenderer();

  private final HandlerExceptionResolver handlerExceptionResolver = new DefaultHandlerExceptionResolver();

  /**
  * This is basically what {@link org.springframework.security.oauth2.provider.error.AbstractOAuth2SecurityExceptionHandler#doHandle(HttpServletRequest, HttpServletResponse, Exception)} does.
  */
  public void handle(HttpServletRequest request, HttpServletResponse response, RuntimeException authException,
    BiFunction<ResponseEntity<OAuth2Exception>, Exception, ResponseEntity<OAuth2Exception>> oauthExceptionEnhancer)
    throws IOException, ServletException {

      try {
        ResponseEntity<OAuth2Exception> defaultErrorResponse = exceptionTranslator.translate(authException);
        defaultErrorResponse = oauthExceptionEnhancer.apply(defaultErrorResponse, authException);

        //this is the actual translation of the error 
        final MyCustomOauthError customErrorPayload = 
        MyCustomOauthError.builder()
        .errorId(defaultErrorResponse.getBody().getOAuth2ErrorCode())
        .message(defaultErrorResponse.getBody().getMessage())
        .details(defaultErrorResponse.getBody().getAdditionalInformation() == null ? emptyMap() : defaultErrorResponse.getBody().getAdditionalInformation())
        .build();

        final ResponseEntity<MyCustomOauthError> responseEntity = new ResponseEntity<>(customErrorPayload, defaultErrorResponse.getHeaders(), defaultErrorResponse.getStatusCode());

        exceptionRenderer.handleHttpEntityResponse(responseEntity, new ServletWebRequest(request, response));
        response.flushBuffer();
      } catch (ServletException e) {
        // Re-use some of the default Spring dispatcher behaviour - the exception came from the filter chain and
        // not from an MVC handler so it won't be caught by the dispatcher (even if there is one)
        if (handlerExceptionResolver.resolveException(request, response, this, e) == null) {
          throw e;
        }
      } catch (IOException | RuntimeException e) {
        throw e;
      } catch (Exception e) {
        // Wrap other Exceptions. These are not expected to happen
        throw new RuntimeException(e);
      }
  }
}

答案 1 :(得分:1)

Spring Boot版本:2.2.5

您实际上不必编写那么多代码。通过扩展OAuth2AuthenticationEntryPoint,创建自定义AuthenticationEntryPoint,覆盖其 enhanceResponse 方法并通过资源服务器配置进行注册,您需要做的所有事情。

第一部分:

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer config) {
        config.authenticationEntryPoint(new CustomOauth2AuthenticationEntryPoint());
    }
}

第二部分:

public class CustomOauth2AuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {

    @Override
    protected ResponseEntity<String> enhanceResponse(ResponseEntity<?> response, Exception exception) {
        return ResponseEntity.status(response.getStatusCode()).body("My custom response body.");
    }
}

请记住,根据spec 401响应,必须发送WWW-Authenticate标头。我们覆盖的 enhanceResponse 发送该标头。查看implementation,如果返回401,则发送该标头。

答案 2 :(得分:0)

我的方式可以自定义此异常

{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
{
"error": "invalid_token",
"error_description": "Invalid access token: 82b47091-c752-4832-b5dd-96a4da4df999"
}
{
"error": "access_denied",
"error_description": "not allowed access"
}

我尝试了配置ResourceServerConfigurerAdapter,它可以工作。

class ResourceServerConfiguration extends ResourceServerConfigurerAdapter{
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.authenticationEntryPoint(new AuthExceptionEntryPoint());
        resources.accessDeniedHandler(new CustomAccessDeniedHandler());
    }
}
public class AuthExceptionEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                        AuthenticationException authException)
            throws ServletException {

        Map map = new HashMap();
        map.put("error", "401");
        map.put("message", authException.getMessage());
        map.put("path", request.getServletPath());
        map.put("timestamp", String.valueOf(new Date().getTime()));
        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        try {
            ObjectMapper mapper = new ObjectMapper();
            mapper.writeValue(response.getOutputStream(), map);
        } catch (Exception e) {
            throw new ServletException();
        }
    }
}
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        Map map = new HashMap();
        map.put("error", "400");
        map.put("message", accessDeniedException.getMessage());
        map.put("path", request.getServletPath());
        map.put("timestamp", String.valueOf(new Date().getTime()));
        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        try {
            ObjectMapper mapper = new ObjectMapper();
            mapper.writeValue(response.getOutputStream(), map);
        } catch (Exception e) {
            throw new ServletException();
        }
    }
}

答案 3 :(得分:0)

如果通过 AuthorizationServer 配置,则必须在 TokenEndpointAuthenticationFilter Bean 中设置AuthenticationEntryPoint

    @Bean
public TokenEndpointAuthenticationFilter tokenEndpointAuthenticationFilter() {
    CustomOauth2AuthenticationEntryPoint entryPoint = new CustomOauth2AuthenticationEntryPoint();
    TokenEndpointAuthenticationFilter filter = new TokenEndpointAuthenticationFilter(authenticationManager, requestFactory());
    filter.setAuthenticationEntryPoint(entryPoint);
    return filter;
}