物流项目第二期(用户端登录与双token三验证)

第一期内容:

物流项目第一期(登录业务)-CSDN博客

用户端登录

实现分析

登录功能 

@Data
public class UserLoginRequestVO {@ApiModelProperty("登录临时凭证")private String code;@ApiModelProperty("手机号临时凭证")private String phoneCode;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginVO {@ApiModelProperty("微信唯一标识符")private String openid;@ApiModelProperty("短令牌,有效期较短")private String accessToken;@ApiModelProperty("长令牌,有效期较长")private String refreshToken;@ApiModelProperty("是否绑定手机号 0否 1是")private Integer binding;}

小程序登录

    @Value("${sl.wechat.appid}")private String appid;@Value("${sl.wechat.secret}")private String secret;public static final String LOGIN_URL = "https://api.weixin.qq.com/sns/jscode2session";private static final int TIMEOUT = 20000;@Overridepublic JSONObject getOpenid(String code) throws IOException {//文档:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html//1. 封装参数Map<String, Object> requestParam = MapUtil.<String, Object>builder().put("appid", this.appid) //小程序 appId.put("secret", this.secret) //小程序 appSecret.put("js_code", code) //	登录时获取的 code,可通过wx.login获取.put("grant_type", "authorization_code") //授权类型.build();//2. 发送get请求HttpResponse response = HttpRequest.get(LOGIN_URL) //设置get请求url.form(requestParam) //设置表单参数.timeout(TIMEOUT) //设置超时时间,20s.execute();//执行请求if (response.isOk()) {// 3. 解析响应的结果,如果出现错误抛出异常JSONObject jsonObject = JSONUtil.parseObj(response.body());if (jsonObject.containsKey("errcode")) {throw new SLWebException(jsonObject.toString());}return jsonObject;}String errMsg = StrUtil.format("调用微信登录接口出错! code = {}", code);throw new SLWebException(errMsg);}

获取手机号

    public static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";public static final String PHONE_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=";@Overridepublic String getPhone(String code) throws IOException {//接口文档:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/phone-number/getPhoneNumber.html//1. 获取手机号,需要先获取微信access_tokenString accessToken = this.getToken();//2. 封装参数Map<String, Object> requestParam = MapUtil.<String, Object>builder().put("code", code) //手机号获取凭证.build();//3. 发送post请求HttpResponse response = HttpRequest.post(PHONE_URL + accessToken) //设置post请求url.body(JSONUtil.toJsonStr(requestParam)) //设置请求体参数.timeout(TIMEOUT) //设置超时时间,20s.execute();//执行请求if (response.isOk()) {// 4. 解析响应的结果,如果errcode不等于0抛出异常JSONObject jsonObject = JSONUtil.parseObj(response.body());if (ObjectUtil.notEqual(jsonObject.getInt("errcode"), 0)) {throw new SLWebException(jsonObject.toString());}return jsonObject.getByPath("phone_info.purePhoneNumber", String.class);}String errMsg = StrUtil.format("调用获取手机号接口出错!");throw new SLWebException(errMsg);}private String getToken() {//接口文档:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getAccessToken.html//1. 封装参数Map<String, Object> requestParam = MapUtil.<String, Object>builder().put("appid", this.appid) //小程序 appId.put("secret", this.secret) //小程序 appSecret.put("grant_type", "client_credential") //授权类型.build();//2. 发送get请求HttpResponse response = HttpRequest.get(TOKEN_URL) //设置get请求url.form(requestParam) //设置表单参数.timeout(TIMEOUT) //设置超时时间,20s.execute();//执行请求if (response.isOk()) {// 3. 解析响应的结果,如果出现错误抛出异常JSONObject jsonObject = JSONUtil.parseObj(response.body());if (jsonObject.containsKey("errcode")) {throw new SLWebException(jsonObject.toString());}//TODO 缓存token到redis,不应该每次都获取tokenreturn jsonObject.getStr("access_token");}String errMsg = StrUtil.format("调用获取接口调用凭据接口出错!");throw new SLWebException(errMsg);}

实现登录

    /*** 登录** @param userLoginRequestVO 登录code* @return 用户信息*/@Overridepublic UserLoginVO login(UserLoginRequestVO userLoginRequestVO) throws IOException {//1. 调用微信开发平台的接口,根据临时登录code获取openid等信息JSONObject jsonObject = this.wechatService.getOpenid(userLoginRequestVO.getCode());String openid = jsonObject.getStr("openid");//2. 根据openid来确认是否为新用户,新用户进行注册,老用户无需直接注册MemberDTO memberDTO = this.getByOpenid(openid);if (ObjectUtil.isEmpty(memberDTO)) {//新用户MemberDTO newMember = MemberDTO.builder().openId(openid) //设置openid.authId(jsonObject.getStr("unionid")) //设置平台唯一id,若当前小程序已绑定到微信开放平台帐号下会返回.build();//注册用户this.save(newMember);//再次查询用户信息memberDTO = this.getByOpenid(openid);}//3. 调用微信开发平台的接口,获取用户手机号,如果用户手机号有更新,需要进行更新操作String phone = this.wechatService.getPhone(userLoginRequestVO.getPhoneCode());if (ObjectUtil.notEqual(phone, memberDTO.getPhone())) {//更新手机号memberDTO.setPhone(phone);this.memberFeign.update(memberDTO.getId(), memberDTO);}//4. 生成token,将用户id存储到token中Map<String, Object> claims = MapUtil.<String, Object>builder().put(Constants.GATEWAY.USER_ID, memberDTO.getId()) //将id存入token.build();String accessToken = this.tokenService.createAccessToken(claims);//5. 返回封装响应数据return UserLoginVO.builder().openid(openid).accessToken(accessToken).binding(StatusEnum.NORMAL.getCode()).build();}
    public String createAccessToken(Map<String, Object> claims) {//生成短令牌的有效期时间单位为:分钟return JwtUtils.createToken(claims, jwtProperties.getPrivateKey(), jwtProperties.getAccessTtl(),DateField.MINUTE);}

登录流程总结

+-----------------------+
|      小程序端         |
+-----------------------+|v
+-----------------------+
|   调用微信登录接口     |
|   获取 openid         |
+-----------------------+|v
+-----------------------+
| 根据 openid 注册/查询用户 |
+-----------------------+|v
+-----------------------+
| 调用微信获取手机号接口   |
| 需要先获取 access_token |
+-----------------------+|v
+-----------------------+
| 更新用户手机号(如有变化)|
+-----------------------+|v
+-----------------------+
| 生成你自己的 JWT Token   |
| 返回给前端              |
+-----------------------+

双token三验证

单token存在的问题

在司机端、快递员端和管理管,登录成功后会生成jwt的token,前端将此token保存起来,当请求后端服务时,在请求头中携带此token,服务端需要对token进行校验以及鉴权操作,这种模式就是【单token模式】。

该模式存在什么问题吗?

其实是有问题的,主要是token有效期设置长短的问题,如果设置的比较短,用户会频繁的登录,如果设置的比较长,会不太安全,因为token一旦被黑客截取的话,就可以通过此token与服务端进行交互了。

另外一方面,token是无状态的,也就是说,服务端一旦颁发了token就无法让其失效(除非过了有效期),这样的话,如果我们检测到token异常也无法使其失效,所以这也是无状态token存在的问题。

为了解决此问题,我们将采用【双token三验证】的解决方案来解决此问题。

方案原理 

代码实现 

生成刷新token

    public static final String REDIS_REFRESH_TOKEN_PREFIX = "SL_CUSTOMER_REFRESH_TOKEN_";@Overridepublic String createRefreshToken(Map<String, Object> claims) {//生成长令牌的有效期时间单位为:小时Integer ttl = jwtProperties.getRefreshTtl();String refreshToken = JwtUtils.createToken(claims, jwtProperties.getPrivateKey(), ttl);//长令牌只能使用一次,需要将其存储到redis中,变成有状态的String redisKey = this.getRedisRefreshToken(refreshToken);this.stringRedisTemplate.opsForValue().set(redisKey, refreshToken, Duration.ofHours(ttl));return refreshToken;}private String getRedisRefreshToken(String refreshToken) {//md5是为了缩短key的长度return REDIS_REFRESH_TOKEN_PREFIX + SecureUtil.md5(refreshToken);}

刷新token

刷新token的动作是在refresh_token过期之后进行的,主要实现关键点有:

  • 校验refresh_token是否被伪造以及是否在有效期内
  • 从redis中查询,是否不存在,如果不存在说明已经失效或已经使用过,如果存在,就需要将其删除
  • 重新生成一对token,响应结果
    @Overridepublic UserLoginVO refreshToken(String refreshToken) {if (StrUtil.isEmpty(refreshToken)) {return null;}Map<String, Object> originClaims = JwtUtils.checkToken(refreshToken, this.jwtProperties.getPublicKey());if (ObjectUtil.isEmpty(originClaims)) {//token无效return null;}//通过redis校验,原token是否使用过,来确保token只能使用一次String redisKey = this.getRedisRefreshToken(refreshToken);Boolean bool = this.stringRedisTemplate.hasKey(redisKey);if (ObjectUtil.notEqual(bool, Boolean.TRUE)) {//原token过期或已经使用过return null;}//删除原tokenthis.stringRedisTemplate.delete(redisKey);//重新生成长短令牌String newRefreshToken = this.createRefreshToken(originClaims);String accessToken = this.createAccessToken(originClaims);return UserLoginVO.builder().accessToken(accessToken).refreshToken(newRefreshToken).build();}
    /*** 刷新token,校验请求头中的长令牌,生成新的长短令牌** @param refreshToken 原令牌* @return 登录结果*/@PostMapping("/refresh")@ApiOperation("刷新token")public R<UserLoginVO> refresh(@RequestHeader(Constants.GATEWAY.REFRESH_TOKEN) String refreshToken) {return R.success(this.memberService.refresh(refreshToken));}
    @Overridepublic UserLoginVO refresh(String refreshToken) {return this.tokenService.refreshToken(refreshToken);}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/81682.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

精准掌控张力动态,重构卷对卷工艺设计

一、MapleSim Web Handling Library仿真和虚拟调试解决方案 在柔性材料加工领域&#xff0c;卷对卷&#xff08;Roll-to-Roll&#xff09;工艺的效率与质量直接决定了产品竞争力。如何在高动态生产场景中实现张力稳定、减少断裂风险、优化加工速度&#xff0c;是行业长期面临的…

Voxblox算法

文章目录 1. 算法简介2. 由 TSDF 构建 ESDF 的方法2.1. 论文解读2.2. 伪代码实现 1. 算法简介 Voxblox 算法出现于文献《Voxblox: Incremental 3D Euclidean Signed Distance Fields for On-Board MAV Planning》&#xff0c;PDF 链接&#xff1a;https://arxiv.org/pdf/1611.…

计算机图形学基础--Games101笔记(一)数学基础与光栅化

文章目录 数学基础向量插值三角形插值双线性插值 平面定义法线-点表示 第一部分&#xff1a;光栅化坐标变换二维变换3D变换视图变换&#xff08;MVP&#xff09;投影变换 光栅化采样抗锯齿&#xff08;反走样&#xff09;可见性&#xff08;遮挡&#xff09; 着色与纹理Blinn-P…

@RequestParam 和 @RequestBody、HttpServletrequest 与HttpServletResponse

在Java Web开发中&#xff0c;RequestParam、RequestBody、HttpServletRequest 和 HttpServletResponse 是常用的组件&#xff0c;它们用于处理HTTP请求和响应。下面分别介绍它们的使用场景和使用方法&#xff1a; 1. RequestParam RequestParam 是Spring MVC框架中的注解&am…

【硬核数学】2. AI如何“学习”?微积分揭秘模型优化的奥秘《从零构建机器学习、深度学习到LLM的数学认知》

在上一篇中&#xff0c;我们探索了线性代数如何帮助AI表示数据&#xff08;向量、矩阵&#xff09;和变换数据&#xff08;矩阵乘法&#xff09;。但AI的魅力远不止于此&#xff0c;它最核心的能力是“学习”——从数据中自动调整自身&#xff0c;以做出越来越准确的预测或决策…

10.15 LangChain v0.3重磅升级:Tool Calling技术颠覆大模型工具调用,效率飙升300%!

LangChain v0.3 技术生态与未来发展:支持 Tool Calling 的大模型 关键词:LangChain Tool Calling, 大模型工具调用, @tool 装饰器, ToolMessage 管理, Few-shot Prompting 1. Tool Calling 的技术革新 LangChain v0.3 的工具调用(Tool Calling)功能标志着大模型应用开发进…

[架构之美]从PDMan一键生成数据库设计文档:Word导出全流程详解(二十)

[架构之美]从PDMan一键生成数据库设计文档&#xff1a;Word导出全流程详解&#xff08;二十&#xff09; 一、痛点 你是否经历过这些场景&#xff1f; 数据库字段频繁变更&#xff0c;维护文档耗时费力用Excel维护表结构&#xff0c;版本混乱难以追溯手动编写Word文档&#…

Image and depth from a conventional camera with a coded aperture论文阅读

Image and depth from a conventional camera with a coded aperture 1. 研究目标与实际意义1.1 研究目标1.2 实际问题与产业意义2. 创新方法:编码光圈设计与统计模型2.1 核心思路2.2 关键公式与模型架构2.2.1 图像形成模型2.2.2 深度可区分性准则2.2.3 统计模型与优化框架2.2…

JMeter 教程:使用 HTTP 请求的参数列表发送 POST 请求(form 表单格式)

目录 ✅ 教程目的 &#x1f6e0;️ 准备工作 &#x1f4c4; 操作步骤 第一步&#xff1a;新建测试计划 第二步&#xff1a;添加 HTTP 请求 第三步&#xff1a;添加参数列表&#xff08;表单参数&#xff09; 第四步&#xff1a;添加结果查看器 第五步&#xff1a;运行测…

交易所开发:构建功能完备的金融基础设施全流程指南

交易所开发&#xff1a;构建功能完备的金融基础设施全流程指南 ——从技术架构到合规安全的系统性解决方案 一、开发流程&#xff1a;从需求分析到运维优化 开发一款功能完备的交易所需要遵循全生命周期管理理念&#xff0c;涵盖市场定位、技术实现、安全防护和持续迭代四大阶…

【数据结构篇】排序1(插入排序与选择排序)

注&#xff1a;本文以排升序为例 常见的排序算法&#xff1a; 目录&#xff1a; 一 直接插入排序&#xff1a; 1.1 基本思想&#xff1a; 1.2 代码&#xff1a; 1.3 复杂度&#xff1a; 二 希尔排序&#xff08;直接插入排序的优化&#xff09;&#xff1a; 2.1 基本思想…

Cursor日常配置指南

文章目录 整体说明一、简单介绍1.1、简介1.2、功能 二、日常配置2.1、Profiles 简介2.2、Cursor 配置2.2.1、通用设置&#xff08;General&#xff09;2.2.2、功能设置&#xff08;Features&#xff09;2.2.2.1、长上下文&#xff08;Large context&#xff09;2.2.2.2、代码索…

客户体验数据使用的三种视角——旅程视角

企业收集到大量的客户体验数据之后&#xff0c;应该如何应用&#xff1f;有哪些主要的使用场景和分析视角呢&#xff1f;接下来&#xff0c;体验家团队将通过三篇文章陆续介绍体验数据的三种应用场景&#xff0c;以帮助企业更有效地利用体验数据进行改进。 这三个场景分别是…

大语言模型怎么进行记忆的

大语言模型怎么进行记忆的 大语言模型(LLM)本身是无状态的,每次输入独立处理,但可通过以下方式实现对话记忆及长期记忆能力: 模型架构改进 显式记忆模块: 记忆网络(Memory Networks) :在模型里嵌入可读写的记忆单元,像键值存储 (Key - Value Memory)或动态记忆矩…

Spring Boot 与 RabbitMQ 的深度集成实践(三)

高级特性实现 消息持久化 在实际的生产环境中&#xff0c;消息的可靠性是至关重要的。消息持久化是确保 RabbitMQ 在发生故障或重启后&#xff0c;消息不会丢失的关键机制。它涉及到消息、队列和交换机的持久化配置。 首先&#xff0c;配置队列持久化。在创建队列时&#xf…

成功案例丨GEZE与Altair合作推动智能建筑系统开发

Altair 作为计算智能领域的全球领导者&#xff0c;将分别在北京、上海、成都、深圳举办 “AI驱动&#xff0c;仿真未来”Altair 区域技术交流会。届时将汇聚行业专家与先锋企业&#xff0c;共同探讨仿真智能化如何赋能工业创新&#xff0c;分享最新仿真与 AI 技术的应用实践。欢…

DDoS与CC攻击:谁才是服务器的终极威胁?

在网络安全领域&#xff0c;DDoS&#xff08;分布式拒绝服务&#xff09;与CC&#xff08;Challenge Collapsar&#xff09;攻击是两种最常见的拒绝服务攻击方式。它们的目标都是通过消耗服务器资源&#xff0c;导致服务不可用&#xff0c;但攻击方式、威胁程度和防御策略存在显…

循环中使用el-form

循环中使用el-form 主要是校验问题 el-table 的数据 :data“ruleForm.tableData” :prop“‘tableData.’ $index ‘.name’” :rules“rules.name” <el-button type"primary" click"addNewData">新增项目</el-button><el-form :model&…

SAP学习笔记 - 开发13 - CAP 之 添加数据库支持(Sqlite)

上一章学习了CAP开发准备&#xff0c;添加Service。 SAP学习笔记 - 开发12 - CAP 之 开发准备&#xff0c;添加服务-CSDN博客 本章继续学习CAP开发 - 添加数据库支持&#xff08;Sqlite&#xff09;。 目录 1&#xff0c;数据库准备 - H2 内存数据库 - Sqlite数据库 a&…

【数据结构与算法】——图(三)——最小生成树

前言 本将介绍最小生成树以及普里姆算法&#xff08;Prim&#xff09;和克鲁斯卡尔&#xff08;Kruskal&#xff09; 本人其他博客&#xff1a;https://blog.csdn.net/2401_86940607 图的基本概念和存储结构&#xff1a;【数据结构与算法】——图&#xff08;一&#xff09; 源…