基于微信公众号实现微信登陆
约 1545 字大约 5 分钟
2025-01-19
1.实现方案选择
我们在生活中,随处可见基于微信扫码登录的场景,例如一些招聘网站、牛客网、力扣等等。但是这种微信扫码登录方式,需要微信公众号是微信认证的,个人公众号没有这个权限。我们便采用了另一种方式:基于微信公众号+验证码登录
2.必备知识
2.1何为半长连接
半长连接,可以分成两个部分理解,一个是半连接,一个是长连接。
其中长连接,我们使用了SSE协议,服务端发送事件,单双工通信,即服务端可以主动向客户端推送信息,但是客户端不可以主动推送信息
其中半连接,类似于TCP半连接,TCP半连接是指在TCP三次握手过程中,服务端开启第二次握手时候的状态,半连接状态。此时,服务器已接收到客户端的连接请求,并发送了确认,但不可以确认服务端发送的消息是否能到达客户端,需要等待客户端发回确认包,才可以建立可靠的连接,所以此时称为半连接状态。
而与这个行为类似的,在用户点击登录按钮后,视为发出登录请求,视作第一次握手,此时服务端根据设备id提供验证码,视作第二次握手,而将验证码和SSE协议共同维护在cache中,等同将socket维护在半连接队列中,最后用户在微信端输入验证码,则是第三次握手了,主要是 应用层连接这个概念和传输层连接概念的相似性
2.2登录流程
如图,为知识流平台的基于微信公众号+验证码的微信登录流程

3.项目实战
3.1微信公众号设置
要配置的内容主要有三项:服务器地址、令牌、消息加解密密钥,服务器地址填服务端绑定公众号的回调接口,令牌用于认证请求是否合法(可随便配置,看服务端是否有要求)
3.2服务端配置
3.2.1服务端token验证、消息回调
@RequestMapping(path = "wx")
@RestController
public class WxCallbackRestController {
/**
* 微信的公众号接入 token 验证,即返回echostr的参数值
*
* @param request
* @return
*/
@GetMapping(path = "callback")
public String check(HttpServletRequest request) {
String echoStr = request.getParameter("echostr");
if (StringUtils.isNoneEmpty(echoStr)) {
return echoStr;
}
return "";
}
/**
* fixme: 需要做防刷校验
* 微信的响应返回
* @param msg
* @return
*/
@PostMapping(path = "callback",
consumes = {"application/xml", "text/xml"},
produces = "application/xml;charset=utf-8")
public BaseWxMsgResVo callBack(@RequestBody WxTxtMsgReqVo msg) {
String content = msg.getContent();
if ("subscribe".equals(msg.getEvent()) || "scan".equalsIgnoreCase(msg.getEvent())) {
String key = msg.getEventKey();
if (StringUtils.isNotBlank(key) || key.startsWith("qrscene_")) {
// 带参数的二维码,扫描、关注事件拿到之后,直接登录,省却输入验证码这一步
// fixme 带参数二维码需要 微信认证,个人公众号无权限
String code = key.substring("qrscene_".length());
sessionService.autoRegisterWxUserInfo(msg.getFromUserName());
qrLoginHelper.login(code);
WxTxtMsgResVo res = new WxTxtMsgResVo();
res.setContent("登录成功");
fillResVo(res, msg);
return res;
}
}
BaseWxMsgResVo res = wxHelper.buildResponseBody(msg.getEvent(), content, msg.getFromUserName());
fillResVo(res, msg);
return res;
}
}
3.2.2建立半长连接
当用户点击登录,前台会展示一个公众号二维码图片和验证码,此时服务端已经建立了该验证码和半长连接之间的映射,这时服务端会等待用户到公众号平台发送验证码
- 后端设计:
- verifyCodeCache:缓存 验证码 和 半长连接 的映射
- deviceCodeCache:缓存 设备ID 和 验证码 的映射,确保同一台设备多次访问前台登录页面,展示的验证码只有一个,用户刷新验证码动作除外
/**
* key = 验证码, value = 半长连接
*/
private LoadingCache<String, SseEmitter> verifyCodeCache;
/**
* key = 设备ID value = 验证码
*/
private LoadingCache<String, String> deviceCodeCache;
- 用户每次访问前台展示验证码页面,都会重新生成验证码和半长连接的映射
/**
*
* 建立验证码与半长连接的映射,基于设备ID和验证码的映射获取验证码
*
* @return
* @throws IOException
*/
public SseEmitter subscribe() throws IOException {
String deviceId = ReqInfoContext.getReqInfo().getDeviceId();
// 基于设备获取验证码
String realCode = deviceCodeCache.getUnchecked(deviceId) ;
// fixme 设置15min的超时时间, 超时时间一旦设置不能修改;因此导致刷新验证码并不会增加连接的有效期
SseEmitter sseEmitter = new SseEmitter(SSE_EXPIRE_TIME);
SseEmitter oldSse = verifyCodeCache.getIfPresent(realCode);
if (oldSse != null) {
oldSse.complete();
}
verifyCodeCache.put(realCode, sseEmitter);
sseEmitter.onTimeout(() -> {
log.info("sse 超时中断 --> {}", realCode);
verifyCodeCache.invalidate(realCode);
sseEmitter.complete();
});
sseEmitter.onError((e) -> {
log.warn("sse error! --> {}", realCode, e);
verifyCodeCache.invalidate(realCode);
sseEmitter.complete();
});
// 若实际的验证码与前端显示的不同,则通知前端更新
sseEmitter.send("initCode!");
sseEmitter.send("init#" + realCode);
return sseEmitter;
}
3.2.3公众号消息回调
当用户扫码关注公众号并输入验证码后,公众号会发起回调,系统根据验证码找到该半长连接,找到该设备,并识别微信号找到用户,这时服务端向客户端推送登录成功消息,实现登录

- 输入的内容为验证码
- 微信公众号登录
/**
* 微信公众号登录
*
* @param verifyCode 用户输入的登录验证码
* @return
*/
public boolean login(String verifyCode) {
// 通过验证码找到对应的长连接
SseEmitter sseEmitter = verifyCodeCache.getIfPresent(verifyCode);
if (sseEmitter == null) {
return false;
}
String session = sessionService.loginByWx(ReqInfoContext.getReqInfo().getUserId());
try {
// 登录成功,写入session
sseEmitter.send(session);
// 设置cookie的路径
sseEmitter.send("login#" + LoginService.SESSION_KEY + "=" + session + ";path=/;");
return true;
} catch (Exception e) {
log.error("登录异常: {}", verifyCode, e);
} finally {
sseEmitter.complete();
verifyCodeCache.invalidate(verifyCode);
}
return false;
}
3.3测试
- 前台页面
- 公众号平台
4.总结
我们在本篇中,实现了基于微信公众号和验证码的登录方式,使用了SSE作为验证码和前端保持连接的方式,同时学习了半连接、长连接、半长连接的定义、区别、以及联系