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

苍穹外卖开发日记-微信登录

苍穹外卖开发日记:微信登录与JWT认证实战

今天完成了苍穹外卖项目小程序端的微信登录功能,打通了C端用户从授权到认证的完整链路,让我们来回顾这个核心功能的实现过程。

一、今日工作概览

时间完成内容
22:36微信登录完整功能开发

二、微信登录流程

2.1 整体流程

微信小程序登录不是传统的"用户名+密码"模式,而是通过微信官方提供的小程序登录凭证校验接口完成身份认证。

┌───────────┐ ┌───────────┐ ┌───────────────┐ │ 微信小程序 │ ① │ 后端服务 │ ② │ 微信接口服务 │ │ │ ───→ │ │ ───→ │ │ │ wx.login()│ code │ /user/ │ code │ jscode2session│ │ │ │ user/login│ │ │ │ │ ←─── │ │ ←─── │ 返回 openid │ │ │ JWT │ 生成JWT │ │ + session_key│ └───────────┘ └───────────┘ └───────────────┘

步骤说明

  1. ① 小程序端:调用wx.login()获取临时登录凭证code
  2. ② 后端服务:拿着code+appid+secret请求微信接口服务jscode2session
  3. ③ 微信接口:返回openid(用户唯一标识)和session_key
  4. ④ 后端处理:根据openid判断新老用户 → 新用户自动注册 → 生成 JWT 令牌返回

2.2 为什么用 openid?

openid是每个微信用户在该小程序下的唯一标识,不同小程序的 openid 不同。用 openid 做用户标识的好处是:

  • 无需用户输入密码,体验丝滑
  • 天然防伪造(openid 由微信后台签发)
  • 同一微信用户在不同设备上登录,openid 一致

三、代码实现

3.1 Controller 层

