Spring Security Oauth - OAuth2Exceptions 的自定义格式
Posted
技术标签:
【中文标题】Spring Security Oauth - OAuth2Exceptions 的自定义格式【英文标题】:Spring Security Oauth - Custom format for OAuth2Exceptions 【发布时间】:2017-12-13 20:21:01 【问题描述】:spring security oauth的错误格式符合OAuth规范,如下所示。
"error":"insufficient_scope",
"error_description":"Insufficient scope for this resource",
"scope":"do.something"
特别是在资源服务器上,我发现为身份验证问题获取不同的错误格式有点奇怪。所以我想改变这个异常的呈现方式。
documentation 说
授权服务器中的错误处理使用标准 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
不允许更改格式。
有什么提示吗?
【问题讨论】:
可能重复:***.com/questions/45985310/… 【参考方案1】:首先,了解一下Spring Security OAuth2。
-
OAuth2 有两个主要部分
AuthorizationServer : /oauth/token,获取令牌
ResourceServer : url 资源权限管理
-
Spring Security 将过滤器添加到服务器容器的过滤器链中,因此 Spring Security 的异常不会到达@ControllerAdvice
然后,应为 AuthorizationServer 和 ResourceServer 考虑自定义 OAuth2Exceptions。
这是配置
@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自定义ourOAuthException序列化器,默认情况下与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();
【讨论】:
非常感谢。你为我节省了很多时间。【参考方案2】:我发现了一个类似的问题,其答案确实帮助我解决了这个问题 - Handle spring security authentication exceptions with @ExceptionHandler
但我的问题专门针对spring-security-oauth2
- 所以我认为仍然值得说明spring-security-oauth2
的特定答案。我的解决方案是从上述问题的不同答案中挑选出来的。
我的示例适用于spring-security-oauth2 2.0.13
因此,我为资源服务器资源上的 oauth2 错误实现不同的自定义错误结构的解决方案是注册自定义 OAuth2AuthenticationEntryPoint
和 OAuth2AccessDeniedHandler
,我使用 ResourceServerConfigurerAdapter
注册。值得一提的是,这只是改变了 ResourceServer 端点的格式——而不是像 TokenEndpoint 这样的 AuthorizationServer 端点。
class MyCustomOauthErrorConversionConfigurerAdapter extends ResourceServerConfigurerAdapter
@Override
public void configure(ResourceServerSecurityConfigurer configurer) throws Exception
configurer.authenticationEntryPoint(new MyCustomOauthErrorOAuth2AuthenticationEntryPoint());
configurer.accessDeniedHandler(new MyCustomOauthErrorOAuth2AccessDeniedHandler());
我无法重用OAuth2AuthenticationEntryPoint
和OAuth2AccessDeniedHandler
中的功能,因为相关方法会转换异常并在同一方法中刷新它。所以我需要复制一些代码:
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);
【讨论】:
你在自定义 authenticationEntryPoint 中放了什么?【参考方案3】: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,则发送该标头。
【讨论】:
【参考方案4】:如果你通过 AuthorizationServer 配置,你必须在 TokenEndpointAuthenticationFilter Bean 中设置AuthenticationEntryPoint
@Bean
public TokenEndpointAuthenticationFilter tokenEndpointAuthenticationFilter()
CustomOauth2AuthenticationEntryPoint entryPoint = new CustomOauth2AuthenticationEntryPoint();
TokenEndpointAuthenticationFilter filter = new TokenEndpointAuthenticationFilter(authenticationManager, requestFactory());
filter.setAuthenticationEntryPoint(entryPoint);
return filter;
【讨论】:
以上是关于Spring Security Oauth - OAuth2Exceptions 的自定义格式的主要内容,如果未能解决你的问题,请参考以下文章
org.springframework.security.oauth 和 org.codehaus.spring-security-oauth 有啥区别?
Spring Security 入门(1-3)Spring Security oauth2.0 指南
使用 spring-session 和 spring-cloud-security 时,OAuth2ClientContext (spring-security-oauth2) 不会保留在 Redis
社交登录,spring-security-oauth2 和 spring-security-jwt?