Spring Security Oauth - OAuth2Exceptions的自定义格式

问题描述 投票:3回答:2

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

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

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

documentation

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不允许更改格式。

任何提示?

spring spring-security spring-security-oauth2
2个回答
3
投票

我找到了一个类似的问题,答案确实有助于我解决这个问题 - Handle spring security authentication exceptions with @ExceptionHandler

但我的问题是关于spring-security-oauth2的 - 所以我认为仍然值得陈述spring-security-oauth2特有的答案。我的解决方案是从上述问题的不同答案中挑选出来的。

我的样本适用于spring-security-oauth2 2.0.13

因此,我为资源服务器资源上的oauth2错误实现不同的自定义错误结构的解决方案是注册我使用OAuth2AuthenticationEntryPoint注册的自定义OAuth2AccessDeniedHandlerResourceServerConfigurerAdapter。值得一提的是,这只是改变了ResourceServer端点的格式 - 而不是像TokenEndpoint那样的AuthorizationServer端点。

class MyCustomOauthErrorConversionConfigurerAdapter extends ResourceServerConfigurerAdapter {

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

我无法重用OAuth2AuthenticationEntryPointOAuth2AccessDeniedHandler中的功能,因为相关方法会转换异常并在同一方法中刷新它。所以我需要复制一些代码:

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

0
投票

首先,为Spring Security OAuth2提供一些知识。

  1. OAuth2有两个主要部分

AuthorizationServer:/ oauth / token,获取令牌

资源服务器:url资源特权管理

  1. Spring Security将过滤器添加到服务器容器的过滤器链中,因此Spring Security的例外不会到达@ControllerAdvice

然后,自定义OAuth2Exceptions应考虑AuthorizationServer和ResourceServer。

这是配置

@Configuration
@EnableAuthorizationServer
public class OAuthSecurityConfig extends AuthorizationServerConfigurerAdapter {
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //for custom
        endpoints.exceptionTranslator(new MyWebResponseExceptionTranslator());
    }
}

@Configuration
@EnableResourceServer
public  class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) {
            // format message
            resources.authenticationEntryPoint(new MyAuthenticationEntryPoint());
            resources.accessDeniedHandler(new MyAccessDeniedHandler());
        }
}

MyWebResponseExceptionTranslator将异常转换为ourOAuthException,我们通过jackson自定义myOAuthException序列化程序,默认情况下OAuth2使用的方式相同。

@JsonSerialize(using = OAuth2ExceptionJackson1Serializer.class)
public class OAuth2Exception extends RuntimeException {

其他自定义句柄类的东西

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;

/**
 * @author qianggetaba
 * @date 2019/6/21
 */
public class MyWebResponseExceptionTranslator implements WebResponseExceptionTranslator {
    @Override
    public ResponseEntity<OAuth2Exception> translate(Exception exception) throws Exception {
        if (exception instanceof OAuth2Exception) {
            OAuth2Exception oAuth2Exception = (OAuth2Exception) exception;
            return ResponseEntity
                    .status(oAuth2Exception.getHttpErrorCode())
                    .body(new CustomOauthException(oAuth2Exception.getMessage()));
        }else if(exception instanceof AuthenticationException){
            AuthenticationException authenticationException = (AuthenticationException) exception;
            return ResponseEntity
                    .status(HttpStatus.UNAUTHORIZED)
                    .body(new CustomOauthException(authenticationException.getMessage()));
        }
        return ResponseEntity
                .status(HttpStatus.OK)
                .body(new CustomOauthException(exception.getMessage()));
    }
}
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;

/**
 * @author qianggetaba
 * @date 2019/6/21
 */
@JsonSerialize(using = CustomOauthExceptionSerializer.class)
public class CustomOauthException extends OAuth2Exception {
    public CustomOauthException(String msg) {
        super(msg);
    }
}
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

import java.io.IOException;
import java.util.Arrays;
import java.util.Map;

/**
 * @author qianggetaba
 * @date 2019/6/21
 */
public class CustomOauthExceptionSerializer extends StdSerializer<CustomOauthException> {

    public CustomOauthExceptionSerializer() {
        super(CustomOauthException.class);
    }

    @Override
    public void serialize(CustomOauthException value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeStartObject();
        jsonGenerator.writeNumberField("code4444", value.getHttpErrorCode());
        jsonGenerator.writeBooleanField("status", false);
        jsonGenerator.writeObjectField("data", null);
        jsonGenerator.writeObjectField("errors", Arrays.asList(value.getOAuth2ErrorCode(),value.getMessage()));
        if (value.getAdditionalInformation()!=null) {
            for (Map.Entry<String, String> entry : value.getAdditionalInformation().entrySet()) {
                String key = entry.getKey();
                String add = entry.getValue();
                jsonGenerator.writeStringField(key, add);
            }
        }
        jsonGenerator.writeEndObject();
    }
}

用于自定义ResourceServer异常

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author qianggetaba
 * @date 2019/6/21
 */
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException)
            throws ServletException {

        Map map = new HashMap();
        map.put("errorentry", "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();
        }
    }

}
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author qianggetaba
 * @date 2019/6/21
 */
public class MyAccessDeniedHandler 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("errorauth", "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();
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.