spring security oauth2 自动刷新续签token (refresh token)

1.引言

  • 前提:了解spring security oauth2的大致流程(对过滤器的内容有一定的了解)
  • 主要思路:
  1. 首先用过期token访问受拦截资源
  2. 认证失败返回401的时候调用异常处理器
  3. 通过异常处理器结合refresh_token进行token的刷新
  4. 刷新成功则通过请求转发(request.getRequestDispatcher)的方式再次访问受拦截资源

2.源码分析核心过滤器OAuth2AuthenticationProcessingFilter

  • 此过滤器与我们的token的各种操作息息相关,不清楚的可以参考别人的博客进行了解https://blog.csdn.net/u013815546/article/details/77046453
  • 下面是此过滤器的过滤方法,从中可以知道当授权失败抛出异常的时候将会被catch,并且通过authenticationEntryPoint.commence()调用端点异常处理器,这个被调用的异常处理器就是我们要重写的类
 
  1. public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
  2. ServletException {
  3.  
  4. final boolean debug = logger.isDebugEnabled();
  5. final HttpServletRequest request = (HttpServletRequest) req;
  6. final HttpServletResponse response = (HttpServletResponse) res;
  7.  
  8. try {
  9.  
  10. Authentication authentication = tokenExtractor.extract(request);
  11.  
  12. ...
  13.  
  14. catch (OAuth2Exception failed) {
  15. SecurityContextHolder.clearContext();
  16.  
  17. if (debug) {
  18. logger.debug("Authentication request failed: " + failed);
  19. }
  20. eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
  21. new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
  22.  
  23. authenticationEntryPoint.commence(request, response,
  24. new InsufficientAuthenticationException(failed.getMessage(), failed));
  25.  
  26. return;
  27. }
  28.  
  29. chain.doFilter(request, response);
  30. }
 

3.分析默认端点异常处理器

  • 从过滤器源码中我们可以看到此异常处理器是有默认实现类的
 
  1. public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean {
  2.  
  3. private final static Log logger = LogFactory.getLog(OAuth2AuthenticationProcessingFilter.class);
  4.  
  5. private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
  6.  
  7. ...
  8.  
  9. }
 
  • 通过查看此默认处理器,我们可以发现里面主要调用了doHandle的方法 
 
  1. public class OAuth2AuthenticationEntryPoint extends AbstractOAuth2SecurityExceptionHandler implements
  2. AuthenticationEntryPoint {
  3.  
  4. ...
  5.  
  6. public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
  7. throws IOException, ServletException {
  8. doHandle(request, response, authException);
  9. }
  10.  
  11. ...
  12.  
  13. }
 
  • 我们再次查看doHandle的具体内容可以得出此过滤器的主要功能有3个:
  1. 解析异常类型
  2. 扩展respone的一些属性和内容
  3. respone 刷新缓存直接返回
 
  1. protected final void doHandle(HttpServletRequest request, HttpServletResponse response, Exception authException)
  2. throws IOException, ServletException {
  3. try {
  4. ResponseEntity<?> result = exceptionTranslator.translate(authException);
  5. result = enhanceResponse(result, authException);
  6. exceptionRenderer.handleHttpEntityResponse(result, new ServletWebRequest(request, response));
  7. response.flushBuffer();
  8. }
  9.  
  10. ...
  11. }
 

4.重写异常处理器

  • 对默认异常处理器的分析,我们可以得出如果是我们需要的异常(401异常)则用我们自定义的方法进行处理,如果是其他异常则让原来的异常处理器处理即可,大致思路如下:
  1. 通过exceptionTranslator.translate(authException)解析异常,判断异常类型(status)
  2. 如果不是401异常,则直接调用默认异常处理器的处理方法即可
  3. 如果是401异常则向授权服务器发起token刷新的请求
  4. 如果token刷新成功,则通过request.getRequestDispatcher(request.getRequestURI()).forward(request,response);再次请求资源
  5. 如果token刷新失败,要么跳转到登陆页面(web的话也可以通过response.sendirect跳转到登陆页面),要么返回错误信息(json)
 
  1. public class LLGAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {
  2.  
  3. @Autowired
  4. private OAuth2ClientProperties oAuth2ClientProperties;
  5. @Autowired
  6. private BaseOAuth2ProtectedResourceDetails baseOAuth2ProtectedResourceDetails;
  7. private WebResponseExceptionTranslator<?> exceptionTranslator = new DefaultWebResponseExceptionTranslator();
  8. @Autowired
  9. RestTemplate restTemplate;
  10.  
  11. @Override
  12. public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
  13. try {
  14. //解析异常,如果是401则处理
  15. ResponseEntity<?> result = exceptionTranslator.translate(authException);
  16. if (result.getStatusCode() == HttpStatus.UNAUTHORIZED) {
  17. MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
  18. formData.add("client_id", oAuth2ClientProperties.getClientId());
  19. formData.add("client_secret", oAuth2ClientProperties.getClientSecret());
  20. formData.add("grant_type", "refresh_token");
  21. Cookie[] cookie=request.getCookies();
  22. for(Cookie coo:cookie){
  23. if(coo.getName().equals("refresh_token")){
  24. formData.add("refresh_token", coo.getValue());
  25. }
  26. }
  27. HttpHeaders headers = new HttpHeaders();
  28. headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
  29. Map map = restTemplate.exchange(baseOAuth2ProtectedResourceDetails.getAccessTokenUri(), HttpMethod.POST,
  30. new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();
  31. //如果刷新异常,则坐进一步处理
  32. if(map.get("error")!=null){
  33. // 返回指定格式的错误信息
  34. response.setStatus(401);
  35. response.setHeader("Content-Type", "application/json;charset=utf-8");
  36. response.getWriter().print("{\"code\":1,\"message\":\""+map.get("error_description")+"\"}");
  37. response.getWriter().flush();
  38. //如果是网页,跳转到登陆页面
  39. //response.sendRedirect("login");
  40. }else{
  41. //如果刷新成功则存储cookie并且跳转到原来需要访问的页面
  42. for(Object key:map.keySet()){
  43. response.addCookie(new Cookie(key.toString(),map.get(key).toString()));
  44. }
  45. request.getRequestDispatcher(request.getRequestURI()).forward(request,response);
  46. }
  47. }else{
  48. //如果不是401异常,则以默认的方法继续处理其他异常
  49. super.commence(request,response,authException);
  50. }
  51. } catch (Exception e) {
  52. e.printStackTrace();
  53. }
  54.  
  55. }
  56.  
  57. }
 

5.将处理器设置到过滤器上

  • 由于spring security遵循适配器的设计模式,所以我们可以直接从配置类上配置此处理器
 
  1. @EnableResourceServer
  2. @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
  3. public abstract class ResServerConfig extends ResourceServerConfigurerAdapter {
  4.  
  5. ...
  6.  
  7. @Override
  8. public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
  9. super.configure(resources);
  10.  
  11. resources.authenticationEntryPoint(new LLGAuthenticationEntryPoint());
  12.  
  13. }
 

6.实战

6.1向授权服务器获取token

  • 首先编写登陆控制器,通过restTemplate向授权服务器获取token并且存入cookie
 
  1. PostMapping(value = "/login")
  2. public ResponseEntity<OAuth2AccessToken> login(@RequestBody @Valid LoginDTO loginDTO, BindingResult bindingResult, HttpServletResponse response) throws Exception {
  3. if (bindingResult.hasErrors()) {
  4. throw new Exception("登录信息格式错误");
  5. } else {
  6. //Http Basic 验证
  7. String clientAndSecret = oAuth2ClientProperties.getClientId() + ":" + oAuth2ClientProperties.getClientSecret();
  8. //这里需要注意为 Basic 而非 Bearer
  9. clientAndSecret = "Basic " + Base64.getEncoder().encodeToString(clientAndSecret.getBytes());
  10. HttpHeaders httpHeaders = new HttpHeaders();
  11. httpHeaders.set("Authorization", clientAndSecret);
  12. //授权请求信息
  13. MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
  14. map.put("username", Collections.singletonList(loginDTO.getUsername()));
  15. map.put("password", Collections.singletonList(loginDTO.getPassword()));
  16. map.put("grant_type", Collections.singletonList(oAuth2ProtectedResourceDetails.getGrantType()));
  17. map.put("scope", oAuth2ProtectedResourceDetails.getScope());
  18. //HttpEntity
  19. HttpEntity httpEntity = new HttpEntity(map, httpHeaders);
  20. //获取 Token
  21. ResponseEntity<OAuth2AccessToken> body = restTemplate.exchange(oAuth2ProtectedResourceDetails.getAccessTokenUri(), HttpMethod.POST, httpEntity, OAuth2AccessToken.class);
  22. OAuth2AccessToken oAuth2AccessToken = body.getBody();
  23. response.addCookie(new Cookie("access_token", oAuth2AccessToken.getValue()));
  24. response.addCookie(new Cookie("refresh_token", oAuth2AccessToken.getRefreshToken().getValue()));
  25. return body;
  26. }
  27. }
 
  • 之后我在这里通过idea的 HTTP Client 工具模拟请求获取token
  • 获取access_token请求(/oauth/token) 
    请求所需参数:client_id、client_secret、grant_type、username、password

6.2模拟失效token访问资源服务器

  • 使用失效的token访问资源的时候,可以发现断点直接到达异常处理器,由此看出token确实是失效的并且进入了异常处理器进行处理,最终通过refresh_token获取到最新的token再次成功访问获取资源
  • 刷新token请求(/oauth/token) 
    请求所需参数:grant_type、refresh_token、client_id、client_secret 
    其中grant_type为固定值:grant_type=refresh_token

7.总结

本次由于对spring security oauth2了解不深入,导致在寻找异常抛出解决方法的时候折腾了一下,整体的思路并不复杂,只是用到了最普通的请求转发,但是需要对过滤器链有一定了解,打断点慢慢看是不错的选择。

 

 

如果令牌不是存放在cookie而是licalstorage中该如何把令牌返回给前段呢?毕竟我们服务器内部进行了重定向,前端完全不知道令牌被刷新了。

 

posted @ 2024-10-24 15:35  CharyGao  阅读(53)  评论(0编辑  收藏  举报