大家好,我是小悟。
微信扫码登录是一种基于OAuth 2.0协议的无密码登录方式,核心流程可概括为“生成唯一标识→扫码关联账号→确认授权→返回登录态”。具体步骤如下:
生成唯一二维码:PC端向微信服务器发送请求,获取包含唯一UUID(或state参数,用于防CSRF)的二维码链接,前端渲染成二维码展示给用户。
扫码与关联:用户用已登录的微信APP扫描二维码,微信客户端提取二维码中的UUID,并将当前登录的微信账号(openid)与该UUID绑定,向微信服务器发送“已扫描”通知。
状态轮询:PC端通过长轮询(或WebSocket)持续向微信服务器查询该UUID的状态,若返回“已扫描待确认”,则提示用户“请在手机端确认登录”。
确认与登录:用户在手机端点击“确认登录”,微信服务器将openid、access_token(用于获取用户信息的临时凭证)返回给PC端。
PC端用access_token获取用户信息,完成登录并生成长期登录态(如JWT)。
整个过程的关键是UUID的唯一性(确保一个二维码只对应一个登录请求)和state参数的防CSRF(防止恶意伪造登录请求)。
Java实现微信扫码登录代码
1. 准备工作
在微信公众平台注册网站应用,获取以下关键参数:
APPID:应用的唯一标识(如wx1234567890abcdef);
APPSECRET:应用的密钥(用于换取access_token,需严格保密);
REDIRECT_URI:
微信回调的地址(如
https://yourdomain.com/wechat/callback,需与微信公众平台配置一致)。
添加依赖(Maven):
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-webflux
com.alibaba
fastjson
1.2.83
org.springframework.boot
spring-boot-starter-data-redis
io.jsonwebtoken
jjwt
0.9.1
2. 生成微信登录二维码链接
作用:构造微信扫码登录的入口URL,包含APPID、REDIRECT_URI、state(随机生成,防CSRF)等参数。
import java.net.URLEncoder;
import java.util.UUID;
@Service
public class WeChatLoginService {
@Value("${wechat.appid}")
private String appId; // 从配置文件读取APPID
@Value("${wechat.redirect-uri}")
private String redirectUri; // 从配置文件读取回调地址
/**
* 生成微信扫码登录的URL
* @return 包含二维码链接和state的Map
*/
public Map buildQrConnectUrl() {
// 生成随机state(防CSRF)
String state = UUID.randomUUID().toString();
// 构造微信扫码登录URL(snsapi_login表示网页授权)
String qrConnectUrl = String.format(
"https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_login&state=%s#wechat_redirect",
appId, URLEncoder.encode(redirectUri, StandardCharsets.UTF_8), state
);
// 将state存入Redis(标记为“未登录”,超时时间10分钟)
redisTemplate.opsForValue().set("wechat_login_state:" + state, "pending", Duration.ofMinutes(10));
return Map.of("qrUrl", qrConnectUrl, "state", state);
}
}
代码说明:
snsapi_login:微信网页授权的作用域,表示需要获取用户基本信息;
URLEncoder.encode:对回调地址进行URL编码,避免特殊字符导致请求失败;
state:随机生成的UUID,用于防止CSRF攻击(后续回调时会验证该参数);
Redis缓存:存储state和登录状态,超时时间设置为10分钟(避免长期占用内存)。
3. 处理微信回调(获取code并换取access_token)
作用:微信用户扫描二维码并授权后,微信服务器会回调REDIRECT_URI,携带code(授权码)和state参数。此处需验证state,并用code换取access_token(用于获取用户信息)。
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class WeChatCallbackController {
@Autowired
private WeChatLoginService weChatLoginService;
@Autowired
private RestTemplate restTemplate; // Spring提供的HTTP客户端
@Value("${wechat.appsecret}")
private String appSecret; // 从配置文件读取APPSECRET
/**
* 微信回调接口(处理扫码授权后的code)
*/
@GetMapping("/wechat/callback")
public void wechatCallback(
@RequestParam String code,
@RequestParam String state,
HttpServletResponse response
) throws IOException {
// 1. 验证state(防止CSRF)
String storedState = redisTemplate.opsForValue().get("wechat_login_state:" + state);
if (storedState == null || !"pending".equals(storedState)) {
response.sendRedirect("https://your-pc-site.com/error?msg=Invalid state");
return;
}
// 2. 用code换取access_token
String tokenUrl = String.format(
"https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
weChatLoginService.getAppId(), appSecret, code
);
String tokenResponse = restTemplate.getForObject(tokenUrl, String.class);
JSONObject tokenJson = JSON.parseObject(tokenResponse);
// 检查是否有错误
if (tokenJson.containsKey("errcode")) {
response.sendRedirect("https://your-pc-site.com/error?msg=" + tokenJson.getString("errmsg"));
return;
}
String accessToken = tokenJson.getString("access_token");
String openId = tokenJson.getString("openid"); // 用户唯一标识
// 3. 获取用户信息
String userInfoUrl = String.format(
"https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN",
accessToken, openId
);
String userInfoResponse = restTemplate.getForObject(userInfoUrl, String.class);
JSONObject userInfoJson = JSON.parseObject(userInfoResponse);
// 4. 业务逻辑:查找/注册用户,生成登录态
String token = weChatLoginService.handleLogin(userInfoJson);
// 5. 登录成功,跳转回原页面并携带token
response.sendRedirect("https://your-pc-site.com/login-success?token=" + token);
}
}
代码说明:
code:微信授权码,有效期为5分钟,需立即换取access_token;
state验证:确保回调请求来自微信(防止恶意伪造);
access_token:临时凭证,用于获取用户信息,有效期为2小时;
openid:用户的唯一标识(同一用户在同一个公众号下openid不变);
userInfoUrl:获取用户信息的接口(lang=zh_CN表示返回中文信息)。
4. 获取用户信息并生成登录态
作用:用access_token和openid获取用户信息,将用户信息存入数据库(若未注册则创建),生成长期登录态(如JWT),并返回给前端。
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Service
public class WeChatLoginService {
@Value("${wechat.appid}")
private String appId;
@Value("${wechat.appsecret}")
private String appSecret;
@Autowired
private RestTemplate restTemplate;
@Autowired
private RedisTemplate redisTemplate;
@Value("${jwt.secret}")
private String jwtSecret; // JWT密钥(需严格保密)
@Value("${jwt.expiration}")
private Long jwtExpiration; // JWT有效期(如2小时,单位:毫秒)
/**
* 处理登录逻辑:查找/注册用户,生成JWT
*/
public String handleLogin(JSONObject userInfoJson) {
String openId = userInfoJson.getString("openid");
// 1. 查找用户(根据openid查询数据库)
User user = userRepository.findByOpenId(openId);
if (user == null) {
// 2. 未注册则创建用户
user = new User();
user.setOpenId(openId);
user.setNickname(userInfoJson.getString("nickname"));
user.setHeadImg(userInfoJson.getString("headimgurl"));
user.setSex(userInfoJson.getInteger("sex"));
user.setState("NORMAL"); // 正常状态
userRepository.save(user);
}
// 3. 生成JWT(包含用户ID,有效期2小时)
String token = Jwts.builder()
.setSubject(user.getId().toString()) // 主题:用户ID
.setIssuedAt(new Date()) // 签发时间
.setExpiration(new Date(System.currentTimeMillis() + jwtExpiration)) // 过期时间
.signWith(SignatureAlgorithm.HS256, jwtSecret) // 签名算法和密钥
.compact();
// 4. 将token存入Redis(用于校验登录态,超时时间与JWT一致)
redisTemplate.opsForValue().set("user_token:" + user.getId(), token, Duration.ofMillis(jwtExpiration));
return token;
}
/**
* 获取APPID(从配置文件)
*/
public String getAppId() {
return appId;
}
}
代码说明:
userRepository:用户数据访问层,用于查找/保存用户信息;
JWT生成:使用jjwt库生成JWT,包含用户ID、签发时间、过期时间,签名密钥为jwtSecret(需与前端一致);
Redis缓存:存储JWT,用于后续校验登录态(如访问其他接口时,前端携带JWT,后端通过Redis验证有效性);
用户信息:从微信返回的userInfoJson中提取,存入数据库(若未注册则创建)。
5. 前端轮询登录状态(可选)
作用:用户扫码后,PC端可通过轮询接口检查登录状态(如“已扫描”“已确认”),提升用户体验。
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import java.util.concurrent.ConcurrentHashMap;
@RestController
public class LoginStatusController {
// 存储登录状态(key:state,value:DeferredResult)
private final ConcurrentHashMap>> statusMap = new ConcurrentHashMap<>();
/**
* 前端轮询登录状态
*/
@GetMapping("/wechat/check-login")
public DeferredResult
代码说明:
DeferredResult:Spring提供的异步结果,用于实现长轮询(避免频繁请求阻塞线程);
statusMap:存储state和DeferredResult的映射,用于在登录成功后通知前端;
前端调用:用户扫码后,前端每隔1秒调用/wechat/check-login?state=xxx,直到返回“success”状态。
6. 配置文件(application.yml)
wechat:
appid: wx1234567890abcdef # 替换为你的APPID
appsecret: your_app_secret # 替换为你的APPSECRET
redirect-uri: https://yourdomain.com/wechat/callback # 替换为你的回调地址
jwt:
secret: your_jwt_secret # 替换为你的JWT密钥(需严格保密)
expiration: 7200000 # JWT有效期(2小时,单位:毫秒)
spring:
redis:
host: localhost
port: 6379
password: your_redis_password # 若有密码则填写
注意事项
安全性:
APPSECRET和jwtSecret需严格保密,不要硬编码在代码中(建议使用环境变量或配置中心);
state参数必须随机生成(如UUID),防止CSRF攻击;
微信回调的REDIRECT_URI需与微信公众平台配置一致,否则无法正常回调。
错误处理:
处理微信返回的错误码(如invalid url domain表示回调地址未配置,code expired表示授权码过期),并给用户友好的提示。
性能优化:
使用Redis缓存state和token,避免频繁访问数据库;
长轮询的超时时间不宜过长(如5秒),避免占用过多资源。
用户体验:
前端展示二维码时,添加“扫码中”“请确认”等提示,提升交互体验;
登录成功后,自动跳转回原页面(如用户之前访问的“个人中心”),避免重复操作。

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海
