当前位置:网站首页>Oauth2.0 自定义响应值以及异常处理
Oauth2.0 自定义响应值以及异常处理
2022-08-02 14:14:00 【zhangyu丶】
自用的响应信息主体
import cn.hutool.http.ContentType;
import cn.hutool.http.HttpStatus;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
import lombok.experimental.Accessors;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.nio.charset.Charset;
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@ApiModel(value = "响应信息主体")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class R<T> implements Serializable {
private static final long serialVersionUID = 1L;
private static final String SUCCESS = "SUCCESS";
private static final String FAILED = "FAILED";
@Getter
@Setter
@ApiModelProperty(value = "返回标记:成功标记=200,失败标记=500")
private int code;
@Getter
@Setter
@ApiModelProperty(value = "返回信息")
private String msg;
@Getter
@Setter
@ApiModelProperty(value = "数据")
private T data;
public static <T> R<T> ok() {
return restResult(null, HttpStatus.HTTP_OK, SUCCESS, true);
}
public static <T> R<T> ok(T data) {
return restResult(data, HttpStatus.HTTP_OK, SUCCESS, true);
}
public static <T> R<T> ok(T data, String msg) {
return restResult(data, HttpStatus.HTTP_OK, msg, true);
}
public static <T> R<T> ok(ResultCode result) {
return restResult(null, result.code, result.msg, true);
}
public static <T> R<T> fail() {
return restResult(null, HttpStatus.HTTP_INTERNAL_ERROR, FAILED, false);
}
public static <T> R<T> fail(String msg) {
return restResult(null, HttpStatus.HTTP_INTERNAL_ERROR, msg, false);
}
public static <T> R<T> fail(T data) {
return restResult(data, HttpStatus.HTTP_INTERNAL_ERROR, FAILED, false);
}
public static <T> R<T> fail(ResultCode result) {
return restResult(null, result.code, result.msg, false);
}
public static <T> R<T> fail(int code, String msg) {
return restResult(null, code, msg, false);
}
private static <T> R<T> restResult(T data, int code, String msg, boolean success) {
R<T> apiResult = new R<>();
apiResult.setCode(code);
apiResult.setData(data);
apiResult.setMsg(msg);
return apiResult;
}
public static void failRender(int code, String msg, HttpServletResponse response, int status) {
try {
ObjectMapper mapper = new ObjectMapper();
response.setContentType(ContentType.build(ContentType.JSON.getValue(), Charset.defaultCharset()));
response.setStatus(status);
response.getWriter().write(mapper.writeValueAsString(R.fail(code, msg)));
} catch (Exception e) {
e.printStackTrace();
}
}
public static void failRender(ResultCode resultCode, HttpServletResponse response, int status) {
try {
ObjectMapper mapper = new ObjectMapper();
response.setContentType(ContentType.build(ContentType.JSON.getValue(), Charset.defaultCharset()));
response.setStatus(status);
response.getWriter().write(mapper.writeValueAsString(R.fail(resultCode)));
} catch (Exception e) {
e.printStackTrace();
}
}
public static void cast(ResultCode resultCode) {
throw new BusinessException(resultCode);
}
public static void cast(int code, String msg) {
throw new BusinessException(code, msg);
}
}
自定义无异常情况下请求 /oauth/token 获取 token 的响应格式
@Slf4j
@Aspect
@Component
public class CustomOAuthTokenAspect {
@Around("execution(* org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(..))")
public ResponseEntity response(ProceedingJoinPoint point) throws Throwable {
Object proceed = point.proceed();
ResponseEntity<OAuth2AccessToken> responseEntity = (ResponseEntity<OAuth2AccessToken>) proceed;
return ResponseEntity.ok(R.ok(responseEntity.getBody()));
}
}
处理 grant_type、username、password 错误的异常响应
- 默认情况是使用 WebResponseExceptionTranslator接口的实现类 DefaultWebResponseExceptionTranslator对抛出的异常进行处理
- 本文处理方法就是通过实现WebResponseExceptionTranslator接口来入手,来达到对异常信息的处理
- 实现之后要记得将其添加到认证服务器核心配置 AuthorizationServerConfig 的端点配置 (AuthorizationServerEndpointsConfigurer.exceptionTranslator) 中,往下看会有写,不急
import cn.mowen.common.result.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.DefaultThrowableAnalyzer;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InsufficientScopeException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.stereotype.Component;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import java.io.IOException;
@Slf4j
@Component
public class CustomWebResponseExceptionTranslator implements WebResponseExceptionTranslator {
private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
@Override
public ResponseEntity translate(Exception e) throws Exception {
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(e);
Exception ase = (OAuth2Exception) this.throwableAnalyzer.getFirstThrowableOfType(OAuth2Exception.class, causeChain);
//异常链中有OAuth2Exception异常
if (ase != null) {
return this.handleOAuth2Exception((OAuth2Exception) ase);
}
//身份验证相关异常
ase = (AuthenticationException) this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase != null) {
return this.handleOAuth2Exception(new CustomWebResponseExceptionTranslator.UnauthorizedException(e.getMessage(), e));
}
//异常链中包含拒绝访问异常
ase = (AccessDeniedException) this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
if (ase instanceof AccessDeniedException) {
return this.handleOAuth2Exception(new CustomWebResponseExceptionTranslator.ForbiddenException(ase.getMessage(), ase));
}
//异常链中包含Http方法请求异常
ase = (HttpRequestMethodNotSupportedException) this.throwableAnalyzer.getFirstThrowableOfType(HttpRequestMethodNotSupportedException.class, causeChain);
if (ase instanceof HttpRequestMethodNotSupportedException) {
return this.handleOAuth2Exception(new CustomWebResponseExceptionTranslator.MethodNotAllowed(ase.getMessage(), ase));
}
return this.handleOAuth2Exception(new CustomWebResponseExceptionTranslator.ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e));
}
private ResponseEntity<OAuth2Exception> handleOAuth2Exception(OAuth2Exception e) throws IOException {
int status = e.getHttpErrorCode();
HttpHeaders headers = new HttpHeaders();
headers.setCacheControl(CacheControl.noCache());
headers.setPragma(CacheControl.noCache().getHeaderValue());
if (status == HttpStatus.UNAUTHORIZED.value() || e instanceof InsufficientScopeException) {
headers.set(HttpHeaders.WWW_AUTHENTICATE, String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary()));
}
ResponseEntity<OAuth2Exception> response = new ResponseEntity(R.fail(e.getMessage()), headers, HttpStatus.valueOf(status));
return response;
}
private static class MethodNotAllowed extends OAuth2Exception {
public MethodNotAllowed(String msg, Throwable t) {
super(msg, t);
}
@Override
public String getOAuth2ErrorCode() {
return "method_not_allowed";
}
@Override
public int getHttpErrorCode() {
return 405;
}
}
private static class UnauthorizedException extends OAuth2Exception {
public UnauthorizedException(String msg, Throwable t) {
super(msg, t);
}
@Override
public String getOAuth2ErrorCode() {
return "unauthorized";
}
@Override
public int getHttpErrorCode() {
return 401;
}
}
private static class ServerErrorException extends OAuth2Exception {
public ServerErrorException(String msg, Throwable t) {
super(msg, t);
}
@Override
public String getOAuth2ErrorCode() {
return "server_error";
}
@Override
public int getHttpErrorCode() {
return 500;
}
}
private static class ForbiddenException extends OAuth2Exception {
public ForbiddenException(String msg, Throwable t) {
super(msg, t);
}
@Override
public String getOAuth2ErrorCode() {
return "access_denied";
}
@Override
public int getHttpErrorCode() {
return 403;
}
}
}
自定义客户端异常处理过滤器 CustomClientCredentialsTokenEndpointFilter
- 自定义客户端异常处理过滤器: {“error”: “invalid_client”, “error_description”: “Bad client credentials”}
- 通过配置到认证服务器 (AuthorizationServerConfig) 的 client 认证异常过滤器中 (client_id、client_secret 错误时会执行)
- 配置的同时要设置 CustomAuthenticationEntryPoint 以此来格式化异常返回值
- 注意:AuthenticationEntryPoint 没有实例,需要我们自己实现这个接口才能进行注入
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter;
import org.springframework.security.web.AuthenticationEntryPoint;
public class CustomClientCredentialsTokenEndpointFilter extends ClientCredentialsTokenEndpointFilter {
private final AuthorizationServerSecurityConfigurer configurer;
private AuthenticationEntryPoint authenticationEntryPoint;
public CustomClientCredentialsTokenEndpointFilter(AuthorizationServerSecurityConfigurer configurer) {
this.configurer = configurer;
}
@Override
public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
this.authenticationEntryPoint = authenticationEntryPoint;
}
@Override
protected AuthenticationManager getAuthenticationManager() {
return configurer.and().getSharedObject(AuthenticationManager.class);
}
@Override
public void afterPropertiesSet() {
setAuthenticationFailureHandler((request, response, exception) -> authenticationEntryPoint.commence(request, response, exception));
setAuthenticationSuccessHandler((request, response, authentication) -> {
// no-op - just allow filter chain to continue to token endpoint
});
}
}
两个公共异常处理类(未认证、未授权)
实现 AuthenticationEntryPoint 接口来处理认证异常的响应信息
- 处理 Authentication 异常,如:token错误、过期
- 配置一:配置到资源服务器核心配置中(ResourceServerConfig)对 token 进行校验
- 配置二:配置到认证服务器核心配置中(AuthorizationServerConfig),当客户端异常(client_id、client_secret错误)时会执行
import cn.hutool.http.HttpStatus;
import cn.mowen.common.result.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
R.failRender(HttpStatus.HTTP_UNAUTHORIZED, exception.getMessage(), response, HttpStatus.HTTP_UNAUTHORIZED);
log.error("Authentication异常: [{}], [{}], [{}]", request.getRequestURI(), exception.getMessage(), exception);
}
}
实现 AccessDeniedHandler 接口来处理权限异常的响应信息
- 处理权限异常的异常信息,只针对于资源服务器,认证服务器无需配置
- 客户端权限异常 (resource_id),用户权限异常,自定义响应值
import cn.hutool.http.HttpStatus;
import cn.mowen.common.result.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException, ServletException {
R.failRender(HttpStatus.HTTP_UNAUTHORIZED, exception.getMessage(), response, HttpStatus.HTTP_UNAUTHORIZED);
log.error("AccessDenied异常: [{}], [{}], [{}]", exception.getMessage(), exception.getLocalizedMessage(), exception.toString());
}
}
将以上定义的异常处理类添加到认证服务器核心配置 AuthorizationServerConfig 中
- AuthorizationServerConfig 其他配置已省略,只需要找到对应方法进行追加就好,详细见 Oauth2.0 认证服务器搭建
@Configuration
@EnableAuthorizationServer
@AllArgsConstructor
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
/** * 用来配置令牌端点的安全约束, 密码校验方式等 */
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 自定义客户端异常处理过滤器: {"error": "invalid_client", "error_description": "Bad client credentials"}
CustomClientCredentialsTokenEndpointFilter endpointFilter = new CustomClientCredentialsTokenEndpointFilter(security);
endpointFilter.afterPropertiesSet();//初始化的时候执行
endpointFilter.setAuthenticationEntryPoint(customAuthenticationEntryPoint);//格式化客户端异常的响应格式
security
//.allowFormAuthenticationForClients()
.addTokenEndpointAuthenticationFilter(endpointFilter) //添加一个客户端认证之前的过滤器
;
/* * allowFormAuthenticationForClients 的作用: * 允许表单认证(申请令牌), 而不仅仅是Basic Auth方式提交, 且url中有client_id和client_secret的会走 ClientCredentialsTokenEndpointFilter 来保护, * 也就是在 BasicAuthenticationFilter 之前添加 ClientCredentialsTokenEndpointFilter,使用 ClientDetailsService 来进行 client 端登录的验证。 * 但是,在使用自定义的 CustomClientCredentialsTokenEndpointFilter 时, * 会导致 oauth2 仍然使用 allowFormAuthenticationForClients 中默认的 ClientCredentialsTokenEndpointFilter 进行过滤,致使我们的自定义 CustomClientCredentialsTokenEndpointFilter 不生效。 * 因此在使用 CustomClientCredentialsTokenEndpointFilter 时,不再需要开启 allowFormAuthenticationForClients() 功能。 */
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.exceptionTranslator(customWebResponseExceptionTranslator)//自定义异常转换类(处理grant_type, username, password错误的异常)
;
}
}
将那两个公共异常处理类配置到资源服务器的核心配置中 ResourceServerConfig
import cn.mowen.common.constant.OauthConstant;
import cn.mowen.common.constant.CommonWhiteConstant;
import cn.mowen.common.exception.oauth.CustomAuthenticationEntryPoint;
import cn.mowen.common.exception.oauth.CustomAccessDeniedHandler;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
@Configuration
@EnableResourceServer
@AllArgsConstructor
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private final TokenStore jwtTokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(OauthConstant.OAUTH_RESOURCE_ID)
.tokenStore(jwtTokenStore)
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler())
.stateless(true)
;
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
//放行 url 在此配置
.antMatchers(CommonWhiteConstant.white).permitAll()
.antMatchers(white).permitAll()
.anyRequest().authenticated()
;
}
// 白名单
private static final String[] white = {
"/test/**"
};
}
Oauth2.0 系列文章
以下是同步到语雀的、可读性好一点,CSDN 继续看的点专栏就好。
Oauth2.0 核心篇
Oauth2.0 安全性(以微信授权登陆为例)
Oauth2.0 认证服务器搭建
Oauth2.0 添加验证码登陆方式
Oauth2.0 资源服务器搭建
Oauth2.0 自定义响应值以及异常处理
Oauth2.0 补充
边栏推荐
猜你喜欢
随机推荐
许多代码……
mininet hosts talk to real internet
Debug on pure method is called
Masters and Masters
mininet multihomed topology
Manifest merger failed : Attribute [email protected] value=
STM32LL library - USART interrupt to receive variable length information
golang-reflect-method-callback
【线程网络】了解线程属性(fork/interview question)
Detailed introduction to the hierarchical method of binary tree creation
tpproxy-tcp透明代理
戴森球计划这个游戏牛逼
【离散化+前缀和】Acwing802. 区间和
6. Unified logging
移动拷贝构造函数
C语言函数调用过程-汇编分析
面试汇总
Server-Sent Events 一种轻量级的Push方式
双链表(普通迭代器和常性迭代器)
系统性能和TCP/UDP网络优化-学习大杂烩









