
用户模块微信小程序授权、登录、用户信息管理“你是谁”——这是任何互联网应用的第一个问题。在无人售货柜项目中用户模块就是回答这个问题的它承载着微信授权登录、JWT签发、用户信息管理的全部职责。一、微信小程序登录流程微信小程序登录是一个三方交互的过程。很多新手在这里被绕晕画张图就清楚了小程序端 后端服务 微信API | | | |--1.wx.login()------| | |--code--------------| | | |--2.发code/appid/secret-| | |--openidsession_key--| | |--3.签发JWT------------| |--返回token----------| | |--4.后续请求带token--| | | |--校验JWT--------------|步骤拆解调用wx.login()获取临时code有效期5分钟一次性后端拿code appid appsecret请求微信接口jscode2session微信返回openid用户唯一标识和session_key后端用openid创建/查找用户签发 JWT小程序端拿到 JWT后续请求都带在Authorization头里二、用户表设计CREATETABLEuser(idbigintNOTNULLCOMMENT用户ID雪花算法,openidvarchar(64)NOTNULLCOMMENT微信openid,unionidvarchar(64)DEFAULTNULLCOMMENT微信unionid(多平台打通用),nicknamevarchar(50)DEFAULTNULLCOMMENT昵称,avatarvarchar(500)DEFAULTNULLCOMMENT头像URL,phonevarchar(11)DEFAULTNULLCOMMENT手机号(AES加密存储),statustinyintNOTNULLDEFAULT1COMMENT1-正常 2-封禁 3-注销,last_login_timedatetimeDEFAULTNULLCOMMENT最后登录时间,create_timedatetimeNOTNULLDEFAULTCURRENT_TIMESTAMP,update_timedatetimeNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,PRIMARYKEY(id),UNIQUEKEYuk_openid(openid))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT用户表;设计注意点openid必加唯一索引是最核心的查询条件phone建议加密存储保护隐私status不要用deleted用户注销和逻辑删除是有区别的三、后端登录与 JWT 签发RestControllerRequestMapping(/user)publicclassUserController{AutowiredprivateUserServiceuserService;PostMapping(/login)publicResultLoginVOlogin(RequestBodyLoginDTOdto){returnResult.ok(userService.login(dto.getCode()));}}ServicepublicclassUserService{Value(${wechat.appid})privateStringappid;Value(${wechat.secret})privateStringsecret;AutowiredprivateRestTemplaterestTemplate;AutowiredprivateRedisTemplateString,ObjectredisTemplate;/** * 微信登录code换openid签发JWT */publicLoginVOlogin(Stringcode){// 1. 调用微信接口获取openid session_keyStringurlString.format(https://api.weixin.qq.com/sns/jscode2session?appid%ssecret%sjs_code%sgrant_typeauthorization_code,appid,secret,code);WechatSessionResponseresprestTemplate.getForObject(url,WechatSessionResponse.class);if(resp.getErrcode()!nullresp.getErrcode()!0){thrownewBusinessException(微信登录失败: resp.getErrmsg());}// 2. 根据openid查找或创建用户Stringopenidresp.getOpenid();UseruseruserMapper.selectByOpenid(openid);if(usernull){usernewUser();user.setId(IdUtil.getSnowflake().nextId());user.setOpenid(openid);user.setStatus(1);user.setLastLoginTime(LocalDateTime.now());userMapper.insert(user);}else{// 检查用户状态if(user.getStatus()!1){thrownewBusinessException(账号状态异常请联系客服);}user.setLastLoginTime(LocalDateTime.now());userMapper.updateById(user);}// 3. 缓存 session_key后续解密手机号等场景需要redisTemplate.opsForValue().set(session_key:openid,resp.getSessionKey(),7,TimeUnit.DAYS);// 4. 签发JWTStringtokenJwtUtil.generateToken(user.getId(),openid);// 5. 返回LoginVOvonewLoginVO();vo.setToken(token);vo.setUserId(user.getId());vo.setIsNew(user.getCreateTime().equals(user.getUpdateTime()));returnvo;}}四、用户信息授权在微信 2021 年改版后wx.getUserProfile需要用户点击按钮触发。后端接收后更新用户表PostMapping(/update/profile)publicResultVoidupdateProfile(RequestBodyProfileDTOdto){LonguserIdUserContext.getCurrentUserId();// 从JWT中解析UserusernewUser();user.setId(userId);user.setNickname(dto.getNickname());user.setAvatar(dto.getAvatarUrl());userMapper.updateById(user);// 清除用户缓存redisTemplate.delete(user:info:userId);returnResult.ok();}五、手机号授权与解密手机号获取需要解密因为微信传给后端的手机号是加密的PostMapping(/bind/phone)publicResultVoidbindPhone(RequestBodyPhoneDTOdto){LonguserIdUserContext.getCurrentUserId();StringopeniduserMapper.selectById(userId).getOpenid();// 从Redis获取session_keyStringsessionKey(String)redisTemplate.opsForValue().get(session_key:openid);if(sessionKeynull){thrownewBusinessException(session_key过期请重新登录);}// AES解密手机号StringphoneJsonWxUtil.decrypt(dto.getEncryptedData(),sessionKey,dto.getIv());WxPhoneInfophoneInfoJSONUtil.toBean(phoneJson,WxPhoneInfo.class);// 加密后存入DBUserusernewUser();user.setId(userId);user.setPhone(AesUtil.encrypt(phoneInfo.getPurePhoneNumber()));userMapper.updateById(user);returnResult.ok();}六、JWT 签发与网关校验publicclassJwtUtil{Value(${jwt.secret})privateStringsecret;Value(${jwt.expiration:86400000})// 默认24小时privatelongexpiration;publicStringgenerateToken(LonguserId,Stringopenid){returnJwts.builder().setSubject(String.valueOf(userId)).claim(openid,openid).setIssuedAt(newDate()).setExpiration(newDate(System.currentTimeMillis()expiration)).signWith(SignatureAlgorithm.HS256,secret).compact();}publicClaimsparseToken(Stringtoken){returnJwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}}在 Spring Cloud Gateway 中统一校验ComponentpublicclassAuthFilterimplementsGlobalFilter{OverridepublicMonoVoidfilter(ServerWebExchangeexchange,GatewayFilterChainchain){Stringpathexchange.getRequest().getURI().getPath();// 白名单放行if(path.startsWith(/user/login))returnchain.filter(exchange);Stringtokenexchange.getRequest().getHeaders().getFirst(Authorization);if(tokennull||!token.startsWith(Bearer )){returnunauthorized(exchange,未登录);}try{ClaimsclaimsJwtUtil.parseToken(token.substring(7));// 检查Token是否在黑名单中用户注销后if(redisTemplate.hasKey(token:blacklist:token)){returnunauthorized(exchange,Token已失效);}// 将userId写入请求头下游服务直接读取exchange.getRequest().mutate().header(X-User-Id,claims.getSubject());}catch(Exceptione){returnunauthorized(exchange,Token无效或已过期);}returnchain.filter(exchange);}}七、用户状态管理正常(1) ──→ 封禁(2) ←── 运营手动操作 │ └──→ 注销(3) ←── 用户主动注销7天冷静期用户登录时检查 status非正常状态拒绝登录。封禁状态保留 openid防止换号绕封禁。八、Feign 对外提供用户信息FeignClient(nameuser-service,path/user)publicinterfaceUserFeignClient{GetMapping(/info/{id})ResultUserVOgetUserById(PathVariable(id)Longid);GetMapping(/info/openid/{openid})ResultUserVOgetUserByOpenid(PathVariable(openid)Stringopenid);}九、Redis 缓存 Token 黑名单// 缓存用户信息publicUserVOgetUserById(Longid){Stringkeyuser:info:id;UserVOcached(UserVO)redisTemplate.opsForValue().get(key);if(cached!null)returncached;UseruseruserMapper.selectById(id);UserVOvoBeanUtil.copyProperties(user,UserVO.class);redisTemplate.opsForValue().set(key,vo,1,TimeUnit.HOURS);returnvo;}// 注销/登出时Token加入黑名单publicvoidlogout(Stringtoken){ClaimsclaimsJwtUtil.parseToken(token);longttlclaims.getExpiration().getTime()-System.currentTimeMillis();redisTemplate.opsForValue().set(token:blacklist:token,1,ttl,TimeUnit.MILLISECONDS);}十、常见问题1. session_key 过期怎么办session_key 本身无有效期但微信建议不自行维护过期。通常做法是每次wx.login都获取新的 session_key缓存覆盖旧的即可。2. Token 续期策略推荐双 Token机制access_token 24小时 refresh_token 30天。access_token 过期后用 refresh_token 换新refresh_token 过期则重新走微信授权登录。3. 多端登录互踢无人售货柜小程序一般单设备登录不需要考虑互踢。如果需要可以用openid → current_token映射在签发新 Token 时把旧 Token 加入黑名单。用户模块是面门写好不难难的是想清楚每一个状态流转和安全边界。