苍穹外卖开发日记-微信登录
苍穹外卖开发日记:微信登录与JWT认证实战
今天完成了苍穹外卖项目小程序端的微信登录功能,打通了C端用户从授权到认证的完整链路,让我们来回顾这个核心功能的实现过程。
一、今日工作概览
| 时间 | 完成内容 |
|---|---|
| 22:36 | 微信登录完整功能开发 |
二、微信登录流程
2.1 整体流程
微信小程序登录不是传统的"用户名+密码"模式,而是通过微信官方提供的小程序登录凭证校验接口完成身份认证。
┌───────────┐ ┌───────────┐ ┌───────────────┐ │ 微信小程序 │ ① │ 后端服务 │ ② │ 微信接口服务 │ │ │ ───→ │ │ ───→ │ │ │ wx.login()│ code │ /user/ │ code │ jscode2session│ │ │ │ user/login│ │ │ │ │ ←─── │ │ ←─── │ 返回 openid │ │ │ JWT │ 生成JWT │ │ + session_key│ └───────────┘ └───────────┘ └───────────────┘步骤说明:
- ① 小程序端:调用
wx.login()获取临时登录凭证code - ② 后端服务:拿着
code+appid+secret请求微信接口服务jscode2session - ③ 微信接口:返回
openid(用户唯一标识)和session_key - ④ 后端处理:根据
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-key | user-secret-key |
| 请求头名称 | token | authentication |
| JWT Claims | EMP_ID | USER_ID |
| 用户标识 | Employee表 | User表 |
| 注册方式 | 管理员手动创建 | 首次登录自动注册 |
七、总结
今天的工作围绕微信登录这个核心功能展开:
- 认证流程:小程序 code → 微信接口 → openid → 自动注册/登录 → JWT 颁发
- 技术栈整合:Apache HttpClient 调用微信接口、jjwt 生成 JWT 令牌、拦截器校验身份
- 安全管理:admin/user 双密钥隔离、配置敏感信息外置、JWT 过期控制
关键设计收获:
- 静默注册:微信登录无需用户额外操作,首次自动创建账号,提升转化率
- 双密钥隔离:管理端和用户端使用不同的 JWT 密钥,安全边界清晰
- ThreadLocal 传递上下文:通过
BaseContext在整个请求链路中传递当前用户信息
下一步计划:
- 小程序端商品浏览功能
- 购物车与下单功能
- 用户端订单管理
本文是苍穹外卖项目开发的学习记录,希望对你有所帮助!
