微信小程序的登录看起来只是调用一个wx.login,但要把它和后端服务正确打通,涉及code换取session_key、openid唯一标识、自定义登录态维护等多个环节。不少团队在实现时直接把openid暴露给前端,或者把session_key下发到客户端,这些都存在安全隐患。本文将按照微信官方推荐的登录时序,结合Spring Boot后端,完整实现一套安全可靠的小程序登录方案。

一、微信小程序登录的完整时序流程
先梳理整体流程,理解每一步的数据流向,后面写代码才不会迷惑。微信官方定义的登录时序可以概括为三个阶段。
第一阶段,小程序端调用wx.login()获取临时登录凭证code。这个code是一次性的,有效期只有5分钟,而且使用一次后就失效,它的作用类似于OAuth中的授权码。小程序拿到code后,需要通过wx.request把它发送到自己的后端服务器,注意不要在前端直接请求微信的接口,因为那样必须把AppSecret放在前端,等于把钥匙交给了所有人。
第二阶段,后端服务器拿着code、AppID和AppSecret,请求微信的code2Session接口。微信服务器校验通过后,返回openid、session_key和可选的unionid。openid是用户在当前小程序下的唯一标识,同一个微信用户在不同小程序中的openid是不同的;unionid则是在同一开放平台账号下的统一标识,适合多端应用识别同一用户。session_key是对用户数据进行加密通信的会话密钥,用于解密用户的敏感数据,比如手机号,绝对不能下发给前端。
第三阶段,后端根据openid判断该用户是否已注册,完成用户创建或更新后,生成自己的登录态(通常是JWT token)返回给小程序端。小程序将token存储在本地缓存中,后续每次请求都携带token,后端通过拦截器校验登录态。整个流程中,前端只接触code和token,敏感信息全部留在服务端。
二、Spring Boot后端实现登录接口
理解了时序,下面动手实现。先在配置文件中填入小程序的凭证信息,建议通过配置类注入,方便管理和更换环境。
@Configuration
public class WxConfig {
@Value("${wx.appid}")
private String appid;
@Value("${wx.secret}")
private String secret;
public String getAppid() { return appid; }
public String getSecret() { return secret; }
}接着封装对微信code2Session接口的调用。接口地址为https://api.weixin.qq.com/sns/jscode2session,需要拼接appid、secret、js_code和grant_type四个参数。返回结果是JSON字符串,建议定义一个实体类来接收。
@Data
public class WxSession implements Serializable {
private String openid;
private String sessionKey;
private String unionid;
private String errcode;
private String errmsg;
}
@Service
public class WxLoginService {
@Resource
private WxConfig wxConfig;
@Resource
private RestTemplate restTemplate;
public WxSession code2Session(String code) {
String url = "https://api.weixin.qq.com/sns/jscode2session"
+ "?appid=" + wxConfig.getAppid()
+ "&secret=" + wxConfig.getSecret()
+ "&js_code=" + code
+ "&grant_type=authorization_code";
String result = restTemplate.getForObject(url, String.class);
WxSession session = JSON.parseObject(result, WxSession.class);
if (session.getOpenid() == null) {
throw new BusinessException("微信登录失败:" + session.getErrmsg());
}
return session;
}
}注意这里的错误处理,微信接口在code无效或过期时会返回errcode,比如40029表示code无效,45011表示频率限制。如果不做判断直接往下走,会产生一堆openid为空的脏数据,排查起来非常麻烦。
然后是核心的登录Controller。逻辑上分三步:换取session、查询或创建用户、签发token。用户表建议以openid建唯一索引,避免并发场景下重复插入。
@RestController
@RequestMapping("/api/auth")
public class LoginController {
@Resource
private WxLoginService wxLoginService;
@Resource
private UserService userService;
@Resource
private JwtUtil jwtUtil;
@PostMapping("/login")
public Result<String> login(@RequestBody @NotBlank String code) {
// 1. 用code换取微信会话信息
WxSession session = wxLoginService.code2Session(code);
// 2. 查询用户,不存在则自动注册
User user = userService.getByOpenid(session.getOpenid());
if (user == null) {
user = userService.registerByOpenid(session.getOpenid(), session.getUnionid());
}
// 3. 签发JWT,session_key只存服务端
String token = jwtUtil.createToken(user.getId());
return Result.success(token);
}
}三、JWT登录态维护与拦截校验
拿到token之后,还需要一套完整的登录态维护机制。JWT方案下,token本身携带了用户ID和过期时间,服务端无需存session,天然适合小程序这种分布式部署场景。
JWT工具类的关键是签名和解析两个方法。签名建议使用HS256算法,密钥长度不低于256位,过期时间一般设置为7天左右,兼顾用户体验和安全性。
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
private static final long EXPIRE = 7 * 24 * 3600 * 1000L;
public String createToken(Long userId) {
return JWT.create()
.withClaim("userId", userId)
.withIssuedAt(new Date())
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRE))
.sign(Algorithm.HMAC256(secret));
}
public Long parseToken(String token) {
DecodedJWT jwt = JWT.require(Algorithm.HMAC256(secret))
.build()
.verify(token);
return jwt.getClaim("userId").asLong();
}
}再写一个拦截器统一校验token,校验通过后把用户ID放入ThreadLocal,后续业务代码直接取用。
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Resource
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
response.setStatus(401);
return false;
}
try {
Long userId = jwtUtil.parseToken(token.substring(7));
UserContext.setUserId(userId);
return true;
} catch (JWTVerificationException e) {
response.setStatus(401);
return false;
}
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
UserContext.clear();
}
}小程序端收到401状态码时,应该静默重新执行一遍wx.login流程换取新token,再重发失败的请求,用户全程无感知。可以在wx.request的封装层统一处理这个刷新重试逻辑。
四、常见安全问题与避坑建议
实现登录只是第一步,安全才是决定这套方案能否上线的关键。以下是几个高频踩坑点。
第一,session_key绝对不能下发到前端。有些教程为了图方便解密手机号,把session_key传给小程序端在本地解密,这是官方明确禁止的做法。正确做法是前端通过button组件的open-type获取encryptedData和iv,传给后端,由后端使用session_key解密。如果用的是新版手机号快速验证组件,直接把code传给后端调用getPhoneNumber接口即可,更加简单安全。
第二,code2Session接口有频率限制,一定要做防重放。恶意用户可以高频触发登录接口刷你的后端,建议在网关层对登录接口做限流,比如单个IP每分钟最多调用20次,超过直接拒绝。
第三,openid也不要直接暴露给前端当作用户标识传输。虽然openid本身泄露危害有限,但攻击者可以拿openid去探测其他接口。业务层面统一使用自己系统内的userId,openid只存在于服务端数据库。
第四,关于token注销问题。JWT本身无法主动失效,如果业务上需要强制下线能力,可以在Redis中维护一个token黑名单,或者干脆把token的校验状态放到Redis中管理,牺牲一点无状态的优雅,换取可控性。
总结一下,微信小程序登录的核心就是:前端只管code,后端负责换取openid和session_key,session_key留在服务端,登录态用JWT维护,配合拦截器统一鉴权。把这套流程吃透,再扩展到手机号绑定、多端unionid打通等场景,都会有清晰的思路。
微信小程序登录Spring BootJWT鉴权修改时间:2026-09-03 05:20:42