SpringSecurityOAuth2(2)请求携带客户端信息校验,自定义异常返回,无权处理,token失效处理

上文地址:SpringSecurityOAuth2(1)(password,authorization_code,refresh_token,client_credentials)获取token

上一篇博客写了一个至简的OAuth2的token认证服务器,只实现了4种获取token的方式 ,对于异常处理,以及无权处理,生成token前的数据完整性校验等等没有涉及,该篇文章对于这些内容做一些补充:

GitHub地址

码云地址

OAUth2的认证适配器AuthorizationServerConfigurerAdapter有三个主要的方法:

  1. AuthorizationServerSecurityConfigurer:

    配置令牌端点(Token Endpoint)的安全约束

  2. ClientDetailsServiceConfigurer:

    配置客户端详细服务, 客户端的详情在这里进行初始化

  3. AuthorizationServerEndpointsConfigurer:

    配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)

1、请求前客户端信息完整校验

对于携带数据不完整的请求,可以直接返回给前端,不需要经过后面的验证 client信息一般以Base64编码放在Authorization 中 例如编码前为

1client_name:111 (client_id:client_secret Base64编码) 2Basic Y2xpZW50X25hbWU6MTEx

新建一个ClientDetailsAuthenticationFilter继承OncePerRequestFilter

1/** 2 * @Description 客户端不带完整client处理 3 * @Author wwz 4 * @Date 2019/07/30 5 * @Param 6 * @Return 7 */ 8@Component 9public class ClientDetailsAuthenticationFilter extends OncePerRequestFilter { 10 11 private ClientDetailsService clientDetailsService; 12 13 @Override 14 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 15 // 只有获取token的时候需要携带携带客户端信息,放过其他 16 if (!request.getRequestURI().equals("/oauth/token")) { 17 filterChain.doFilter(request, response); 18 return; 19 } 20 String[] clientDetails = this.isHasClientDetails(request); 21 22 if (clientDetails == null) { 23 ResponseVo resultVo = new ResponseVo(HttpStatus.UNAUTHORIZED.value(), "请求中未包含客户端信息"); 24 HttpUtilsResultVO.writerError(resultVo, response); 25 return; 26 } 27 this.handle(request, response, clientDetails, filterChain); 28 } 29 private void handle(HttpServletRequest request, HttpServletResponse response, String[] clientDetails, FilterChain filterChain) throws IOException, ServletException { 30 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 31 32 if (authentication != null && authentication.isAuthenticated()) { 33 filterChain.doFilter(request, response); 34 return; 35 } 36 37 38 MyClientDetails details = (MyClientDetails) this.getClientDetailsService().loadClientByClientId(clientDetails[0]); 39 UsernamePasswordAuthenticationToken token = 40 new UsernamePasswordAuthenticationToken(details.getClientId(), details.getClientSecret(), details.getAuthorities()); 41 42 SecurityContextHolder.getContext().setAuthentication(token); 43 44 45 filterChain.doFilter(request, response); 46 } 47 /** 48 * 判断请求头中是否包含client信息,不包含返回null Base64编码 49 */ 50 private String[] isHasClientDetails(HttpServletRequest request) { 51 52 String[] params = null; 53 54 String header = request.getHeader(HttpHeaders.AUTHORIZATION); 55 56 if (header != null) { 57 58 String basic = header.substring(0, 5); 59 60 if (basic.toLowerCase().contains("basic")) { 61 62 String tmp = header.substring(6); 63 String defaultClientDetails = new String(Base64.getDecoder().decode(tmp)); 64 65 String[] clientArrays = defaultClientDetails.split(":"); 66 67 if (clientArrays.length != 2) { 68 return params; 69 } else { 70 params = clientArrays; 71 } 72 73 } 74 } 75 String id = request.getParameter("client_id"); 76 String secret = request.getParameter("client_secret"); 77 78 if (header == null && id != null) { 79 params = new String[]{id, secret}; 80 } 81 return params; 82 } 83 public ClientDetailsService getClientDetailsService() { 84 return clientDetailsService; 85 } 86 87 public void setClientDetailsService(ClientDetailsService clientDetailsService) { 88 this.clientDetailsService = clientDetailsService; 89 } 90}

然后在AuthorizationServerSecurityConfigurer中加入过滤链

1 /** 2 * 配置令牌端点(Token Endpoint)的安全约束 3 */ 4 @Override 5 public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { 6 // 加载client的 获取接口 7 clientDetailsAuthenticationFilter.setClientDetailsService(clientDetailsService); 8 // 客户端认证之前的过滤器 9 oauthServer.addTokenEndpointAuthenticationFilter(clientDetailsAuthenticationFilter); 10 oauthServer 11 .tokenKeyAccess("permitAll()") 12 .checkTokenAccess("isAuthenticated()") 13 .allowFormAuthenticationForClients(); // 允许表单登录 14 }

验证效果:

未携带client信息

携带client信息

2、自定义异常返回格式

OAuth2自带的异常返回格式是:

1 { 2 "error": "invalid_grant", 3 "error_description": "Bad credentials" 4 }

这个格式对前端来说不是很友好,我们期望的格式是:

1{ 2 "code":401, 3 "msg":"msg" 4}

下面是具体实现:

新建MyOAuth2WebResponseExceptionTranslator实现 WebResponseExceptionTranslator接口 重写ResponseEntity<Oauth2Exception> translate(Exception e)方法; 认证发送的异常在这里捕获,认证发生的异常在这里能捕获到,在这里我们可以将我们的异常信息封装成统一的格式返回即可,这里怎么处理因项目而异,这里我直接复制了DefaultWebResponseExceptionTranslator 实现方法

1/** 2 * @Description WebResponseExceptionTranslator 3 * @Author wwz 4 * @Date 2019/07/30 5 * @Param 6 * @Return 7 */ 8@Component 9public class MyOAuth2WebResponseExceptionTranslator implements WebResponseExceptionTranslator<OAuth2Exception> { 10 11 private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); 12 13 @Override 14 public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception { 15 16 // Try to extract a SpringSecurityException from the stacktrace 17 Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e); 18 19 // 异常栈获取 OAuth2Exception 异常 20 Exception ase = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType( 21 OAuth2Exception.class, causeChain); 22 23 // 异常栈中有OAuth2Exception 24 if (ase != null) { 25 return handleOAuth2Exception((OAuth2Exception) ase); 26 } 27 ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, 28 causeChain); 29 if (ase != null) { 30 return handleOAuth2Exception(new UnauthorizedException(e.getMessage(), e)); 31 } 32 33 ase = (AccessDeniedException) throwableAnalyzer 34 .getFirstThrowableOfType(AccessDeniedException.class, causeChain); 35 if (ase instanceof AccessDeniedException) { 36 return handleOAuth2Exception(new ForbiddenException(ase.getMessage(), ase)); 37 } 38 39 ase = (HttpRequestMethodNotSupportedException) throwableAnalyzer 40 .getFirstThrowableOfType(HttpRequestMethodNotSupportedException.class, causeChain); 41 if (ase instanceof HttpRequestMethodNotSupportedException) { 42 return handleOAuth2Exception(new MethodNotAllowed(ase.getMessage(), ase)); 43 } 44 45 // 不包含上述异常则服务器内部错误 46 return handleOAuth2Exception(new ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e)); 47 } 48 49 private ResponseEntity<OAuth2Exception> handleOAuth2Exception(OAuth2Exception e) throws IOException { 50 51 int status = e.getHttpErrorCode(); 52 HttpHeaders headers = new HttpHeaders(); 53 headers.set("Cache-Control", "no-store"); 54 headers.set("Pragma", "no-cache"); 55 if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) { 56 headers.set("WWW-Authenticate", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary())); 57 } 58 59 MyOAuth2Exception exception = new MyOAuth2Exception(e.getMessage(), e); 60 61 ResponseEntity<OAuth2Exception> response = new ResponseEntity<OAuth2Exception>(exception, headers, 62 HttpStatus.valueOf(status)); 63 64 return response; 65 66 } 67 68 public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) { 69 this.throwableAnalyzer = throwableAnalyzer; 70 } 71 72 @SuppressWarnings("serial") 73 private static class ForbiddenException extends OAuth2Exception { 74 75 public ForbiddenException(String msg, Throwable t) { 76 super(msg, t); 77 } 78 79 public String getOAuth2ErrorCode() { 80 return "access_denied"; 81 } 82 83 public int getHttpErrorCode() { 84 return 403; 85 } 86 87 } 88 89 @SuppressWarnings("serial") 90 private static class ServerErrorException extends OAuth2Exception { 91 92 public ServerErrorException(String msg, Throwable t) { 93 super(msg, t); 94 } 95 96 public String getOAuth2ErrorCode() { 97 return "server_error"; 98 } 99 100 public int getHttpErrorCode() { 101 return 500; 102 } 103 104 } 105 106 @SuppressWarnings("serial") 107 private static class UnauthorizedException extends OAuth2Exception { 108 109 public UnauthorizedException(String msg, Throwable t) { 110 super(msg, t); 111 } 112 113 public String getOAuth2ErrorCode() { 114 return "unauthorized"; 115 } 116 117 public int getHttpErrorCode() { 118 return 401; 119 } 120 121 } 122 123 @SuppressWarnings("serial") 124 private static class MethodNotAllowed extends OAuth2Exception { 125 126 public MethodNotAllowed(String msg, Throwable t) { 127 super(msg, t); 128 } 129 130 public String getOAuth2ErrorCode() { 131 return "method_not_allowed"; 132 } 133 134 public int getHttpErrorCode() { 135 return 405; 136 } 137 138 } 139}

定义自己的OAuth2Exception格式 MyOAuth2Exception

1/** 2* @Description 异常格式 3* @Author wwz 4* @Date 2019/07/30 5* @Param 6* @Return 7*/ 8@JsonSerialize(using = MyOAuthExceptionJacksonSerializer.class) 9public class MyOAuth2Exception extends OAuth2Exception { 10 public MyOAuth2Exception(String msg, Throwable t) { 11 super(msg, t); 12 13 } 14 public MyOAuth2Exception(String msg) { 15 super(msg); 16 17 } 18}

定义异常的MyOAuth2Exception的序列化类 MyOAuth2ExceptionJacksonSerializer

1/** 2* @Description 定义异常MyOAuth2Exception的序列化 3* @Author wwz 4* @Date 2019/07/11 5* @Param 6* @Return 7*/ 8public class MyOAuthExceptionJacksonSerializer extends StdSerializer<MyOAuth2Exception> { 9 10 protected MyOAuthExceptionJacksonSerializer() { 11 super(MyOAuth2Exception.class); 12 } 13 14 @Override 15 public void serialize(MyOAuth2Exception value, JsonGenerator jgen, SerializerProvider serializerProvider) throws IOException { 16 jgen.writeStartObject(); 17 jgen.writeObjectField("code", value.getHttpErrorCode()); 18 jgen.writeStringField("msg", value.getSummary()); 19 jgen.writeEndObject(); 20 } 21}

将定义好的异常处理 加入到授权配置的 AuthorizationServerEndpointsConfigurer配置中

1 /** 2 * 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services) 3 */ 4 @Override 5 public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { 6 endpoints 7 .tokenStore(tokenStore()) // 配置token存储 8 .userDetailsService(userDetailsService) // 配置自定义的用户权限数据,不配置会导致token无法刷新 9 .authenticationManager(authenticationManager) 10 .tokenServices(defaultTokenServices())// 加载token配置 11 .exceptionTranslator(webResponseExceptionTranslator); // 自定义异常返回 12 }

演示效果:

3、自定义无权访问处理器

默认的无权访问返回格式是:

1{ 2 "error": "access_denied", 3 "error_description": "不允许访问" 4}

我们期望的格式是:

1{ 2 "code":401, 3 "msg":"msg" 4}

新建一个MyAccessDeniedHandler 实现AccessDeniedHandler,自定义返回信息:

1/** 2 * @Description 无权访问处理器 3 * @Author wwz 4 * @Date 2019/07/30 5 * @Param 6 * @Return 7 */ 8@Component 9public class MyAccessDeniedHandler implements AccessDeniedHandler { 10 @Override 11 public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { 12 ResponseVo resultVo = new ResponseVo(); 13 resultVo.setMessage("无权访问!"); 14 resultVo.setCode(403); 15 HttpUtilsResultVO.writerError(resultVo, response); 16 } 17}

在ResourceServerConfigurerAdapter资源配置中增加

http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); // 无权处理器

因为我在请求上增加了注解权限只能ROLE_USER用户访问,然后我登录的是ROLE_ADMIN用户,所以无权处理。

1 @GetMapping("/hello") 2 @PreAuthorize("hasRole('ROLE_USER')") 3 public String hello(Principal principal) { 4 return principal.getName() + " has hello Permission"; 5 }

4、自定义token无效处理器

默认的token无效返回信息是:

1{ 2 "error": "invalid_token", 3 "error_description": "Invalid access token: 78df4214-8e10-46ae-a85b-a8f5247370a" 4}

我们期望的格式是:

1{ 2 "code":403, 3 "msg":"msg" 4}

新建MyTokenExceptionEntryPoint 实现AuthenticationEntryPoint

1/** 2 * @Description 无效Token返回处理器 3 * @Author wwz 4 * @Date 2019/07/30 5 * @Param 6 * @Return 7 */ 8@Component 9public class MyTokenExceptionEntryPoint implements AuthenticationEntryPoint { 10 @Override 11 public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { 12 Throwable cause = authException.getCause(); 13 response.setStatus(HttpStatus.OK.value()); 14 response.setHeader("Content-Type", "application/json;charset=UTF-8"); 15 try { 16 HttpUtilsResultVO.writerError(new ResponseVo(401, authException.getMessage()), response); 17 } catch (IOException e) { 18 e.printStackTrace(); 19 } 20 } 21}

在 资源配置中ResourceServerConfigurerAdapter中注入:

1@Override 2 public void configure(ResourceServerSecurityConfigurer resources) throws Exception { 3 resources.authenticationEntryPoint(tokenExceptionEntryPoint); // token失效处理器 4 resources.resourceId("auth"); // 设置资源id 通过client的 scope 来判断是否具有资源权限 5 }

展示效果:

点赞
收藏

评论区

加载中...

相关推荐

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

Spring Cloud OAuth 实现微服务内部Token传递的源码解析

背景分析!(http://pic.pigx.top/20190414113622_whRvQH_havetoken.jpeg)1.客户端携带认证中心发放的token,请求资源服务器A(SpringSecurityOAuth发放Token源码解析(https://my.oschina.net/giegie/blog/

OAuth2 Token 一定要放在请求头中吗?

Token一定要放在请求头中吗?答案肯定是否定的,本文将从源码的角度来分享一下springsecurityoauth2的解析过程,及其扩展点的应用场景。Token解析过程说明当我们使用springsecurityoauth2时,一般情况下需要把认证中心申请的token放在请求头中请求目标接口,如下

SpringCloud consul 微服务(注册到主机名的问题)

目前项目在使用consul做服务注册与发现,做SpringSecurityOAuth2权限认证的authorization\_code模式的时候发现一个异常坑爹的问题这是开始的服务注册代码块bootstrap.yml:spring:cloud:consul:port:8500

SpringSecurity OAuth2自定义ClientDetails

最近在做SpringSecurityOAuth2的自定义ClientDetails目前实现了两种方式1.实现ClientDetailsService并把值传入BaseClientDetails然后返回@OverridepublicClientDetailsloadClientByClientId(Stringc

SpringSecurityOAuth2(1)(password,authorization_code,refresh_token,client_credentials)获取token

最近项目准备使用SpringSecurityOAuth2做权限认证管理,所以先了解一下SpringSecurityOAuth的使用原理并做一个demo做参考GitHub地址(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fgithub.com%2Fweizongw