
在 Web 应用开发中,用户登录是必不可少的一环。传统的账号密码登录往往让用户感到繁琐:忘记密码、重复注册、输入错误... 而微信扫码登录虽然便捷,但个人订阅号并不支持网页授权(OAuth2.0)。
这就陷入了一个死胡同吗?当然不是!
我们完全可以利用微信公众号的基础消息接口,通过“曲线救国”的方式实现一种安全、低成本且用户体验极佳的登录方案:用户扫码关注 -> 回复关键词 -> 获取验证码 -> 网页输入登录。
本文将带你从零开始,使用 Spring Boot + Hutool 打造一套完整的验证码登录系统,并附带内网穿透工具的使用指南。
这种登录方式的本质,是打通“微信用户”与“网页端”的信息壁垒。

微信公众号分为订阅号、服务号和企业号,不同账号类型的功能权限差异较大:
账号类型 | 认证要求 | 接口权限 | 适用场景 |
|---|---|---|---|
订阅号 | 个人/企业 | 基础接口 | 资讯推送 |
服务号 | 企业认证 | 全部接口 | 服务交互 |
测试号 | 无需认证 | 全部接口 | 开发测试 |
提示:为了展示最真实的开发流程,本文将使用真实的微信公众号进行实战演示,带你体验完整的接入过程。
在开始开发前,我们需要在微信后台配置服务器地址(URL)和令牌(Token)。
⚠️ 2025年12月1日 新版入口变动说明
以前我们都是直接在 微信公众平台 (mp.weixin.qq.com) 的“基本配置”里进行设置。
但从 2025年12月1日 起,微信官方将“开发者工具”和“接口权限配置”相关功能迁移到了全新的 微信开发者平台。
具体操作流程:
URL: 填写您的公网访问地址(需配合下一步的内网穿透),例如 http://你的域名/wx/callback。
Token: 自定义一个字符串(如 mySecretToken),必须与 Java 代码保持一致。