@RestController@RequestMapping("/user/user")@Api(tags="C端用户相关接口")@Slf4jpublicclassUserController{@AutowiredprivateUserServiceuserService;@AutowiredprivateJwtPropertiesjwtProperties;@PostMapping("/login")@ApiOperation("微信登陆")publicResult<UserLoginVO>login(@RequestBodyUserLoginDTOuserLoginDTO){log.info("用户登录:{}",userLoginDTO);// 调用微信登录逻辑,返回用户对象Useruser=userService.wxlogin(userLoginDTO);// 生成 JWT 令牌Map<String,Object>claims=newHashMap<>();claims.put(JwtClaimsConstant.USER_ID,user.getId());Stringtoken=JwtUtil.createJWT(jwtProperties.getUserSecretKey(),jwtProperties.getUserTtl(),claims);// 封装返回值UserLoginVOuserLoginVO=UserLoginVO.builder().id(user.getId()).openid(user.getOpenid()).token(token).build();returnResult.success(userLoginVO);}}

技术要点

  • Controller 只负责请求转发和响应组装,核心逻辑在 Service 层
  • JWT 的 secretKey 和 ttl 通过配置文件管理,管理端和用户端使用不同的密钥

3.2 Service 层 — 微信登录核心逻辑

@Service@Slf4jpublicclassUserServiceImplimplementsUserService{publicstaticfinalStringWX_LOGIN="https://api.weixin.qq.com/sns/jscode2session";@AutowiredprivateUserMapperuserMapper;@AutowiredprivateWeChatPropertiesweChatProperties;@OverridepublicUserwxlogin(UserLoginDTOuserLoginDTO){// 第一步:调用微信接口,用 code 换取 openidStringopenid=getOpenid(userLoginDTO.getCode());if(openid==null){thrownewLoginFailedException(MessageConstant.LOGIN_FAILED);}// 第二步:查询用户是否已存在Useruser=userMapper.getByOpenid(openid);// 第三步:新用户自动注册if(user==null){user=User.builder().openid(openid).createTime(LocalDateTime.now()).build();userMapper.insert(user);}returnuser;}privateStringgetOpenid(Stringcode){Map<String,String>map=newHashMap<>();map.put("appid",weChatProperties.getAppid());map.put("secret",weChatProperties.getSecret());map.put("js_code",code);map.put("grant_type","authorization_code");Stringjson=HttpClientUtil.doGet(WX_LOGIN,map);JSONObjectjsonObject=JSON.parseObject(json);returnjsonObject.getString("openid");}}

核心设计

  • 新用户静默注册:首次登录的用户自动插入数据库,无需额外注册步骤
  • 老用户直接登录:已存在的用户直接返回用户信息
  • 异常处理:openid 为空时抛出LoginFailedException,由全局异常处理器统一返回错误信息

3.3 Mapper 层

@MapperpublicinterfaceUserMapper{voidinsert(Useruser);@Select("select * from user where openid = #{openid}")UsergetByOpenid(Stringopenid);}
<insertid="insert"useGeneratedKeys="true"keyProperty="id">insert into user (openid, name, phone, sex, id_number, avatar, create_time) values (#{openid}, #{name}, #{phone}, #{sex}, #{idNumber}, #{avatar}, #{createTime})</insert>

技术要点:insert 使用useGeneratedKeys="true"回填主键,确保后续能通过user.getId()获取到自增ID,用于 JWT 令牌生成。

3.4 HTTP 工具类 — 调用微信接口

publicclassHttpClientUtil{staticfinalintTIMEOUT_MSEC=5*1000;publicstaticStringdoGet(Stringurl,Map<String,String>paramMap){CloseableHttpClienthttpClient=HttpClients.createDefault();CloseableHttpResponseresponse=null;Stringresult="";try{URIBuilderbuilder=newURIBuilder(url);if(paramMap!=null){for(Stringkey:paramMap.keySet()){builder.addParameter(key,paramMap.get(key));}}URIuri=builder.build();HttpGethttpGet=newHttpGet(uri);response=httpClient.execute(httpGet);if(response.getStatusLine().getStatusCode()==200){result=EntityUtils.toString(response.getEntity(),"UTF-8");}}catch(Exceptione){e.printStackTrace();}finally{response.close();httpClient.close();}returnresult;}}

技术要点

  • 使用 Apache HttpClient 进行跨服务 HTTP 调用
  • URIBuilder 自动处理 URL 参数拼接和编码
  • 5 秒超时设置,防止微信接口响应慢导致请求堆积
  • 正确的资源关闭(finally 块中关闭 response 和 httpClient)

3.5 JWT 工具类

publicclassJwtUtil{publicstaticStringcreateJWT(StringsecretKey,longttlMillis,Map<String,Object>claims){SignatureAlgorithmsignatureAlgorithm=SignatureAlgorithm.HS256;longexpMillis=System.currentTimeMillis()+ttlMillis;Dateexp=newDate(expMillis);JwtBuilderbuilder=Jwts.builder().setClaims(claims).signWith(signatureAlgorithm,secretKey.getBytes(StandardCharsets.UTF_8)).setExpiration(exp);returnbuilder.compact();}publicstaticClaimsparseJWT(StringsecretKey,Stringtoken){Claimsclaims=Jwts.parser().setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();returnclaims;}}

JWT 结构

Header.Payload.Signature xxxxx.yyyyy.zzzzz
  • Header:签名算法 HS256
  • Payload:自定义 claims(如user_id)+ 过期时间
  • Signature:使用 secretKey 对前两部分进行签名,防篡改

3.6 配置管理

Web端和C端各自独立一套 JWT 配置:

sky:jwt:admin-secret-key:itcast# 管理端密钥admin-ttl:7200000# 管理端过期时间(2小时)admin-token-name:token# 管理端请求头名称user-secret-key:itheima# 用户端密钥user-ttl:7200000# 用户端过期时间(2小时)user-token-name:authentication# 用户端请求头名称wechat:app-id:${sky.wechat.appid}# 小程序appidapp-secret:${sky.wechat.secret}# 小程序密钥

设计要点

  • 管理端和用户端使用不同的 JWT secretKey,防止一端令牌被拿到另一端滥用
  • 敏感配置(appid/secret)通过${}占位符引用,实际值放在application-dev.yml
  • 过期时间设置为 2 小时,平衡用户体验和安全性

3.7 实体类

@Data@Builder@NoArgsConstructor@AllArgsConstructorpublicclassUserimplementsSerializable{privateLongid;privateStringopenid;// 微信用户唯一标识privateStringname;// 姓名privateStringphone;// 手机号privateStringsex;// 性别 0女 1男privateStringidNumber;// 身份证号privateStringavatar;// 头像privateLocalDateTimecreateTime;// 注册时间}
@DatapublicclassUserLoginDTOimplementsSerializable{privateStringcode;// 小程序登录凭证}
@Data@Builder@NoArgsConstructor@AllArgsConstructorpublicclassUserLoginVOimplementsSerializable{privateLongid;privateStringopenid;privateStringtoken;// JWT令牌}

四、认证拦截器 — 请求鉴权

JWT 生成后,后续请求需要在请求头中携带 token。拦截器负责校验:

@ComponentpublicclassJwtTokenAdminInterceptorimplementsHandlerInterceptor{publicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{if(!(handlerinstanceofHandlerMethod)){returntrue;// 静态资源放行}// 从请求头获取 tokenStringtoken=request.getHeader(jwtProperties.getAdminTokenName());try{// 解析 JWTClaimsclaims=JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(),token);LongempId=Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());// 存入 ThreadLocalBaseContext.setCurrentId(empId);returntrue;}catch(Exceptionex){response.setStatus(401);// 未授权returnfalse;}}}

设计模式

  • 通过ThreadLocal(BaseContext)存储当前用户ID,请求结束自动清理
  • 非 HandlerMethod(静态资源等)直接放行
  • JWT 校验失败统一返回 401

五、项目结构变化

sky-take-out/ ├── sky-common/ │ └── src/main/java/com/sky/ │ ├── properties/ │ │ ├── JwtProperties.java # JWT配置属性 │ │ └── WeChatProperties.java # 微信配置属性 │ └── utils/ │ ├── JwtUtil.java # JWT工具类 │ └── HttpClientUtil.java # HTTP请求工具类 │ ├── sky-pojo/ │ └── src/main/java/com/sky/ │ ├── entity/ │ │ └── User.java # 用户实体 │ ├── dto/ │ │ └── UserLoginDTO.java # 登录请求DTO │ └── vo/ │ └── UserLoginVO.java # 登录响应VO │ └── sky-server/ └── src/main/java/com/sky/ ├── controller/user/ │ └── UserController.java # C端用户控制器(新增) ├── service/ │ ├── UserService.java # 用户服务接口(新增) │ └── impl/ │ └── UserServiceImpl.java # 用户服务实现(新增) ├── mapper/ │ └── UserMapper.java # 用户数据访问(新增) ├── interceptor/ │ └── JwtTokenAdminInterceptor.java # JWT拦截器 └── resources/ └── mapper/ └── UserMapper.xml # 用户Mapper XML(新增)

六、技术对比:管理端 vs 用户端认证

对比维度管理端(admin)用户端(user)
登录方式用户名+密码(MD5)微信code换openid
JWT密钥admin-secret-keyuser-secret-key
请求头名称tokenauthentication
JWT ClaimsEMP_IDUSER_ID
用户标识Employee表User表
注册方式管理员手动创建首次登录自动注册

七、总结

今天的工作围绕微信登录这个核心功能展开:

  1. 认证流程:小程序 code → 微信接口 → openid → 自动注册/登录 → JWT 颁发
  2. 技术栈整合:Apache HttpClient 调用微信接口、jjwt 生成 JWT 令牌、拦截器校验身份
  3. 安全管理:admin/user 双密钥隔离、配置敏感信息外置、JWT 过期控制

关键设计收获

  • 静默注册:微信登录无需用户额外操作,首次自动创建账号,提升转化率
  • 双密钥隔离:管理端和用户端使用不同的 JWT 密钥,安全边界清晰
  • ThreadLocal 传递上下文:通过BaseContext在整个请求链路中传递当前用户信息

下一步计划

  • 小程序端商品浏览功能
  • 购物车与下单功能
  • 用户端订单管理

本文是苍穹外卖项目开发的学习记录,希望对你有所帮助!

http://www.jsqmd.com/news/819351/

相关文章:

  • 2026年5月更新:美甲产业升级,甲片专用机定制厂家遴选全攻略 - 2026年企业推荐榜
  • PKSM终极指南:从菜鸟到宝可梦存档管理大师的完整路径
  • Dify插件重打包工具:标准化分发与一键部署实践
  • SPI长距离通信的时钟同步与信号完整性优化
  • 从零上手VibeCoding(ClaudeCode+DeepSeek V4.Pro)
  • 0. 深度学习课程大纲:
  • Redis 身份迷失
  • 从“边缘人”到香饽饽:35岁程序员的开源逆袭路
  • 《我的世界》Java版客户端模组开发:基于freedom-for-steve框架的底层定制实践
  • 【ElevenLabs有声书制作黄金法则】:20年音频工程师亲授,零基础7天交付商用级有声书
  • Node 版本升级后 Electron 原生模块编译失败怎么解决
  • AI工程化实战:从模型到服务的全链路部署与优化指南
  • 手摸手教你用Claude多智能体,零代码构建专属“超级办公助理”全过程
  • Claw-ED:基于Python的配置驱动Web爬虫框架实战指南
  • Gemini CLI提示词库:AI辅助开发提效的工程化实践
  • 为你的开源项目集成多模型能力,Taotoken接入方案详解
  • 基于MCP协议构建AI工具调用客户端:原理、实践与Node.js实现
  • 代码随想录算法训练营Day-50 图论02 | 99.岛屿数量-深搜、99.岛屿数量-广搜 、100.岛屿的最大面积
  • 基于Node.js的静态博客生成器:从零构建自动化内容流水线
  • 从英文恐惧到设计自信:一个产品设计师的Axure中文界面改造之旅
  • RS-485与RS-422工业通信技术详解与应用实践
  • SciDownl终极指南:5步高效获取学术文献的完整教程
  • 脚本的下一站:让自然语言直接成为可执行入口
  • 运维系列【仅供参考】:Git提交邮箱配置全攻略:从全局到本地仓库的灵活设置(附GitHub关联技巧)
  • 基于ROACH2平台的VLBI数字后端系统设计与实现
  • Perplexity搜索ACM结果不排序?揭秘影响因子加权算法逆向工程,自定义排序脚本已开源
  • 程序员的职业地图:从入门到架构师的全路径规划
  • copy4ai:专为AI工作流设计的智能复制工具,解决网页内容格式粘贴难题
  • 写论文软件哪个好?2026 全新实测:真文献 + 实证 + 全流程,虎贲等考 AI 成毕业论文最优解
  • 基于Claude的模块化代码生成框架:多代理协作开发实践