
写这篇教程之前先坦白一下我的心态在真正动手把微信登录集成进 Spring Boot 项目之前我一度觉得这是个很麻烦的事。网上搜微信登录四个字跳出来一堆开放平台的资质说明、审核要求、回调地址配置看着就头大。但实际做完一遍之后我发现微信登录并没有想象中那么复杂它本质上就是典型的 OAuth2 授权码模式Spring Boot 这边只需要三个步骤引导用户授权、拿 code 换 token、用 token 拉取用户信息。整个流程理顺之后代码量其实非常小。这篇文章不是搬运官方文档而是把我从零到一在 Spring Boot 里接入微信扫码登录的完整过程聊清楚包括流程原理、工程代码、数据库设计、实测遇到的坑以及生产环境里值得留意的细节。无论你是刚接触 Spring Boot 的初学者还是正在规划统一登录体系的开发者这篇文章应该都能给你一条可以直接落地的路线。1. 微信登录的流程拆解从二维码到自家用户体系的必经之路很多新手卡在微信登录这里不是因为代码难写而是没搞明白微信登录到底在做什么。我先把这个流程掰开揉碎讲清楚后面写代码的时候你会觉得每一步都是顺理成章的。1.1 微信扫码登录本质上是一次 OAuth2 授权码模式微信扫码登录的本质是微信开放平台作为授权服务器帮我们确认当前操作者是谁这件事。整个过程可以类比成你去小区物业办事你出示身份证微信账号物业核实后给你一张临时通行证授权码 code你拿着通行证去业务窗口你的后端服务办理入网手续业务窗口再拿通行证找物业确认你的身份信息openid 和用户资料最后给你一把常驻门禁卡你系统里的登录态。这里对应到技术层面的顺序是前端引导用户跳转到微信授权页面用户扫码并确认授权。微信服务器回调你的后端接口带上一个授权码 code。后端拿着 code 向微信服务器请求 access_token 和 openid。后端拿着 access_token 获取微信用户的基本信息。后端在自己的系统里创建或匹配用户记录并生成业务登录态。有 OAuth2 知识背景的人看这个流程会非常亲切它就是标准 authorization code 模式。微信没有发明新协议只是把参数名和接口地址换成了自己的风格。1.2 微信登录的类型选择扫码登录和公众号内网页授权微信登录在落地时有两条主流路径很多人一开始没分清导致代码写了一半发现场景对不上。第一种是 PC 端网站扫码登录使用微信开放平台创建的网站应用通过open.weixin.qq.com/connect/qrconnect这个地址跳转用户用手机微信扫码完成授权。这种场景适合 PC 网站、管理后台一类的产品。第二种是微信公众号内的网页授权用户在微信里打开公众号菜单或文章里的 H5 页面通过open.weixin.qq.com/connect/oauth2/authorize这个地址跳转用户在微信内置浏览器里确认授权。这种场景适合公众号内嵌商城、H5 活动页等。两种路径拿 code 的入口不同获取 userinfo 的接口倒是几乎一致但 appid 完全不互通——开放平台的 appid 和公众号的 appid 是两个东西。如果你在做的是一个既有 PC 官网又有公众号 H5 的产品需要分别注册创建两个应用并在数据库里做好来源标记。1.3 为什么说 openid 才是用户唯一标识微信登录里有一个新手很容易误解的点用户信息接口返回里有一个unionid字段有的人以为用它来关联用户最稳妥。但实际上普通开发者拿到的权限里多数情况下只有openid可用。openid 是针对某个 appid 维度生成的用户唯一编号——同一个微信用户在你的两个不同微信应用下openid 是不同的但 unionid 相同前提是微信开放平台做了账号绑定。我的做法是如果业务不跨应用打通直接用 openid 做主键逻辑。如果确实需要将来打通小程序和公众号用户数据就得注册微信开放平台并把多个应用绑定到同一个开放平台账号下然后使用 unionid 建立全局用户关联。这个决策要在建表之前想清楚不然后期数据清洗很痛苦。2. 应用注册与回调地址配置90% 的登录失败其实栽在这一步代码写之前必须把户口办好。微信登录的很多诡异报错根源不是代码问题而是开放平台的配置不对。我在本地调试时就被回调地址的校验卡了整整一个下午这里必须单独拎出来讲。2.1 开放平台账号和网站应用的创建流程先去微信开放平台官网使用开发者账号登录在管理中心创建网站应用。这里需要提供网站域名、应用名称、应用简介等信息还要上传 Logo 和资质材料。创建完成后你会拿到AppID和AppSecret两个关键凭证。流程上有几点需要提前准备网站域名必须已经完成备案且能正常访问。审核时会校验域名归属。应用需要通过审核才能正式调用登录接口。审核通常以工作日计算所以务必把这一项排到项目早期不要等上线前一天才注册。AppSecret 只显示一次创建后立刻保存到自己的凭证管理系统里。如果泄露了可以在开放平台重置但重置后旧 secret 秒废所有用旧 secret 刷 access_token 的接口瞬间失效。提示不要把 AppSecret 硬编码在前端代码或者 git 仓库里任何能进代码仓库的敏感信息都迟早会出事。至少放到环境变量里有条件就上配置中心或者 KMS 密钥管理。2.2 redirect_uri 的校验逻辑为什么本地调试一直报错微信授权页面需要带上一个redirect_uri参数也就是授权成功后微信服务器回调后端的地址。这个地址不是随便填的微信要求它必须和你在开放平台配置的授权回调域完全匹配协议、域名、端口都不能差。注意微信校验的是域名级别具体路径可以不参与比对但域名必须一致。我本地调试时把回调地址写成http://localhost:8080/wechat/callback但开放平台上配置的是生产域名结果每次授权完都报redirect_uri 参数错误。解决办法有几种思路如果你是本地调试可以在开放平台暂时加一个本地地址。但微信开放平台不接受localhost和 IP 地址只接受已备案的域名。更实用的做法是在服务器上配一个反向代理例如 Nginx把/wechat/callback路径转发到本地开发环境的端口。相当于用生产域名触发微信回调流量再走内网穿透回到本机。如果你用的是内网穿透类工具域名是第三方提供的同样面临备案域名的限制能否用取决于穿透服务是否给你分配已备案域名。反正认准一条线上域名配在开放平台回调地址就一定用线上域名。本地联调时通过代理转发来解决。2.3 微信小程序登录和微信公众号登录的配置区别现在很多项目是 Spring Boot 小程序 的组合小程序登录和公众号网页授权在流程上高度相似但有几个关键参数不一样场景授权入口scope 参数获取用户手机号PC 扫码登录open.weixin.qq.com/connect/qrconnectsnsapi_login不支持需用户手动绑定公众号网页授权open.weixin.qq.com/connect/oauth2/authorizesnsapi_userinfo需认证服务号配合接口小程序登录小程序前端 wx.login无由 code2Session 接口处理需认证小程序配合 getPhoneNumber 按钮小程序登录拿 code 的方式和扫码登录略有不同小程序前端通过wx.login()拿到临时 code直接把它交给你的后端后端再用这个 code 调微信的jscode2session接口换取 openid 和 session_key。不需要跳转授权页不需要配置回调域整体更轻量。如果业务里既有小程序又有公众号建议统一落到一张用户表并把来源字段source作为首位判断维度因为同一微信用户在不同端的 openid 并不相同。3. Spring Boot 工程落地接口设计、依赖配置和完整代码说完了理论和配置这部分直接上工程代码。我以一个标准的 Spring Boot 3 项目为例用 Maven 管理依赖持久层用 MyBatis-Plus登录态用 JWT 签发。因为不同团队的技术栈略有差异我会把核心代码逻辑拆成可复制的片段你只需要在接口返回值等边缘部分做替换。3.1 工程依赖和 application.yml 的初始配置在pom.xml中只需要引入常规 Web 依赖、MyBatis-Plus、JWT 相关依赖以及我们用来调微信接口的 HTTP 客户端。我习惯用 Hutool 的 HttpUtil它对这种简单 GET 请求特别友好一行代码就能完成请求和 JSON 解析。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.25/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency在application.yml里单独建一组自定义配置把微信相关的参数收拢到一起方便后续替换环境。wechat: appid: your-app-id secret: your-app-secret redirect-uri: https://your-domain.com/wechat/callback # 扫码登录时前端组装二维码使用的地址一般不用改 authorize-url: https://open.weixin.qq.com/connect/qrconnect同时建一个配置属性类Spring Boot 会自动把 yml 里的值映射进来Data Component ConfigurationProperties(prefix wechat) public class WechatProperties { private String appid; private String secret; private String redirectUri; private String authorizeUrl; }3.2 生成微信授权二维码的接口扫码登录的二维码其实是前端的工作前端把后端返回的授权 URL 丢给二维码生成组件用户扫出来的是一个跳转微信授权页的链接。所以后端的任务很简单拼一个合规的授权 URL 返回给前端即可。RestController RequestMapping(/wechat) public class WechatAuthController { private final WechatProperties wechatProperties; public WechatAuthController(WechatProperties wechatProperties) { this.wechatProperties wechatProperties; } GetMapping(/qr-code) public RString getQrCodeUrl(RequestParam(required false) String state) { // 官方要求 state 用于防止 CSRF建议每次都随机生成 String stateValue StrUtil.isBlank(state) ? UUID.randomUUID().toString().replace(-, ) : state; String url StrUtil.format({}?appid{}redirect_uri{}response_typecodescopesnsapi_loginstate{}, wechatProperties.getAuthorizeUrl(), wechatProperties.getAppid(), URLEncoder.encode(wechatProperties.getRedirectUri(), StandardCharsets.UTF_8), stateValue); return R.ok(url); } }这里有个细节值得强调微信要求redirect_uri做一次 URL 编码后再放进授权链接所以不能直接拼接必须先用URLEncoder.encode转义。我见过同学漏掉这一步结果授权页直接报参数错误排查了很久才发现是编码问题。3.3 回调接口拿 code 换 access_token 和 openid用户扫码确认后微信会 302 跳转到配置的redirect_uri并携带code和state参数。后端在这个接口里做兑换和登录。GetMapping(/callback) public RLoginResult callback(RequestParam(code) String code, RequestParam(state) String state) { // 1. 校验 state防止 CSRF 攻击 // 这段逻辑建议放在 Redis 里校验见第 4 节 String tokenUrl StrUtil.format( https://api.weixin.qq.com/sns/oauth2/access_token?appid{}secret{}code{}grant_typeauthorization_code, wechatProperties.getAppid(), wechatProperties.getSecret(), code); String result HttpUtil.get(tokenUrl); JSONObject json JSONUtil.parseObj(result); if (json.containsKey(errcode)) { // 记录日志返回统一错误提示 return R.fail(微信授权失败); } String accessToken json.getStr(access_token); String openid json.getStr(openid); // 2. 拉取用户信息 String userInfoUrl StrUtil.format( https://api.weixin.qq.com/sns/userinfo?access_token{}openid{}, accessToken, openid); String userInfo HttpUtil.get(userInfoUrl); JSONObject userJson JSONUtil.parseObj(userInfo); // 3. 业务侧查表如果 openid 不存在则创建新用户 // 4. 签发 JWT 返回给前端 }拉取用户信息这一步目前在普通网站应用下返回的字段较少有些老接口字段如性别、城市已经不可靠或者不再返回完整值。所以我的策略是把 openid 作为身份主键用户资料的缺失项引导用户在前端自行补全而不是完全依赖微信接口。3.4 用户表设计openid 和业务用户如何映射用户表的落地是整个微信登录把微信身份映射到业务身份的关键步骤。我常用的表结构如下涵盖了扫码登录、公众号、小程序三种来源的兼容CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL COMMENT 微信 openid按来源分开存, unionid varchar(64) DEFAULT NULL COMMENT 同一开放平台下唯一, source varchar(20) NOT NULL COMMENT pc_scan / mp / mini_program, nickname varchar(64) DEFAULT NULL, avatar varchar(255) DEFAULT NULL, phone varchar(20) DEFAULT NULL, status tinyint(4) DEFAULT 1, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid_source (openid, source) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT微信登录用户表;openid加上source做联合唯一索引是为了防止同一个用户在小程序端和公众号端产生歧义。如果你在开发中遇到同一微信用户登录后出现两个账号的疑问根因就在这里——不同来源的 openid 不同不做 unionid 关联就会重复建号。登录逻辑我用一个简单的流程图描述根据source openid查用户查到就更新最近登录时间和昵称头像查不到就新建一条用户记录然后签发登录态。3.5 用 JWT 替换微信 token后端不要再向外暴露 access_token微信返回的 access_token 是有时效的通常 2 小时有效而且频繁刷新会影响接口配额。它只适合在回调后端处理的那一瞬间使用不能把它直接返回给前端当登录态。我推荐在回调里换取 openid 之后立刻走自己的授权体系用 JWT 给前端签发一个业务 token。Redis 里存一份 openid 到系统用户 ID 的临时映射关系用于校验也可以直接在 JWT 里携带用户 ID。这样业务接口只需要校验 JWT 签名不再关心微信 token 的状态。private String generateJwt(Long userId, String openid) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(openid, openid) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000)) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); }业务接口里配合 Spring Security 或者拦截器统一解析 JWT就能在任何 Controller 里快速拿到当前用户身份。这个模式对所有第三方登录GitHub、钉钉、谷歌都是通用的以后接入别的 OAuth 登录只需要换一个回调实现类。4. 边界情况与踩坑记录我实测过的六种经典翻车现场微信登录的坑通常不在主流程上而在各种边界情况里。我和团队在压测和线上问题复盘里遇到了不少奇葩问题下面挑六个最典型的分享。4.1 state 参数的校验与 CSRF 防护共享 WiFi 都救不了的回调劫持state参数从授权地址发出去到回调过程原样返回它存在意义是防止攻击者伪造授权请求。如果完全不做校验攻击者可以诱导用户点击构造好的微信授权链接然后窃取用户的登录态——用户本人可能完全不知情。我见过很多 demo 代码里state是一个写死的字符串或者直接不传这在生产环境是安全隐患。正确做法是在生成授权二维码时把随机 state 存到 Redis 或者 Cookie 里回调接口再取出来比对。GetMapping(/qr-code) public RString getQrCodeUrl() { String state UUID.randomUUID().toString().replace(-, ); redis.set(wechat:state: state, 1, Duration.ofMinutes(5)); // 拼 URL... }回调接口里核对时如果 state 不存在或者不一致果断拒绝请求。4.2 同一微信用户在不同应用下 openid 不同这个前面也提过这里用一个真实场景再强调。我们有个客户同时拥有公众号商城和 PC 官网用户在公众号里下单后到官网想查订单发现要重新注册一遍——因为两个平台的 openid 是不同的。如果不记录 unionid数据完全没法合并。解决方案只能是绑定微信开放平台将公众号和小程序、网站应用都挂到同一开放平台账号下用户授权后拿到 unionid用 unionid 关联业务账号。接口返回的unionid在同一开放平台下所有应用中是唯一的这才是跨端打通的唯一钥匙。4.3 access_token 直接换取用户资料失败权限收窄问题微信在隐私保护政策上持续收紧。早年间snsapi_userinfo授权能拿到用户昵称、头像、性别、城市后来很多新创建的应用只能拿到 openid部分资料字段需要用户主动授权或另行调用接口。我测试过新建的网站应用调用/sns/userinfo时返回的字段已经明显减少性别城市基本拿不到。这块不要写死兼容逻辑关键要有一个降级方案。比如把获取用户资料失败包装成一个可容忍的警告而不是把整个登录流程彻底卡死。用户首次登录后再让他在自己的资料页手动补充比在登录环节强迫用户完成所有信息采集要顺利得多。4.4 并发回调与重复绑定问题微信的回调虽然是用户主动触发的但极端情况下同一用户可能连续多次扫码、重复点击授权导致后端同一 openid 的创建请求并发打进来。如果只在业务层做了查不到则插入没有数据库唯一索引兜底就会插入两条甚至多条用户记录。我的解决套路是数据库层加uk_openid_source唯一索引这层兜底最重要。业务层用先查后插之外捕获重复键异常然后回查一圈返回已有用户。如果并发量真的高可以把 openid 作为 Redis 锁的 key登录流程串行化。4.5 公众号网页授权的实际调试限制公众号网页授权会涉及一个snsapi_base和snsapi_userinfo两个 scope 的选择。如果只是登录不需要用户资料用snsapi_base就能拿 openid静默授权用户没有任何感知如果要展现昵称头像必须用snsapi_userinfo用户会看到明确的授权弹窗。需要留意的是测试公众号和认证服务号在授权能力上有差别。个人主体订阅号没有网页授权接口权限这一步会直接影响功能是否能上线。做公众号 H5 项目之前先确认你的公众号类型避免开发完才发现接口调用被拒。4.6 回调接口要加日志和幂等设计微信回调接口不像自研接口它由外部服务器直接触发一旦用户重复扫码或者微信重试机制触发同一个 code 可能被请求多次。微信明确说明一个 code 只能使用一次再用会返回40029之类的错误码。所以代码里要有对code 已使用的容错处理。我的做法是回调接口进来先查 Rediswechat:code:{code}是否存在不存在就正常处理并写入一个 5 分钟过期标记已存在则直接返回上一次的结果。这个幂等设计看起来小在弱网环境下能挡住不少重复通知导致的数据错乱。5. 生产环境部署的进阶思考结构设计和跨端统一功能跑通只是第一步生产环境里的工程化改造才是拉开团队之间差距的地方。这块我聊三个方向模块化封装、日志监控、多端账号打通。5.1 把微信登录封装成独立模块或 Starter如果公司里有多个 Spring Boot 项目都要接微信登录重复 Controller、Service 代码会很痛苦。我们后来把整个微信登录提取成了独立模块wx-auth-spring-boot-starter把 AppId、Secret、回调地址全部交给使用方通过配置注入Controller 和 Service 做成自动装配。下游项目只需要引入依赖、填好配置就能获得一套可用回调接口。模块化边界划分上要单独抽取WechatOauthStrategy接口不同场景分别实现public interface WechatOauthStrategy { String getAuthorizeUrl(String state); WechatUserInfo handleCallback(String code); }扫码登录、公众号授权、小程序 code2Session 各写一个实现类通过策略路由到对应的处理器。这样以后微信接口升级只需要替换单个实现类核心业务代码完全不用动。5.2 回调接口的监控和日志规范每一次微信回调都应该至少在日志里记录这些字段openid、来源、code、耗时、是否命中已有用户、返回错误码。我在线上排查过一个偶发登录卡顿的问题查日志发现瓶颈不是微信接口慢而是我们业务自己的用户查询没有加索引微信响应才 80ms我们却花了 400ms 查库。强烈建议针对wechat.callback和wechat.qrcode两个接口埋点统计响应时间和异常率。一旦微信侧接口临时故障或者 AppSecret 被重置你能在监控图上第一时间察觉而不是等用户投诉。5.3 多端登录的统一方案从 openid 到用户身份中心负责过中大型项目的同学应该都有感触用户可能在 PC 扫码、小程序、公众号 H5 三个端登录业务希望他们在三个端共享同一份订单、会员等级、积分。这时候sys_user单表已经不够用需要抽象出一个用户中心服务把微信身份和业务主账号分开存储。设计上粗粒度分为两张表主账号表存储用户的核心业务数据用户 ID 是唯一主键。第三方身份映射表存储 openid、unionid、source指向主账号表 ID。登录时通过 third-party identity 找到主账号 ID没有则先建主账号再建映射。这套设计对将来扩展其他登录方式手机号验证码、支付宝、Apple 登录非常友好每个登录方式都只是在映射表里多一行记录不会把业务用户表搞得越来越臃肿。从我个人经验看Spring Boot 接微信登录真的不算大工程真正花时间的地方全在开放平台配置、边界情况处理和工程化封装上。如果你正在开发新项目建议先把用户表设计和开放平台审核走起来这两件事是可以和前端并行推进的会帮你节省整个项目周期里相当多平静的时间。