注意:此时先不要点击“提交”,等 Java 代码写好并启动后再提交。
微信服务器需要访问你本地的开发电脑,所以需要“内网穿透”。
配置 Natapp:
natapp.exe)。
natapp.exe 所在的目录。YOUR_AUTHTOKEN 替换为你后台显示的 authtoken):natapp -authtoken=YOUR_AUTHTOKEN
Forwarding 后的公网地址(如 http://xxxx.natappfree.cc)。微信公众号的对接,需要做一个 Spring Boot 服务,提供同名接口的不同请求方式:
配置步骤:
回到微信测试号管理后台,找到 “接口配置信息”:
http://xxxx.natappfree.cc/wx/callback。mySecretToken),必须与 Java 代码保持一致。注意:此时先不要点击“提交”,等 Java 代码写好并启动后再提交。

除了 Spring Web,我们引入两个神器:
commons-codec: 也就是 Apache 的加密库,用于 SHA1 签名验证(不用自己手写算法了)。hutool-all: 国产 Java 工具包之光,用来生成随机数、管理带过期的缓存、解析 XML 等。<dependencies>
<!-- 简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- XML 解析 -->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
<!-- 签名加密 -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<!-- Hutool 工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
</dependencies>将配置外置,方便管理。
server.port=8080
# 微信公众号Token,请确保与微信后台配置一致
wechat.token=mySecretToken这是与微信服务器“对话”的唯一入口。
注意: 微信后台配置 URL 时,如果填的是 http://域名/wx/callback,那么我们的 Controller 就要监听这个路径。
@Slf4j
@RestController
@RequestMapping("/wx")
public class CallBackController {
@Value("${wechat.token}")
private String token; // 读取配置文件
// 使用 Hutool 的 TimedCache,自动管理过期,无需手动清理!
// Key: 验证码, Value: 用户OpenID
public static final TimedCache<String, String> LOGIN_CACHE = CacheUtil.newTimedCache(300 * 1000); // 5分钟
static {
LOGIN_CACHE.schedulePrune(1000); // 每秒检查一次过期
}
/**
* 1. 服务器验证接口 (GET)
* 微信后台点击“提交”时调用,用于确认服务器身份。
*/
@GetMapping("/callback")
public String verify(@RequestParam(name = "signature", required = false) String signature,
@RequestParam(name = "timestamp", required = false) String timestamp,
@RequestParam(name = "nonce", required = false) String nonce,
@RequestParam(name = "echostr", required = false) String echostr) {
// 1. 参数校验(防御性编程)
if (signature == null || timestamp == null || nonce == null || echostr == null) {
return "fail";
}
// 2. 签名校验 (安全核心)
// 将 token、timestamp、nonce 字典排序并 SHA1 加密
String sha1 = SHA1.getSHA1(token, timestamp, nonce);
// 3. 比对成功,原样返回 echostr
if (sha1 != null && sha1.equals(signature)) {
return echostr;
}
return "fail";
}
/**
* 2. 消息接收接口 (POST)
* 用户发送消息时,微信会将 XML 推送到这里。
*/
@PostMapping(value = "/callback", produces = "application/xml;charset=UTF-8")
public String handleMessage(@RequestBody String requestBody,
@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce) {
// 同样需要验签,防止黑客伪造请求
if (!SHA1.checkSignature(token, timestamp, nonce, signature)) {
return "";
}
// 解析 XML
Map<String, String> msgMap = MessageUtil.parseXml(requestBody);
String fromUser = msgMap.get("FromUserName"); // 用户 OpenID
String toUser = msgMap.get("ToUserName"); // 公众号 ID
String content = msgMap.get("Content"); // 消息内容
// 业务逻辑:用户回复 "验证码"
if ("text".equals(msgMap.get("MsgType")) && "验证码".equals(content)) {
// 生成 6 位纯数字验证码
String code = RandomUtil.randomNumbers(6);
// 存入缓存
LOGIN_CACHE.put(code, fromUser);
// 构造回复 XML
// 注意:回复时,发送人是公众号(toUser),接收人是用户(fromUser)
return MessageUtil.textMessageToXml(fromUser, toUser,
"您的验证码是:" + code + "\n有效期为5分钟。");
}
return MessageUtil.textMessageToXml(fromUser, toUser, "回复 '验证码' 获取登录验证码。");
}
}为了代码整洁,我们把繁琐的逻辑抽离出来。
SHA1 签名工具类 (利用 commons-codec)
public class SHA1 {
public static String getSHA1(String token, String timestamp, String nonce) {
String[] arr = new String[]{token, timestamp, nonce};
Arrays.sort(arr); // 1. 字典序排序
StringBuilder sb = new StringBuilder();
for (String s : arr) sb.append(s);
return DigestUtils.sha1Hex(sb.toString()); // 2. SHA1 加密 (一行代码搞定!)
}
// 封装校验逻辑
public static boolean checkSignature(String token, String timestamp, String nonce, String signature) {
String sha1 = getSHA1(token, timestamp, nonce);
return sha1 != null && sha1.equals(signature);
}
}MessageUtil XML 处理类
这里不需要复杂的对象映射,直接字符串拼接 XML 是性能最高且最不易出错的方式。
public static String textMessageToXml(String toUserName, String fromUserName, String content) {
return "<xml>" +
"<ToUserName><![CDATA[" + toUserName + "]]></ToUserName>" +
"<FromUserName><![CDATA[" + fromUserName + "]]></FromUserName>" +
"<CreateTime>" + System.currentTimeMillis() / 1000 + "</CreateTime>" +
"<MsgType><![CDATA[text]]></MsgType>" +
"<Content><![CDATA[" + content + "]]></Content>" +
"</xml>";
}前端页面非常简单,核心逻辑是:轮询 或者 用户手动点击登录。这里演示最简单的“输入验证码登录”模式。
<!-- 核心代码片段 -->
<div class="login-box">
<!-- 请替换为你自己的二维码图片 -->
<img src="你的公众号二维码.jpg" alt="扫码关注">
<p>关注回复 <b>"验证码"</b> 获取登录码</p>
<input type="text" id="code" placeholder="输入6位验证码">
<button onclick="doLogin()">登录</button>
</div>
<script>
function doLogin() {
let code = document.getElementById("code").value;
// 调用后端接口
fetch('/login?code=' + code)
.then(res => res.text())
.then(data => {
if(data.includes("成功")) {
alert("登录成功!OpenID: " + data);
location.href = "/success.html";
} else {
alert(data);
}
});
}
</script>经过以上步骤,我们已经完成了一个完整的闭环。让我们来看看最终的效果:


wechat.token 必须和微信后台填写的 Token 完全一致。如果不一致,后台提交配置时会提示“Token验证失败”。From 是用户,To 是公众号。To 是用户,From 是公众号。如果不反转,用户永远收不到消息(因为你发给公众号自己了)。Map 做缓存!因为 Map 不会自动清理过期数据,运行久了会内存泄漏。Hutool 的 TimedCache 是轻量级场景的最佳选择。通过这套方案,我们成功绕过了个人订阅号的权限限制,实现了一个体验流畅的扫码登录功能。这不仅展示了 Spring Boot 的灵活性,也体现了 Hutool 等工具库在提升开发效率上的巨大价值。
希望这篇详细的教程能帮助到你,如果有任何问题,欢迎在评论区留言交流!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。