当前位置:网站首页>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 补充
边栏推荐
猜你喜欢
随机推荐
【进程间通信】消息队列
win10无法识别蓝牙麦克风
shader入门精要3
6. Unified logging
Based on the matrix calculation in the linear regression equation of the coefficient estimates
audio console无法连接到RPC服务
第二十五章:一文掌握while循环
LITESTAR 4D应用:室内植物照明模拟
px和em和rem的区别
灵活的区域定义
第三十一章:二叉树的概念与性质
指针/【类型】对指针加一能力的影响(&*ip ,*&ipd)
关于推荐系统的随想
LeetCode 2343. 裁剪数字后查询第 K 小的数字 暴力+语法考察
MySQL协议长什么样子
光栅区域衍射级数和效率的规范
剑指offer:数值的整数次方
Qt | 定时器的使用 QTimer
使用1D-1D EPE的光波导布局设计工具
Knapsack Problem - Dynamic Programming - Theory









