本文主要研究关于springOAuth2的自定义错误类型,我们知道,对外的生态系统一定会有统一的错误类型,下面就来讲怎么自定义错误类型。
1.鉴权服务器
1.1自定义错误类
@JsonSerialize(using = JiheOauthExceptionJacksonSerializer.class)
public class JiheOauth2Exception extends OAuth2Exception {
public JiheOauth2Exception(String msg, Throwable t) {
super(msg, t);
}
public JiheOauth2Exception(String msg) {
super(msg);
}
}
springOAuth2的错误静态值最要定义在OAuth2Exception,所以我们继承它
1.2然后我们定义错误的统一错误格式
public class JiheOauthExceptionJacksonSerializer extends StdSerializer<JiheOauth2Exception> {
public JiheOauthExceptionJacksonSerializer() {
super(JiheOauth2Exception.class);
}
@Override
public void serialize(JiheOauth2Exception value, JsonGenerator gen, SerializerProvider provider) throws IOException {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
gen.writeStartObject();
gen.writeStringField("errorCode", String.valueOf(value.getHttpErrorCode()));
gen.writeStringField("message", value.getMessage());
gen.writeStringField("path", request.getServletPath());
gen.writeStringField("timestamp", String.valueOf(System.currentTimeMillis()));
if (value.getAdditionalInformation()!=null) {
for (Map.Entry<String, String> entry : value.getAdditionalInformation().entrySet()) {
String key = entry.getKey();
String add = entry.getValue();
gen.writeStringField(key, add);
}
}
gen.writeEndObject();
}
}
1.3然后我们定义自定义错误实现类,来看下官方文档上面的解释
先上代码
@Component("JiheOAuth2WebResponseExceptionTranslator")
public class JiheOauth2WebResponseExceptionTranslator implements WebResponseExceptionTranslator {
private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
@Override
public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e);
// 异常栈获取 OAuth2Exception 异常
Exception ase = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType(
OAuth2Exception.class, causeChain);
// 异常栈中有OAuth2Exception
if (ase != null) {
return handleOAuth2Exception((OAuth2Exception) ase);
}
ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class,
causeChain);
if (ase != null) {
return handleOAuth2Exception(new UnauthorizedException(e.getMessage(), e));
}
ase = (AccessDeniedException) throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
if (ase instanceof AccessDeniedException) {
return handleOAuth2Exception(new ForbiddenException(ase.getMessage(), ase));
}
ase = (HttpRequestMethodNotSupportedException) throwableAnalyzer
.getFirstThrowableOfType(HttpRequestMethodNotSupportedException.class, causeChain);
if (ase instanceof HttpRequestMethodNotSupportedException) {
return handleOAuth2Exception(new MethodNotAllowed(ase.getMessage(), ase));
}
// 不包含上述异常则服务器内部错误
return handleOAuth2Exception(new ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e));
}
private ResponseEntity<OAuth2Exception> handleOAuth2Exception(OAuth2Exception e) throws IOException {
int status = e.getHttpErrorCode();
HttpHeaders headers = new HttpHeaders();
headers.set("Cache-Control", "no-store");
headers.set("Pragma", "no-cache");
if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) {
headers.set("WWW-Authenticate", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary()));
}
//按自定义格式输出错误码
JiheOauth2Exception exception = new JiheOauth2Exception(e.getMessage(), e);
ResponseEntity<OAuth2Exception> response = new ResponseEntity<OAuth2Exception>(exception, headers,
HttpStatus.valueOf(status));
return response;
}
public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
this.throwableAnalyzer = throwableAnalyzer;
}
@SuppressWarnings("serial")
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;
}
}
@SuppressWarnings("serial")
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;
}
}
@SuppressWarnings("serial")
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;
}
}
@SuppressWarnings("serial")
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;
}
}
}
这里我们直接WebResponseExceptionTranslator,然后重写里面的方法,让处理错误的是跳入我们自定义的错误类里面去处理
这样基本的错误实现的工具类就定义好了
然后我要把定义的类进行初始化的注入,在注入之前别忘了我们只是处理表单方式来请求鉴权服务时的错误输入,并没有处理将鉴权信息带在头部时候的场景,所以再定义一个过滤器,继承OncePerRequestFilter,调试一下可以发现,请求时候会先跳入这个过滤器里面先进行过滤处理,上代码
@Component
public class JiheBasicAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (!request.getRequestURI().equals("/oauth/token")) {
filterChain.doFilter(request, response);
return;
}
String[] clientDetails = this.isHasClientDetails(request);
if (clientDetails == null) {
BaseResponse bs = HttpResponse.baseResponse(HttpStatus.UNAUTHORIZED.value(), "请求中未包含客户端信息");
HttpUtils.writerError(bs, response);
return;
}
this.handle(request,response,clientDetails,filterChain);
}
private void handle(HttpServletRequest request, HttpServletResponse response, String[] clientDetails, FilterChain filterChain) throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
filterChain.doFilter(request,response);
return;
}
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(clientDetails[0], clientDetails[1], authentication.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(token);
filterChain.doFilter(request,response);
}
// 判断请求头中是否包含client信息,不包含返回false
private String[] isHasClientDetails(HttpServletRequest request) {
String[] params = null;
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header != null) {
String basic = header.substring(0, 5);
if (basic.toLowerCase().contains("basic")) {
String tmp = header.substring(6);
String defaultClientDetails = new String(Base64.getDecoder().decode(tmp));
String[] clientArrays = defaultClientDetails.split(":");
if (clientArrays.length != 2) {
return params;
} else {
params = clientArrays;
}
}
}
String id = request.getParameter("client_id");
String secret = request.getParameter("client_secret");
if (header == null && id != null) {
params = new String[]{id, secret};
}
return params;
}
}
OK,下面开始初始化的注入
在OAuthSecurityConfig里面
@Configuration
@EnableResourceServer
@EnableAuthorizationServer
public class OAuthSecurityConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
@Qualifier("authenticationManagerBean")
AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private WebResponseExceptionTranslator JiheOAuth2WebResponseExceptionTranslator;
static final Logger logger = LoggerFactory.getLogger(OAuthSecurityConfig.class);
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* @Description: JWT 密钥非对称加密
* @param:
* @return:
* @author: fanjc
* @Date: 2019/3/12
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("test-jwt.jks"), "test123".toCharArray());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("test-jwt"));
return converter;
}
/**
* @Description: 声明JDBCClientDetails实现
* @param:
* @return:
* @author: fanjc
* @Date: 2019/3/13
*/
@Bean
public ClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
/**
* @Description: 注入clientDetailsService
* @param:
* @return:
* @author: fanjc
* @Date: 2019/3/13
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
String finalSecret = "++++++" + new BCryptPasswordEncoder().encode("123456");
System.out.println(finalSecret);
clients.withClientDetails(clientDetailsService);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore)
.tokenEnhancer(jwtAccessTokenConverter())
.authenticationManager(authenticationManager)
//支持GET方法
.allowedTokenEndpointRequestMethods(HttpMethod.POST,HttpMethod.GET)
//自定义错误接口实现类
.exceptionTranslator(JiheOAuth2WebResponseExceptionTranslator);
// 配置tokenServices参数
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(false);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
// tokenServices.setAccessTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(30)); // 30天
endpoints.tokenServices(tokenServices);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 允许表单认证
security.allowFormAuthenticationForClients()
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
//client_secret用BCrypt方式加密
.passwordEncoder(new BCryptPasswordEncoder())
.addTokenEndpointAuthenticationFilter(new JiheBasicAuthenticationFilter());
;
}
}
在这里分别注入
Ok,完了我们可以测试一下
可以看到我们的自定义类型输出了。
但是这里有一个遗留问题,如果你输出错误的client_id和client_secret的话,并不会跳入到我们自定义的错误实现里面,经过调试发现流程是直接进入了
DefaultWebResponseExceptionTranslator里面,这个问题后面再多研究研究
这样的话,我们简单的把鉴权服务的错误类型给自定义成功了。
下面我们来看资源服务
这里我们定义一个错误类AuthExceptionEntryPoint和一个错误处理器CustomAccessDeniedHandler
上代码
AuthExceptionEntryPoint
public class AuthExceptionEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException)
throws ServletException {
Map map = new HashMap();
map.put("errorCode", "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();
}
}
}
CustomAccessDeniedHandler
@Component("customAccessDeniedHandler")
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Autowired
private ObjectMapper objectMapper;
@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("errorCode", "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);
response.getWriter().write(objectMapper.writeValueAsString(map));
}
}
然后我们测试一下,我输入无效的token来请求资源
成功返回错误码了
git源码地址
https://github.com/fjc440/spring-oauth2-demo/tree/feature_exception