用户模块:微信小程序授权、登录、用户信息管理
“你是谁?”——这是任何互联网应用的第一个问题。在无人售货柜项目中,用户模块就是回答这个问题的,它承载着微信授权登录、JWT签发、用户信息管理的全部职责。
一、微信小程序登录流程
微信小程序登录是一个三方交互的过程。很多新手在这里被绕晕,画张图就清楚了:
小程序端 后端服务 微信API | | | |--1.wx.login()------>| | |<--code--------------| | | |--2.发code/appid/secret->| | |<--openid+session_key--| | |--3.签发JWT------------>| |<--返回token----------| | |--4.后续请求带token-->| | | |--校验JWT-------------->|步骤拆解:
- 调用
wx.login()获取临时code(有效期5分钟,一次性) - 后端拿
code + appid + appsecret请求微信接口jscode2session - 微信返回
openid(用户唯一标识)和session_key - 后端用
openid创建/查找用户,签发 JWT - 小程序端拿到 JWT,后续请求都带在
Authorization头里
二、用户表设计
CREATETABLE`user`(`id`bigintNOTNULLCOMMENT'用户ID,雪花算法',`openid`varchar(64)NOTNULLCOMMENT'微信openid',`unionid`varchar(64)DEFAULTNULLCOMMENT'微信unionid(多平台打通用)',`nickname`varchar(50)DEFAULTNULLCOMMENT'昵称',`avatar`varchar(500)DEFAULTNULLCOMMENT'头像URL',`phone`varchar(11)DEFAULTNULLCOMMENT'手机号(AES加密存储)',`status`tinyintNOTNULLDEFAULT'1'COMMENT'1-正常 2-封禁 3-注销',`last_login_time`datetimeDEFAULTNULLCOMMENT'最后登录时间',`create_time`datetimeNOTNULLDEFAULTCURRENT_TIMESTAMP,`update_time`datetimeNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,PRIMARYKEY(`id`),UNIQUEKEY`uk_openid`(`openid`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='用户表';设计注意点:
openid必加唯一索引,是最核心的查询条件phone建议加密存储,保护隐私status不要用deleted,用户注销和逻辑删除是有区别的
三、后端登录与 JWT 签发
@RestController@RequestMapping("/user")publicclassUserController{@AutowiredprivateUserServiceuserService;@PostMapping("/login")publicResult<LoginVO>login(@RequestBodyLoginDTOdto){returnResult.ok(userService.login(dto.getCode()));}}@ServicepublicclassUserService{@Value("${wechat.appid}")privateStringappid;@Value("${wechat.secret}")privateStringsecret;@AutowiredprivateRestTemplaterestTemplate;@AutowiredprivateRedisTemplate<String,Object>redisTemplate;/** * 微信登录:code换openid,签发JWT */publicLoginVOlogin(Stringcode){// 1. 调用微信接口获取openid + session_keyStringurl=String.format("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",appid,secret,code);WechatSessionResponseresp=restTemplate.getForObject(url,WechatSessionResponse.class);if(resp.getErrcode()!=null&&resp.getErrcode()!=0){thrownewBusinessException("微信登录失败: "+resp.getErrmsg());}// 2. 根据openid查找或创建用户Stringopenid=resp.getOpenid();Useruser=userMapper.selectByOpenid(openid);if(user==null){user=newUser();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. 签发JWTStringtoken=JwtUtil.generateToken(user.getId(),openid);// 5. 返回LoginVOvo=newLoginVO();vo.setToken(token);vo.setUserId(user.getId());vo.setIsNew(user.getCreateTime().equals(user.getUpdateTime()));returnvo;}}四、用户信息授权
在微信 2021 年改版后,wx.getUserProfile需要用户点击按钮触发。后端接收后更新用户表:
@PostMapping("/update/profile")publicResult<Void>updateProfile(@RequestBodyProfileDTOdto){LonguserId=UserContext.getCurrentUserId();// 从JWT中解析Useruser=newUser();user.setId(userId);user.setNickname(dto.getNickname());user.setAvatar(dto.getAvatarUrl());userMapper.updateById(user);// 清除用户缓存redisTemplate.delete("user:info:"+userId);returnResult.ok();}五、手机号授权与解密
手机号获取需要解密,因为微信传给后端的手机号是加密的:
@PostMapping("/bind/phone")publicResult<Void>bindPhone(@RequestBodyPhoneDTOdto){LonguserId=UserContext.getCurrentUserId();Stringopenid=userMapper.selectById(userId).getOpenid();// 从Redis获取session_keyStringsessionKey=(String)redisTemplate.opsForValue().get("session_key:"+openid);if(sessionKey==null){thrownewBusinessException("session_key过期,请重新登录");}// AES解密手机号StringphoneJson=WxUtil.decrypt(dto.getEncryptedData(),sessionKey,dto.getIv());WxPhoneInfophoneInfo=JSONUtil.toBean(phoneJson,WxPhoneInfo.class);// 加密后存入DBUseruser=newUser();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{@OverridepublicMono<Void>filter(ServerWebExchangeexchange,GatewayFilterChainchain){Stringpath=exchange.getRequest().getURI().getPath();// 白名单放行if(path.startsWith("/user/login"))returnchain.filter(exchange);Stringtoken=exchange.getRequest().getHeaders().getFirst("Authorization");if(token==null||!token.startsWith("Bearer ")){returnunauthorized(exchange,"未登录");}try{Claimsclaims=JwtUtil.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(name="user-service",path="/user")publicinterfaceUserFeignClient{@GetMapping("/info/{id}")Result<UserVO>getUserById(@PathVariable("id")Longid);@GetMapping("/info/openid/{openid}")Result<UserVO>getUserByOpenid(@PathVariable("openid")Stringopenid);}九、Redis 缓存 + Token 黑名单
// 缓存用户信息publicUserVOgetUserById(Longid){Stringkey="user:info:"+id;UserVOcached=(UserVO)redisTemplate.opsForValue().get(key);if(cached!=null)returncached;Useruser=userMapper.selectById(id);UserVOvo=BeanUtil.copyProperties(user,UserVO.class);redisTemplate.opsForValue().set(key,vo,1,TimeUnit.HOURS);returnvo;}// 注销/登出时,Token加入黑名单publicvoidlogout(Stringtoken){Claimsclaims=JwtUtil.parseToken(token);longttl=claims.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 加入黑名单。
用户模块是"面门",写好不难,难的是想清楚每一个状态流转和安全边界。