苍穹外卖开发日记-微信登录
苍穹外卖开发日记微信登录与JWT认证实战今天完成了苍穹外卖项目小程序端的微信登录功能打通了C端用户从授权到认证的完整链路让我们来回顾这个核心功能的实现过程。一、今日工作概览时间完成内容22:36微信登录完整功能开发二、微信登录流程2.1 整体流程微信小程序登录不是传统的用户名密码模式而是通过微信官方提供的小程序登录凭证校验接口完成身份认证。┌───────────┐ ┌───────────┐ ┌───────────────┐ │ 微信小程序 │ ① │ 后端服务 │ ② │ 微信接口服务 │ │ │ ───→ │ │ ───→ │ │ │ wx.login()│ code │ /user/ │ code │ jscode2session│ │ │ │ user/login│ │ │ │ │ ←─── │ │ ←─── │ 返回 openid │ │ │ JWT │ 生成JWT │ │ session_key│ └───────────┘ └───────────┘ └───────────────┘步骤说明① 小程序端调用wx.login()获取临时登录凭证code② 后端服务拿着codeappidsecret请求微信接口服务jscode2session③ 微信接口返回openid用户唯一标识和session_key④ 后端处理根据openid判断新老用户 → 新用户自动注册 → 生成 JWT 令牌返回2.2 为什么用 openidopenid是每个微信用户在该小程序下的唯一标识不同小程序的 openid 不同。用 openid 做用户标识的好处是无需用户输入密码体验丝滑天然防伪造openid 由微信后台签发同一微信用户在不同设备上登录openid 一致三、代码实现3.1 Controller 层RestControllerRequestMapping(/user/user)Api(tagsC端用户相关接口)Slf4jpublicclassUserController{AutowiredprivateUserServiceuserService;AutowiredprivateJwtPropertiesjwtProperties;PostMapping(/login)ApiOperation(微信登陆)publicResultUserLoginVOlogin(RequestBodyUserLoginDTOuserLoginDTO){log.info(用户登录{},userLoginDTO);// 调用微信登录逻辑返回用户对象UseruseruserService.wxlogin(userLoginDTO);// 生成 JWT 令牌MapString,ObjectclaimsnewHashMap();claims.put(JwtClaimsConstant.USER_ID,user.getId());StringtokenJwtUtil.createJWT(jwtProperties.getUserSecretKey(),jwtProperties.getUserTtl(),claims);// 封装返回值UserLoginVOuserLoginVOUserLoginVO.builder().id(user.getId()).openid(user.getOpenid()).token(token).build();returnResult.success(userLoginVO);}}技术要点Controller 只负责请求转发和响应组装核心逻辑在 Service 层JWT 的 secretKey 和 ttl 通过配置文件管理管理端和用户端使用不同的密钥3.2 Service 层 — 微信登录核心逻辑ServiceSlf4jpublicclassUserServiceImplimplementsUserService{publicstaticfinalStringWX_LOGINhttps://api.weixin.qq.com/sns/jscode2session;AutowiredprivateUserMapperuserMapper;AutowiredprivateWeChatPropertiesweChatProperties;OverridepublicUserwxlogin(UserLoginDTOuserLoginDTO){// 第一步调用微信接口用 code 换取 openidStringopenidgetOpenid(userLoginDTO.getCode());if(openidnull){thrownewLoginFailedException(MessageConstant.LOGIN_FAILED);}// 第二步查询用户是否已存在UseruseruserMapper.getByOpenid(openid);// 第三步新用户自动注册if(usernull){userUser.builder().openid(openid).createTime(LocalDateTime.now()).build();userMapper.insert(user);}returnuser;}privateStringgetOpenid(Stringcode){MapString,StringmapnewHashMap();map.put(appid,weChatProperties.getAppid());map.put(secret,weChatProperties.getSecret());map.put(js_code,code);map.put(grant_type,authorization_code);StringjsonHttpClientUtil.doGet(WX_LOGIN,map);JSONObjectjsonObjectJSON.parseObject(json);returnjsonObject.getString(openid);}}核心设计新用户静默注册首次登录的用户自动插入数据库无需额外注册步骤老用户直接登录已存在的用户直接返回用户信息异常处理openid 为空时抛出LoginFailedException由全局异常处理器统一返回错误信息3.3 Mapper 层MapperpublicinterfaceUserMapper{voidinsert(Useruser);Select(select * from user where openid #{openid})UsergetByOpenid(Stringopenid);}insertidinsertuseGeneratedKeystruekeyPropertyidinsert into user (openid, name, phone, sex, id_number, avatar, create_time) values (#{openid}, #{name}, #{phone}, #{sex}, #{idNumber}, #{avatar}, #{createTime})/insert技术要点insert 使用useGeneratedKeystrue回填主键确保后续能通过user.getId()获取到自增ID用于 JWT 令牌生成。3.4 HTTP 工具类 — 调用微信接口publicclassHttpClientUtil{staticfinalintTIMEOUT_MSEC5*1000;publicstaticStringdoGet(Stringurl,MapString,StringparamMap){CloseableHttpClienthttpClientHttpClients.createDefault();CloseableHttpResponseresponsenull;Stringresult;try{URIBuilderbuildernewURIBuilder(url);if(paramMap!null){for(Stringkey:paramMap.keySet()){builder.addParameter(key,paramMap.get(key));}}URIuribuilder.build();HttpGethttpGetnewHttpGet(uri);responsehttpClient.execute(httpGet);if(response.getStatusLine().getStatusCode()200){resultEntityUtils.toString(response.getEntity(),UTF-8);}}catch(Exceptione){e.printStackTrace();}finally{response.close();httpClient.close();}returnresult;}}技术要点使用 Apache HttpClient 进行跨服务 HTTP 调用URIBuilder 自动处理 URL 参数拼接和编码5 秒超时设置防止微信接口响应慢导致请求堆积正确的资源关闭finally 块中关闭 response 和 httpClient3.5 JWT 工具类publicclassJwtUtil{publicstaticStringcreateJWT(StringsecretKey,longttlMillis,MapString,Objectclaims){SignatureAlgorithmsignatureAlgorithmSignatureAlgorithm.HS256;longexpMillisSystem.currentTimeMillis()ttlMillis;DateexpnewDate(expMillis);JwtBuilderbuilderJwts.builder().setClaims(claims).signWith(signatureAlgorithm,secretKey.getBytes(StandardCharsets.UTF_8)).setExpiration(exp);returnbuilder.compact();}publicstaticClaimsparseJWT(StringsecretKey,Stringtoken){ClaimsclaimsJwts.parser().setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();returnclaims;}}JWT 结构Header.Payload.Signature xxxxx.yyyyy.zzzzzHeader签名算法 HS256Payload自定义 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 实体类DataBuilderNoArgsConstructorAllArgsConstructorpublicclassUserimplementsSerializable{privateLongid;privateStringopenid;// 微信用户唯一标识privateStringname;// 姓名privateStringphone;// 手机号privateStringsex;// 性别 0女 1男privateStringidNumber;// 身份证号privateStringavatar;// 头像privateLocalDateTimecreateTime;// 注册时间}DatapublicclassUserLoginDTOimplementsSerializable{privateStringcode;// 小程序登录凭证}DataBuilderNoArgsConstructorAllArgsConstructorpublicclassUserLoginVOimplementsSerializable{privateLongid;privateStringopenid;privateStringtoken;// JWT令牌}四、认证拦截器 — 请求鉴权JWT 生成后后续请求需要在请求头中携带 token。拦截器负责校验ComponentpublicclassJwtTokenAdminInterceptorimplementsHandlerInterceptor{publicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{if(!(handlerinstanceofHandlerMethod)){returntrue;// 静态资源放行}// 从请求头获取 tokenStringtokenrequest.getHeader(jwtProperties.getAdminTokenName());try{// 解析 JWTClaimsclaimsJwtUtil.parseJWT(jwtProperties.getAdminSecretKey(),token);LongempIdLong.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());// 存入 ThreadLocalBaseContext.setCurrentId(empId);returntrue;}catch(Exceptionex){response.setStatus(401);// 未授权returnfalse;}}}设计模式通过ThreadLocalBaseContext存储当前用户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换openidJWT密钥admin-secret-keyuser-secret-key请求头名称tokenauthenticationJWT ClaimsEMP_IDUSER_ID用户标识Employee表User表注册方式管理员手动创建首次登录自动注册七、总结今天的工作围绕微信登录这个核心功能展开认证流程小程序 code → 微信接口 → openid → 自动注册/登录 → JWT 颁发技术栈整合Apache HttpClient 调用微信接口、jjwt 生成 JWT 令牌、拦截器校验身份安全管理admin/user 双密钥隔离、配置敏感信息外置、JWT 过期控制关键设计收获静默注册微信登录无需用户额外操作首次自动创建账号提升转化率双密钥隔离管理端和用户端使用不同的 JWT 密钥安全边界清晰ThreadLocal 传递上下文通过BaseContext在整个请求链路中传递当前用户信息下一步计划小程序端商品浏览功能购物车与下单功能用户端订单管理本文是苍穹外卖项目开发的学习记录希望对你有所帮助