当前位置: 首页 > news >正文

JWT单双token实现机制记录

JWT单token实现机制记录

JWT双token实现机制记录

  • 基于的jwt版本:
    <!-- JWT依赖 -->
    <dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>${jjwt.version}</version>
    </dependency>
  • 网关(spring cloud gateway)某过滤器JwtAuthFilter(专门用于移动端请求的过滤)中过滤请求头是否含Authorization: Bearar token,无则放行(return chain.filter(exchange);)有则进入此过滤器,往exchange中添加一个标记exchange.getAttributes().put(SKIP_FILTER, true);,用于跳过后续的用于CAS登录过滤的CasFilter和用于CAS的ST认证的MyCas30ProxyTicketValidationFilter(因为移动端登录采用JWT实现不再是CAS)
    • public static final String SKIP_FILTER = "SKIP_CasFilter_MyCas30ProxyTicketValidationFilter";String tokenWithSchema = exchange.getRequest().getHeaders().getFirst(AuthConstants.HEADER_AUTHORIZATION);
      // 移动端请求
      if (StringUtils.hasLength(tokenWithSchema)) {// 设置跳过CasFilter和MyCas30ProxyTicketValidationFilterexchange.getAttributes().put(SKIP_FILTER, true);ServerHttpRequest request = exchange.getRequest();
      。。。。。。
      }
  • 在CasFilter和MyCas30ProxyTicketValidationFilter的头部就检查是否跳过此过滤器
    • // 检查是否跳过当前过滤器 
      if (exchange.getAttribute(JwtAuthFilter.SKIP_FILTER) != null) { return chain.filter(exchange); // 直接跳过处理 
      }
  • 回到JwtAuthFilter,紧接着在JwtAuthFilter中,根据URL校验是否是登出请求(携带双token,即access token 和 refresh token),删除redis中存储的双token,完成登出。
  • 是否是登录白名单请求,是则放行,不做登录校验了。
    • // 放行登录白名单请求
      if (isLoginWhiteListUrl(CasFilter.getRequestUri(request))) {return chain.filter(exchange);
      }public static String getRequestUri(ServerHttpRequest request) {StringBuffer urlBuffer = new StringBuffer(request.getURI().toString());if (request.getURI().getQuery() != null) {urlBuffer.append("?").append(request.getURI().getQuery());}return urlBuffer.toString();
      }@Value("${cas.white-url}")
      private String loginWhiteList;/*** 是否登录白名单请求** @param url* @return*/
      public boolean isLoginWhiteListUrl(String url) {if (StringUtils.hasLength(this.loginWhiteList)) {Pattern pattern = Pattern.compile(this.loginWhiteList);return pattern.matcher(url).matches();} else {return false;}
      }
  • 从请求头获取access token,如果access token传输格式不正确/未携带access token,重定向到移动端登录页面。
    • log.error("access token传输格式不正确/未携带access token:{},重定向到移动端登录页面", tokenWithSchema);
      return redirectMobileEndLoginPage(exchange);@Value("${access-control.mobile-end-login-url}")private String mobileEndLoginUrl;//伪代码,也可以是别的名称,这里取的是和CAS一样的名称。Protocol.CAS3.getServiceParameterName() = "service"/*** 重定向到移动端登录页面(并在登录页面url后拼接service参数)*/public Mono<Void> redirectMobileEndLoginPage(ServerWebExchange exchange) {ServerHttpRequest request = exchange.getRequest();// 根据URI构建出访问移动端登录页面地址要携带的service(service即登录成功后要重定向到的原访问地址)String serviceUrl = GatewayCommonUtils.buildCasServiceUrl(request);String urlToRedirectTo = GatewayCommonUtils.constructRedirectUrl(this.mobileEndLoginUrl, Protocol.CAS3.getServiceParameterName() , serviceUrl);// 判断请求类型String xRequested = request.getHeaders().getFirst("x-requested-with");if ("XMLHttpRequest".equals(xRequested)) {// AJAX请求,返回携带service的CAS登录地址(在前端通过window.document.location手动重定向,以解决ajax请求不能自动重定向)// vue-resource、axios、Ajax、jQuery的$.get/$.post都是对xhr(XMLHttpRequest)的封装Map<String, Object> resultBody = new HashMap<>(2);resultBody.put("code", HttpStatus.SEE_OTHER.value());resultBody.put("url", urlToRedirectTo);// 响应状态代码同样返回303 保证部分前端框架能从json获取异常状态return GatewayCommonUtils.getVoidMono(exchange, HttpStatus.SEE_OTHER, resultBody);} else {// 浏览器请求,直接重定向return GatewayCommonUtils.redirect(exchange, urlToRedirectTo);}}/*** 重定向** @param exchange        web交换* @param urlToRedirectTo 重定向地址* @return*/public static Mono<Void> redirect(ServerWebExchange exchange, String urlToRedirectTo) {return GatewayCommonUtils.redirect(exchange.getResponse(), urlToRedirectTo);}public static Mono<Void> redirect(ServerHttpResponse response, String urlToRedirectTo) {response.setStatusCode(HttpStatus.SEE_OTHER);response.getHeaders().set("Location", urlToRedirectTo);return response.setComplete();}public static Mono<Void> getVoidMono(ServerWebExchange serverWebExchange, HttpStatus status, Map<String, Object> body) {serverWebExchange.getResponse().setStatusCode(status);byte[] bytes = JSONObject.toJSONString(body).getBytes(StandardCharsets.UTF_8);DataBuffer buffer = serverWebExchange.getResponse().bufferFactory().wrap(bytes);return serverWebExchange.getResponse().writeWith(Flux.just(buffer));}
  • 若access token正常传递,则验证token:Claims claims = jwtUtil.verifyAccessToken(accessToken);
  • 若抛出ExpiredJwtException说明token过期,设置401状态码并以报文提示客户端以客户端本地保存的refresh token请求【刷新(即重新生成)access token的/refreshToken接口"】,同时构建客户端请求refreshToken接口时需要携带的service参数(目的:在refreshToken接口中若校验出refreshToken过期/refreshToken无效/refreshToken在redis不存在需要重定向到登录页时提供登录接口要求携带的service参数)并把service(service即登录成功后要重定向到的原访问地址)传递给客户端。
    • log.info("过期access token:{},提示客户端使用refresh token刷新(重新生成)access token", accessToken);
      Map<String, Object> resultBody = new HashMap<>(2);
      //返回json结构尽量和包装类Result保持一致
      resultBody.put("code", ErrorCode.A0311.code());
      resultBody.put("message", "The access token has expired. Please use the refresh token saved by the client and the service parameter in the message to refresh the access token.");
      //构建客户端请求refreshToken接口时需要携带的service参数(目的:在refreshToken接口中若校验出refreshToken过期/refreshToken无效/refreshToken在redis不存在需要重定向到登录页时提供登录接口要求携带的service参数)
      String serviceUrl = GatewayCommonUtils.buildCasServiceUrl(request);
      resultBody.put("service", serviceUrl);
      return GatewayCommonUtils.getVoidMono(exchange, HttpStatus.UNAUTHORIZED, resultBody);ErrorCode.A0311("A0311", "授权已过期")
      
    • 客户端收到过期报文后在响应拦截器中以客户端本地保存的refresh token请求【刷新(即重新生成)access token的/refreshToken接口"】。逻辑类似如下:(下图未处理/refreshToken接口中由于refresh token过期/refresh token无效/redis无对应refresh token存在而要求客户端重定向到移动端登录页面的情况,实际开发中应按/refreshToken接口返回的提示报文补全相关代码)
    • /refreshToken接口(重新生成access token和refresh token)逻辑:
        @PostMapping("/refreshToken")@ResponseBodypublic Result refreshAccessTokenByRefreshToken(@RequestBody Map<String, String> param, HttpServletRequest request, HttpServletResponse response, HttpSession session) {String refreshToken = param.get("refreshToken");String service = param.get("service");Assert.notNull(refreshToken, "service不能为空");log.info("/auth/login/refreshToken service is {}", service);// service = https://192.168.3.214/api/plm-system/aia-portal-svc/user/site?dev_server_host=https://192.168.3.214&back_to=https://192.168.3.214/plm-system/portal/indexAssert.state(service.split("/").length > 4 && service.matches("^\\S+/api/[^\\s/]+/\\S+$"), "service值不符合接口预期");try {Claims claims = jwtUtil.verifyRefreshToken(refreshToken);UserBO redis2user = redisLoginUserService.getLoginUserInfoForToken(refreshToken, claims.get("userId").toString(), service);if(redis2user != null) {String newAccessToken = jwtUtil.generateAccessToken(claims);String newRefreshToken = jwtUtil.generateRefreshToken(claims);// 存储accessToken和用户信息到redisredisLoginUserService.setLoginUserInfoForToken(redis2user, newAccessToken, service, accessExpiration, redis2user.getId());// 存储refreshToken和用户信息到redisredisLoginUserService.setLoginUserInfoForToken(redis2user, newRefreshToken, service, refreshExpiration, redis2user.getId());// 刷新成功,返回新的access token 和 refresh token给客户端Map<String, String> map = new HashMap<>(2);map.put("newAccessToken", newAccessToken);map.put("newRefreshToken", newRefreshToken);// 后续客户端会在响应拦截器中携带新的access token重试原请求(客户端在响应拦截器中可以获取原请求地址,服务端无须传递)return Result.makeOKDataRsp("刷新成功,请将报文中的双token保存到本地并携带新的access token重试原请求,以实现无感刷新", map);}} catch (ExpiredJwtException e) {log.error("/auth/login/refreshToken refreshToken:{} expired", refreshToken);} catch (Exception e) {log.error("/auth/login/refreshToken refreshToken:{} invalid:", refreshToken, e);}// 刷新失败,通知客户端重定向移动端登录页面(接着传递service参数<--/refreshToken接口<--响应拦截器<--网关JwtAuthFilter检测到过期access token时传递来的service参数)return redirectMobileEndLoginPage(request, response, service);}

      入参中的service取自网关检测到token过期后,返回的401状态码的报文中的service参数,入参中的refresh token是保存到

    •  

http://www.xdnf.cn/news/552439.html

相关文章:

  • 精益数据分析(71/126):从移情到黏性——创业阶段的关键跨越与数据驱动策略
  • JavaScript性能优化实战(13):性能测试与持续优化
  • Mysql面经
  • 基于Python批量删除文件和批量增加文件
  • List介绍
  • Java操作Elasticsearch 之 [Java High Level REST Clientedit]
  • 数据库索引是什么,什么时候不适合使用?
  • 【深度学习实战】梯度爆炸怎么解决?
  • 量子通信技术:原理、应用与未来展望
  • 华三(H3C)IRF堆叠心跳的LACP MAD、BFD MAD和ARP MAD差异
  • 蓝桥杯2114 李白打酒加强版
  • JAVASE查漏补缺
  • CAP分布式理论
  • SpringBoot(三)--- 数据库基础
  • MySQL事务管理:事务控制与锁机制详解
  • 【Java实战】线程池 并发 并行 生命周期(详细解释)
  • idea本地debug断点小技巧
  • cplex12.9 安装教程以及下载
  • LabVIEW下AI开发
  • 在 Excel 中使用 C# .NET 用户定义函数 操作步骤
  • oracle以注释作为表头进行查询并导出
  • LeetCode 3024.三角形类型
  • EtherCAT转CANopen协议转换网关在电力行业的融合应用
  • 《微机原理与接口技术》第 7 章 输入/输出技术
  • 基于Yolov8+PyQT5的绝缘子识别系统
  • 《Effective Python》第三章 循环和迭代器——永远不要在迭代容器的同时修改它们
  • 推一帧,通一气:跨平台RTMP推流的内家功夫
  • 国产远程工具如何重新定义高效连接?——从协议支持到生态整合的全面解析
  • vue路由小案例
  • 2020年中国地级与省级高标准农田分布数据