1. 项目概述:从扫码到登录的最后一公里
做微信生态开发,扫码登录几乎是绕不开的标配功能。上一篇文章我们聊了如何生成那个带参数的二维码,引导用户扫码授权。但那个二维码被用户用微信“扫一扫”之后,到底发生了什么?服务器怎么知道用户同意了?又怎么拿到用户的昵称和头像?这整个过程,就是OAuth2.0授权流程中最为核心也最容易出错的“授权回调”与“用户信息获取”环节。很多开发者的项目卡在这里,不是回调地址收不到通知,就是换access_token时各种报错,最后只能对着文档干瞪眼。
今天,我们就来彻底打通这“最后一公里”。我会以一个真实的Spring Boot后端项目为例,带你完整走一遍从用户扫码同意,到服务器接收授权码,再到用授权码换令牌,最后用令牌换用户信息的全流程。这不是一个简单的API调用演示,我会重点拆解其中每个环节的“坑点”和“心法”,比如回调地址的配置玄学、授权码的安全处理、令牌的缓存策略,以及用户信息解密这些文档里不会细说的实战细节。无论你是正在集成此功能,还是想深入理解OAuth2.0的交互逻辑,这篇内容都能让你避开我当年踩过的那些坑。
2. 核心流程与OAuth2.0角色再认知
在动手写代码之前,我们必须把微信OAuth2.0在这个场景下的几个角色和两次关键跳转理清楚。这能帮你建立正确的“世界观”,写代码时才知道每一步在干什么。
2.1 流程全景图与关键跳转
整个扫码登录的OAuth2.0流程,可以概括为两次重要的“跳转”:
- 第一次跳转(前端->微信):用户扫描二维码,实质是访问了一个微信的授权页面。这个页面的URL我们已经构造好并嵌入二维码中。用户在此页面点击“同意”按钮。
- 第二次跳转(微信->我们的后端):用户点击同意后,微信授权服务器会携带一个临时的
code(授权码),跳转回我们预先在微信开放平台配置好的redirect_uri(回调地址)。这是整个流程的触发器。 - 后端核心处理(我们的服务器):我们的后端服务在
redirect_uri对应的接口里,接收到微信传来的code。然后用这个code,加上我们的AppID和AppSecret,去微信服务器换取一个access_token(访问令牌)和openid。最后,再用这个access_token和openid去获取用户的基本信息。
这里务必明确OAuth2.0中的四个角色在我们场景下的具体指代:
- 资源所有者 (Resource Owner):就是我们的终端用户,他拥有自己的微信头像、昵称等资源。
- 客户端 (Client):在这里特指我们的后端服务器应用。很多人会误以为是前端网页,其实在“授权码模式”下,前端(浏览器)只是一个引导者和授权码的传递者,真正拿着
code去换token、拿着token去换资源的关键安全操作,都必须由后端完成。因为AppSecret绝不能暴露给前端。 - 授权服务器 (Authorization Server):微信的开放平台授权服务器。
- 资源服务器 (Resource Server):存储用户基本信息的微信服务器。
理解“客户端是我们的后端”这一点至关重要,它决定了代码的编写位置和安全边界。
2.2 回调地址(redirect_uri)的配置玄学与最佳实践
回调地址是微信找到你家的“门牌号”,配置错了,一切白搭。这里面的坑最多。
1. 配置位置与格式要求:你必须在微信开放平台的“开发-接口权限-网页授权获取用户基本信息”里,点击“修改”,填写你的授权回调域名。注意,这里填的是域名,不是完整URL!例如,你的回调接口是https://api.yourdomain.com/wx/callback,那么这里只填api.yourdomain.com(如果你用www.yourdomain.com访问,则填后者)。微信会校验,发起跳转的域名必须与此处配置的域名严格一致(包括二级域名)。
2. 常见坑点解析:
- 端口问题:微信默认只支持80(HTTP)和443(HTTPS)端口。如果你在本地开发用
http://localhost:8080/callback,微信是无法回调的,因为域名对不上且可能包含端口。本地开发时,通常需要借助内网穿透工具(如ngrok、localtunnel)生成一个临时的HTTPS域名进行测试。 - 编码问题:
redirect_uri参数在构造授权URL时必须进行URLEncode。这是一个高频错误。如果你直接拼接redirect_uri=https://api.xx.com/callback,一旦回调地址中包含?、&等字符,整个URL就会被解析错误。一定要用URLEncoder.encode(redirectUri, "UTF-8")。 - 测试号与正式号:微信公众平台测试号也有回调域名配置,但限制更少,适合前期开发调试。正式号修改回调域名后,可能需要一段时间(几分钟)生效。
3. 我的实战建议:
- 在应用配置中,将回调地址作为一个可配置的项,例如放在
application.yml里:wx.callback-url: https://api.yourdomain.com/wx/callback。 - 在构造授权URL的方法里,强制对该配置值进行URL编码,养成习惯。
- 对于需要区分环境的项目(开发、测试、生产),准备多套配置,并通过Spring Profile来切换。
注意:很多开发者在本地调试时,喜欢用IP地址(如
http://192.168.1.100:8080)。这在微信回调场景下是行不通的,微信服务器无法将你的内网IP与一个可信的域名关联起来。内网穿透是本地开发调试扫码登录的必备技能。
3. 后端核心实现:接收授权码与交换令牌
现在,我们来到后端服务器的核心战场。这里将创建两个关键的Controller接口。
3.1 设计回调接口(Callback Endpoint)
这个接口就是微信服务器跳转回来的目标,它的路径必须与你配置的redirect_uri匹配。
@RestController @RequestMapping("/wx") @Slf4j public class WxLoginController { @Value("${wx.app-id}") private String appId; @Value("${wx.app-secret}") private String appSecret; /** * 微信授权回调接口 * @param code 微信回调时携带的授权码 * @param state 我们之前生成二维码时传入的随机状态参数,用于防CSRF攻击 * @return 通常跳转到前端登录成功页或携带信息给前端 */ @GetMapping("/callback") public String callback(@RequestParam("code") String code, @RequestParam(value = "state", required = false) String state) { log.info("收到微信授权回调,code: {}, state: {}", code, state); // 1. 校验state(重要!) if (!validateState(state)) { log.error("State校验失败,可能为CSRF攻击。state: {}", state); return "redirect:/error?msg=invalid_state"; } // 2. 用code换取access_token和openid String tokenUrl = String.format("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code", appId, appSecret, code); // 使用RestTemplate或HttpClient等工具发起GET请求 ResponseEntity<String> tokenResponse = restTemplate.getForEntity(tokenUrl, String.class); // ... 解析响应JSON,获取access_token和openid // 3. 用access_token和openid获取用户信息 // 4. 处理用户信息(登录/注册逻辑) // 5. 创建会话(如生成JWT或设置Session),并重定向到前端 return "redirect:/login-success?token=生成的本地令牌"; } }关键点解析:
state参数校验:这个参数在生成二维码时我们随机生成并存入Session或Redis,在回调时进行比对。这是防止CSRF(跨站请求伪造)攻击的关键一步。如果校验失败,应立即终止流程。code的使用:授权码code具有一次性且极短的有效期(通常约5分钟)。它只能用于换取一次access_token,用过后立即失效。所以你的代码必须保证,即使这个接口被意外刷新或重复调用,用同一个code换token的操作也要有幂等性处理(比如换到的token先缓存,第二次请求直接使用缓存)。- 错误处理:微信接口返回的错误码需要妥善处理。例如,
code无效、code已被使用等,都应给用户明确的反馈。
3.2 调用微信API换取访问令牌
上面代码中的第2步,我们构造了一个URL去请求https://api.weixin.qq.com/sns/oauth2/access_token。让我们深入看一下这个请求和响应。
请求参数:
appid: 你的应用唯一标识。secret: 你的应用密钥。这是整个流程中最敏感的信息,必须保存在后端,严禁出现在前端代码、URL或日志中。code: 上一步回调收到的授权码。grant_type: 固定为authorization_code。
成功响应示例(JSON):
{ "access_token": "ACCESS_TOKEN", "expires_in": 7200, "refresh_token": "REFRESH_TOKEN", "openid": "OPENID", "scope": "snsapi_login", "unionid": "UNIONID" // 如果存在且应用具备相应权限 }字段解读与处理策略:
access_token: 接口调用凭证,有效期2小时(expires_in秒)。你需要用它来获取用户信息。建议将其与对应的openid一起缓存起来(如存入Redis,设置过期时间略小于7200秒),避免在短时间内多次获取用户信息时重复请求微信服务器。refresh_token: 用于刷新access_token的凭证,有效期30天。如果你的应用需要长期维护与用户的关联(比如定期同步头像),则需要保存此refresh_token,并在access_token过期后用它来刷新。对于扫码登录即走的场景,可能用不到。openid: 用户在当前微信开放平台应用下的唯一标识。这是你识别用户的核心ID,应该与你业务系统的用户账号绑定。unionid: 如果多个应用(同一开放平台账号下的公众号、小程序、App等)需要打通用户体系,那么unionid就是同一个用户在平台下的唯一标识。如果响应里有unionid,优先使用它作为用户唯一标识进行绑定,比openid更通用。
代码实现建议:为这个HTTP请求和响应解析封装一个独立的方法或服务类。
@Service public class WxAuthService { @Value("${wx.app-id}") private String appId; @Value("${wx.app-secret}") private String appSecret; @Autowired private RestTemplate restTemplate; @Autowired private RedisTemplate<String, String> redisTemplate; /** * 使用code换取access_token */ public AccessTokenResponse getAccessToken(String code) throws WxAuthException { String url = String.format("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code", appId, appSecret, code); String response = restTemplate.getForObject(url, String.class); JSONObject json = JSON.parseObject(response); // 错误处理 if (json.containsKey("errcode")) { int errCode = json.getIntValue("errcode"); String errMsg = json.getString("errmsg"); log.error("换取access_token失败,code: {}, errcode: {}, errmsg: {}", code, errCode, errMsg); throw new WxAuthException(errCode, errMsg); } AccessTokenResponse tokenResp = new AccessTokenResponse(); tokenResp.setAccessToken(json.getString("access_token")); tokenResp.setExpiresIn(json.getIntValue("expires_in")); tokenResp.setRefreshToken(json.getString("refresh_token")); tokenResp.setOpenid(json.getString("openid")); tokenResp.setScope(json.getString("scope")); tokenResp.setUnionid(json.getString("unionid")); // 缓存token: key可以是 "wx:token:" + openid String cacheKey = "wx:token:" + tokenResp.getOpenid(); redisTemplate.opsForValue().set(cacheKey, tokenResp.getAccessToken(), tokenResp.getExpiresIn() - 300, TimeUnit.SECONDS); // 提前5分钟过期 return tokenResp; } }4. 获取并处理用户基本信息
拿到access_token和openid后,我们就可以向微信的资源服务器请求用户的基本信息了。
4.1 调用用户信息接口
请求的API是https://api.weixin.qq.com/sns/userinfo。
请求参数:
access_token: 上一步获取到的接口调用凭证。openid: 用户的标识。lang: 返回国家地区语言版本,zh_CN简体,zh_TW繁体,en英语。
成功响应示例(JSON):
{ "openid": "OPENID", "nickname": "NICKNAME", "sex": 1, "province": "PROVINCE", "city": "CITY", "country": "COUNTRY", "headimgurl": "http://thirdwx.qlogo.cn/mmopen/...", "privilege": [], "unionid": "UNIONID" }代码实现:
public WxUserInfo getUserInfo(String accessToken, String openid) throws WxAuthException { String url = String.format("https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN", accessToken, openid); String response = restTemplate.getForObject(url, String.class); JSONObject json = JSON.parseObject(response); if (json.containsKey("errcode")) { // 处理错误,例如access_token过期 throw new WxAuthException(json.getIntValue("errcode"), json.getString("errmsg")); } WxUserInfo userInfo = new WxUserInfo(); userInfo.setOpenid(json.getString("openid")); userInfo.setNickname(json.getString("nickname")); userInfo.setSex(json.getIntValue("sex")); userInfo.setProvince(json.getString("province")); userInfo.setCity(json.getString("city")); userInfo.setCountry(json.getString("country")); userInfo.setHeadimgurl(json.getString("headimgurl")); userInfo.setUnionid(json.getString("unionid")); // 注意:privilege字段是个数组 userInfo.setPrivilege(json.getJSONArray("privilege").toJavaList(String.class)); return userInfo; }4.2 用户信息的安全处理与业务绑定
拿到用户信息后,才是我们业务逻辑的开始。这里有几个核心步骤:
1. 信息解码:微信返回的nickname字段可能是经过编码的,特别是包含Emoji表情时。为了正确存储到数据库(如MySQL的utf8mb4字符集),你可能需要进行转码处理。
// 使用Apache Commons Lang或类似工具 import org.apache.commons.text.StringEscapeUtils; String decodedNickname = StringEscapeUtils.unescapeHtml4(wxUserInfo.getNickname());2. 头像URL处理:headimgurl是一个指向微信CDN的链接。这个链接本身是有效的,但如果你担心长期稳定性,或者想加速访问,可以考虑将头像下载到你自己的服务器或对象存储(如OSS、COS)上,并替换为你自己的URL。这是一个可选的优化步骤。
3. 核心业务逻辑:用户绑定/登录这是扫码登录的最终目的。通常有两种情况:
- 新用户:系统中不存在此
unionid或openid对应的用户。此时,你需要创建一个新的业务系统用户,并将微信的unionid/openid、昵称、头像等信息与之绑定。然后,为用户创建系统内的登录态(如Session、JWT Token)。 - 老用户:系统中已存在绑定的用户。直接根据
unionid/openid找到对应的用户,更新其昵称、头像(如果需要),然后创建登录态。
我的实战心得:绑定策略选择
- 优先使用
unionid绑定:只要微信返回了unionid,就用它作为绑定的唯一标识。这为你的应用未来接入同一开放平台下的其他应用(如小程序)铺平了道路,可以实现用户体系互通。 - 建立独立的绑定表:不建议直接把微信的
openid或unionid作为业务用户表的主键或唯一用户名。更好的做法是创建一个user_wx_auth这样的关联表,字段包括id,user_id(关联你的业务用户表),unionid,openid,app_id(来自哪个微信应用),create_time等。这样设计更灵活,一个业务用户可以绑定多个第三方平台账号。 - 登录态保持:生成系统登录态后,如何传递给前端?在回调接口的最后,我们通常有两种方式:
- 重定向到前端页面并携带Token:如
return "redirect:https://frontend.yourdomain.com/login-success?token=" + jwtToken。注意Token需要放在URL中,可能存在安全风险(如被浏览器历史记录),可以考虑缩短Token有效期,或使用一次性Code让前端再换Token。 - 设置Cookie:如果前后端同域,可以直接在回调接口设置一个HttpOnly的Cookie。但跨域场景下较复杂。
- 实践推荐:对于现代前后端分离架构,我更喜欢让回调接口渲染一个极简的HTML页面,该页面通过
window.opener.postMessage将登录成功的信息(或Token)发送给打开扫码窗口的父页面,然后自行关闭。这样体验最流畅,且安全可控。
- 重定向到前端页面并携带Token:如
5. 异常处理、安全考量与性能优化
一个健壮的扫码登录模块,必须妥善处理各种异常,并考虑安全和性能。
5.1 常见异常码与处理策略
微信接口返回的错误码需要被优雅处理。以下是一些常见的:
| 错误码 | 错误描述 | 可能原因与处理策略 |
|---|---|---|
| 40029 | code无效 | code已被使用、过期或伪造。应引导用户重新扫码。 |
| 40163 | code已被使用 | 同上。确保你的服务对同一个code的换token请求是幂等的。 |
| 40001 | 获取access_token时AppSecret错误 | 检查后台配置的AppSecret是否正确,或是否已重置。 |
| 42001 | access_token过期 | 缓存的access_token已失效。应尝试使用refresh_token刷新,或让用户重新授权。 |
| 41008 | 缺少code参数 | 回调接口未收到code,检查授权URL构造和微信回调是否正常。 |
| 43002 | 需要GET请求 | 用户信息接口要求GET,误用POST。 |
| 48001 | api功能未授权 | 未在开放平台正确配置“网页授权获取用户基本信息”权限。 |
在你的代码中,应该定义一个统一的业务异常(如WxAuthException),并在Service层抛出,在Controller层或全局异常处理器中捕获,根据错误码转换为对用户友好的提示信息或进行重试等操作。
5.2 安全加固要点
- State参数防CSRF:必须生成随机的
state并验证,这是OAuth2.0协议的标准安全要求。 - AppSecret保护:这是你应用的“根密码”。必须使用环境变量、配置中心或密钥管理服务来存储,绝不能写在代码里或提交到版本库。
- Token缓存与隔离:缓存的
access_token应以openid为键进行隔离,避免混淆。缓存时间应略短于微信返回的expires_in。 - 回调接口防重放:虽然
code一次性有效,但恶意攻击者可能重复发起带有相同code的回调请求。你的回调接口应具备防重放能力,例如在成功换取access_token后,在极短时间内(如5秒)将code标记为已使用,后续相同code的请求直接拒绝。 - 用户信息校验:虽然信息来自微信,但在绑定到你的系统前,仍可做基本的合法性校验(如昵称长度、头像URL格式)。
5.3 性能优化建议
- 缓存、缓存、缓存:
access_token务必缓存,避免每次获取用户信息都重新换取。- 用户基本信息(特别是头像URL、昵称)在短时间内也可以缓存,减少对微信接口的调用。注意设置合理的过期时间(如30分钟)。
- 异步处理:对于下载用户头像到自有存储这类耗时操作,不要阻塞主要的登录流程。可以在登录成功后,通过消息队列异步执行头像下载和更新任务。
- 连接池与超时设置:调用微信API的HTTP客户端(如RestTemplate、OkHttp)应配置连接池和合理的连接、读写超时时间(如连接超时3秒,读写超时5秒),避免因网络波动导致线程长时间阻塞。
- 降级与熔断:在极端情况下(如微信接口不稳定),你的登录功能应该有所准备。例如,如果获取用户详细信息失败,但
openid是有效的,可以允许用户以一个“微信用户”的默认身份登录,待服务恢复后再异步补全信息。
6. 前端闭环与体验优化
后端流程走通后,我们需要让前端页面形成一个流畅的闭环。
6.1 前端如何感知登录成功
这是前后端配合的关键点。我们的回调接口(/wx/callback)处理完所有逻辑后,最终需要通知前端登录页面:“嘿,用户登录成功了,这是他的令牌”。
方案一:重定向携带Token(简单直接,适用于传统架构)回调接口最后执行:return "redirect:https://your-frontend.com/#/login-success?token=" + jwtToken。前端在login-success页面解析URL中的token,存入本地存储(如localStorage),并跳转到首页。
缺点:Token暴露在URL和浏览器历史记录中,安全性较低。适合内部系统或Token有效期极短的场景。
方案二:后端渲染页面传递消息(推荐用于SPA)回调接口渲染一个简单的HTML页面。
@GetMapping("/callback") public String callback(..., HttpServletResponse response) { // ... 处理逻辑,生成token String token = generateToken(user); // 将token放入缓存,关联一个一次性key String authCode = UUID.randomUUID().toString(); redisTemplate.opsForValue().set("auth:code:" + authCode, token, 5, TimeUnit.MINUTES); // 重定向到一个专门的中转页面,并传递一次性code return "redirect:/wx/auth-success?code=" + authCode; } @GetMapping("/auth-success") public String authSuccessPage(@RequestParam("code") String authCode, Model model) { model.addAttribute("authCode", authCode); return "wx-auth-success"; // 这是一个Thymeleaf/FreeMarker模板视图 }对应的wx-auth-success.html模板:
<!DOCTYPE html> <html> <head><title>登录成功</title></head> <body> <script> // 从URL参数获取一次性code const urlParams = new URLSearchParams(window.location.search); const authCode = urlParams.get('code'); if (authCode && window.opener) { // 向打开扫码窗口的父页面发送消息 window.opener.postMessage({ type: 'WX_LOGIN_SUCCESS', code: authCode }, 'https://your-frontend.com'); // 指定目标Origin,增强安全 window.close(); // 关闭当前窗口 } else { // 备用方案:显示成功提示,引导用户手动关闭 document.body.innerHTML = '<h3>微信登录成功!请关闭此窗口。</h3>'; } </script> </body> </html>前端登录页面需要监听这个message事件:
// 在打开扫码窗口的页面 window.addEventListener('message', function(event) { // 验证消息来源是否是你的后端域名,防止恶意网站攻击 if (event.origin !== 'https://your-backend.com') return; if (event.data && event.data.type === 'WX_LOGIN_SUCCESS') { const authCode = event.data.code; // 调用后端接口,用这个一次性code换取真正的token fetch(`/api/auth/exchange-token?code=${authCode}`) .then(res => res.json()) .then(data => { localStorage.setItem('user_token', data.token); // 跳转到首页或更新用户状态 window.location.href = '/'; }); } }, false);这个方案安全性更高,Token不经过URL传递,且使用一次性code进行交换。
6.2 处理用户拒绝授权或扫码超时
不是所有用户都会点击“同意”。我们需要处理这些边缘情况。
- 用户拒绝授权:微信在跳转回调地址时,会携带
error参数,例如error=access_denied&error_description=user_denied。你的回调接口需要检查这些参数,并跳转到一个友好的提示页面,告知用户登录失败,引导其重新尝试或使用其他登录方式。 - 二维码过期:如果用户扫描了一个过期的二维码,微信会提示“二维码已失效”。这个处理主要在前端,二维码生成时应设置一个有效期(如5分钟),并定时检查状态。后端在生成二维码时记录生成时间,当收到一个过期二维码对应的
state时,可以直接返回错误。
6.3 移动端H5适配要点
如果你的登录页面需要在微信内置浏览器或手机浏览器中打开,流程基本一致,但有一些细节:
- 回调地址:必须是在微信开放平台配置的域名下的地址。
- 页面布局:手机屏幕较小,二维码和提示信息需要做响应式适配。
- 自动跳转:在微信内,扫码授权后可能会自动跳转,体验更流畅。确保你的回调页面能正确处理。
- 返回按钮:在H5中,用户可能使用物理返回键。需要合理设计登录成功后的页面跳转逻辑,避免用户点返回又看到登录页的死循环。
7. 实战问题排查与调试技巧
即使流程清晰,实战中还是会遇到各种“妖魔鬼怪”。这里分享几个我踩过的坑和调试方法。
问题一:回调接口收不到code和state。
- 检查点1:确保微信开放平台配置的回调域名精确无误,没有
http://或https://前缀,没有尾随斜杠。 - 检查点2:本地开发时,必须使用内网穿透工具(如ngrok、cpolar)提供的HTTPS域名进行测试,并将此域名配置到测试号或正式号的回调域名中。
- 检查点3:在服务器上,检查Nginx/Apache等反向代理配置,确保请求能正确路由到你的后端应用,并且没有丢失URL参数。
- 调试方法:在回调接口的第一行打印所有请求参数和头部信息,或者使用抓包工具(如Charles、Fiddler)拦截微信服务器的回调请求,查看原始请求内容。
问题二:换取access_token时返回40029(code无效)。
- 检查点1:确认
code是否被重复使用。确保你的回调接口逻辑是幂等的。 - 检查点2:
code的有效期非常短(约5分钟)。检查用户扫码到回调处理的时间是否过长。 - 检查点3:确认你用来换取
token的AppID和AppSecret,与生成二维码时使用的AppID是同一个应用。跨应用使用code是无效的。 - 调试方法:记录下每次收到的
code和换取token的结果。如果同一个code被多次尝试换取,第二次必然失败。
问题三:获取用户信息返回48001(api未授权)。
- 检查点:登录微信开放平台,在“管理中心-应用详情-接口权限”中,查看“网页授权获取用户基本信息”权限是否已经获取(而不仅仅是“已获得”)。对于新应用或修改过配置,可能需要点击“确认”或“修改”来激活。
- 注意:测试号需要在测试号管理页面单独配置“网页授权获取用户基本信息”的域名。
问题四:用户昵称或头像显示乱码。
- 检查点1:数据库字符集。MySQL需要设置为
utf8mb4字符集和utf8mb4_unicode_ci排序规则,以支持完整的UTF-8字符(包括Emoji)。 - 检查点2:Java应用连接数据库的URL中,需要指定字符集,如
jdbc:mysql://...?useUnicode=true&characterEncoding=utf8&useSSL=false。 - 检查点3:在接收到微信返回的JSON后,对
nickname字段进行正确的解码处理,如前文提到的StringEscapeUtils.unescapeHtml4。
通用调试建议:
- 日志详尽:在关键节点(收到回调、换取token、获取用户信息、绑定用户)打印详细的INFO日志,包括关键ID(
code,state,openid)和结果摘要。错误处打印ERROR日志,并带上完整的上下文信息。 - 使用微信的在线调试工具:微信官方提供了接口调试工具,可以手动输入参数测试
access_token和userinfo接口,这能帮你快速判断是参数问题还是代码逻辑问题。 - 分步测试:不要一次性写完所有代码。可以先写一个最简单的回调接口,只打印参数,确保能收到微信回调。然后再逐步加上换token、取信息的逻辑。