当前位置:网站首页>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 补充
边栏推荐
猜你喜欢
随机推荐
Detailed explanation of MATLAB drawing function fplot
光波导应用中的真实光栅效应
tcp transparent proxy (IP_TRANSPARENT)
4. Publish Posts, Comment on Posts
Debug on pure method is called
十天学习Unity3D脚本(一)九个回调
EastWave:垂直腔表面激光器
饥荒联机版Mod开发——准备工具(一)
剑指offer:删除链表中重复的节点
使用三个线程,按顺序打印X,Y,Z,连续打印10次
LeetCode 2343. 裁剪数字后查询第 K 小的数字 暴力+语法考察
SkyWalking Agent数据采集和上报原理浅析
Qt | 读取文件内容并删除文件 QFile
Unity Line-Renderer
基类和派生类的关系【继承】/多态和虚函数/【继承和多态】抽象类和简单工厂
Google AdSense注册流程
【进程间通信】消息队列
Ubuntu通过apt安装Mysql
使用1D-1D EPE的光波导布局设计工具
Detailed explanation of MATLAB drawing function plot