ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信登录三场景详解:从OAuth2到C#后端排错指南

微信登录三场景详解:从OAuth2到C#后端排错指南 很多人找我调微信登录的 bug 时第一句话往往是“登录不好使”。等我远程看一眼代码十有八九是把网页扫码登录和小程序登录的逻辑搅在一起了。微信登录这个功能看起来就是一个授权弹窗背后其实是好几条完全独立的链路网页扫码、小程序内授权、移动 App 拉起、公众号内跳转。如果一开始没把这几个概念拆开后面几乎每个问题都会跟着跑偏。这篇文章是我自己整理微信登录全流程的学习笔记也包含这些年踩过的真实排错记录给正在写后端、前端和移动端登录的同学一个能直接对照排查的思路。1. 微信登录不是一套流程而是三套流程先说结论微信登录至少有三种主流场景分别是网页扫码登录、小程序登录、移动 App 登录。它们都基于 OAuth2 思想但参数、回调方式、接口路径完全不同混着用最容易出事。1.1 网页扫码登录标准的 OAuth2 授权码模式网页扫码登录的场景是“桌面上有一个二维码手机扫一下网页自动登录”。它的通信链路就是 OAuth2 的授权码模式网页加载一个授权二维码。用户用微信扫码手机上出现“确认登录”页面。用户点确认微信服务器把浏览器重定向到开发者配置的redirect_uri并带上code。后端拿到这个code再去微信服务器换access_token和用户信息。整个过程中二维码本身不重要它只是在手机上“触发一次授权点击”真正的身份交换在后端完成。这是理解整套流程的关键。1.2 小程序登录静默登录与 code2Session小程序没有浏览器地址栏也没办法像网页那样做重定向所以微信给了另一套 API。通过wx.login()拿到一个临时code然后后端拿着 code 去调jscode2session接口换取openid和session_key。这个过程没有“授权弹窗”用户进入小程序的时候后端已经能识别出他是谁了。如果还需要头像昵称和手机号那才需要额外的授权组件。很多把网页扫码登录习惯带过来的同事会在小程序里到处找回调地址绕了半天其实方向就错了。1.3 移动 App 登录由系统唤醒进入的统一登录App 里的微信登录走的是微信开放平台提供的 SDK也叫“拉起微信授权”。App 注册好 Universal Links 或 URL Scheme 后点击登录按钮会直接跳到微信授权完跳回 App。流程同样是授权码模式只不过跳转载体从浏览器变成了 App。三种场景的差别我整理了个表场景典型用户操作核心接口用户标识网页扫码手机扫码确认sns/oauth2/access_tokenopenid unionid小程序登录自动完成sns/jscode2sessionopenid session_key移动 App跳微信授权再跳回sns/oauth2/access_tokenopenid unionid理解了这三条分支后面所有代码都不会错得太离谱。2. 网页扫码登录的完整闭环从二维码到用户资料网页扫码登录是最常被问到的一类我把它拆成前后端两部分来讲。2.1 前置条件开放平台账号与回调域名的绑定规则网页微信登录必须在微信开放平台注册“网站应用”拿到独立的AppID和AppSecret。这里要注意开放平台的应用和小程序的 AppID 是两个体系别拿小程序 AppID 去调扫码登录接口。开放平台后台会让你填写“授权回调域名”。这个域名必须和你实际跳转的redirect_uri域名完全一致不然微信直接报“redirect_uri参数错误”。它不校验路径只校验域名意味着你可以配https://example.com回调地址写到https://example.com/auth/wx/callback都能过。但如果前端把回调地址写成了http://example.com协议不一致照样被拒。2.2 拼授权链接最容易错的是 redirect_uri 编码网页端生成二维码有两种做法一种是直接放一个iframe里面加载微信的授权链接另一种是自己用后端生成一个二维码图片指向授权链接。最终都要落到下面这个地址https://open.weixin.qq.com/connect/qrconnect?appidAPPIDredirect_uriREDIRECT_URIresponse_typecodescopesnsapi_loginstateSTATE#wechat_redirect这里的redirect_uri必须经过 URL 编码。比如原始地址是https://example.com/auth/wx/callback编码完应该长这样https%3A%2F%2Fexample.com%2Fauth%2Fwx%2Fcallback我见过好几次线上报错原因就是前端在后端拼了一个未编码的地址或者用encodeURI把整个链接都编码了连appid和scope也一起转义微信自然解析不出来。正确做法是只编码redirect_uri这一项的值其余参数保持原样。2.3 后端换取 access_token 并清除一次性 code用户扫码确认后微信会跳转到你的回调地址形如https://example.com/auth/wx/callback?codeXXXstateYYY后端要做的第一件事不是读用户信息而是拿这个code去换access_token。接口是https://api.weixin.qq.com/sns/oauth2/access_token?appidAPPIDsecretSECRETcodeCODEgrant_typeauthorization_code注意code是一次性的用过就废。如果同一份 code 被发两次请求微信会返回40029之类错误。所以在网关层面最好直接对 code 做幂等处理不要让并发请求把同一个 code 消费两次。顺手说一句state参数它本来是用来防 CSRF 的。前端生成一个随机串传给微信微信原样带回后端需要校验这个串和自己发出去的一致。很多人不校验 state这等于给账号劫持留了一个口子。2.4 用户信息与 openid、unionid 的关系换到access_token之后还要再请求一次用户信息接口https://api.weixin.qq.com/sns/userinfo?access_tokenACCESS_TOKENopenidOPENID返回的字段包括openid、nickname、headimgurl等。openid是你在当前这个网站应用下的唯一 ID如果同一个用户换了一个公众号或小程序他的openid会变。如果多个应用挂在同一个开放平台账号下微信会返回unionid这个才是真正的“一号通”。做多端系统时数据库里建议把openid和unionid都存下来unionid作为账号唯一的关联键openid作为当前端的快速查索引。3. C# 服务端如何接住微信回调代码级拆解“C# 使用微信登录验证”是很多人搜过来的关键词这里给一套我实际用过的 C# 后端处理逻辑。环境是 ASP.NET Core核心思路其实和别的语言一样但 C# 里有些序列化和 HttpClient 的坑值得单独说一下。3.1 配置文件和基础实体的写法我一般会把微信配置单独放到一个类里避免在 Controller 里散落字符串。配置如下public class WxConfig { public string AppId { get; set; } public string AppSecret { get; set; } public string RedirectUri { get; set; } }再定义两个用来接收微信返回结果的实体public class AccessTokenResult { public string Access_token { get; set; } public string Refresh_token { get; set; } public string Openid { get; set; } public string Unionid { get; set; } public string Scope { get; set; } public int Expires_in { get; set; } public int Errcode { get; set; } public string Errmsg { get; set; } }3.2 使用 HttpClient 请求 access_token 接口ASP.NET Core 里推荐用IHttpClientFactory不要每次new HttpClient()避免 Socket 耗尽。我习惯把微信接口调用封装成一个服务public async TaskAccessTokenResult GetAccessTokenAsync(string code) { var client _httpClientFactory.CreateClient(wechat); string url $https://api.weixin.qq.com/sns/oauth2/access_token?appid{config.AppId}secret{config.AppSecret}code{code}grant_typeauthorization_code; string json await client.GetStringAsync(url); var result JsonSerializer.DeserializeAccessTokenResult(json); return result; }这里有个 C# 特有的小坑微信返回的 JSON 字段是小写比如access_token。如果直接用System.Text.Json反序列化到一个属性名为大写Access_token的类默认匹配不成功。要么属性名跟着用access_token要么反序列化时配置PropertyNameCaseInsensitive true但后者对下划线无效。最省事的是给属性加[JsonPropertyName(access_token)]标签。3.3 返回结果反序列化与错误码判断微信接口的返回格式很特殊成功时很多字段是“缺省”状态失败时则统一返回{errcode: 40029, errmsg: invalid code}。所以反序列化之后要同时判断if (result?.Errcode ! 0 !string.IsNullOrEmpty(result?.Errmsg)) { logger.LogError(微信登录失败errcode{Errcode}errmsg{Errmsg}, result.Errcode, result.Errmsg); throw new WxAuthException(result.Errmsg); }这里建议把Errcode定义成int微信成功时不返回这个字段缺省值正好是0判断起来比较顺畅。如果定义成int?每次还要判断HasValue啰嗦。3.4 建立自有登录态别让前端抱着 openid 裸奔拿到openid和用户资料后不要让前端一直靠“openid”来认证。正确做法是后端根据 openid 找到本地用户然后签发自己的登录凭证比如 JWT 或 Session。后续请求都走自有凭证而不是每次都拿 openid 去微信验证。我遇到的典型反例是前端把openid存到 localStorage每次请求都传openid后端也不校验这个 openid 是否真实有效。有心人只要知道别人的 openid就能伪造身份。这样登录等于没做。实际写法一般是var localUser await userService.FindOrCreateByOpenIdAsync(tokenResult.Openid, tokenResult.Unionid); var jwtToken tokenService.GenerateToken(localUser.UserId); return new { Token jwtToken, UserInfo localUser };3.5 C# 环境下的几个特殊坑第一微信接口偶尔会超时HttpClient 默认超时时间不能直接依赖我给微信接口单独设置的超时是 5 秒并且加了重试一次的逻辑。第二回调接口要允许匿名访问否则微信跳回来就被拦截登录态中间件挡掉了。第三OpenID 和 UnionID 的绑定要加数据库唯一索引并发登录时很容易插入重复用户。4. 小程序登录与手机号获取和扫码登录完全是另一套打法小程序相关的热搜词里“微信小程序登录获取手机号”出现频率非常高。很多同学以为登录和拿手机号是一回事其实是两回事。4.1 wx.login 拿到的 code 不能直接交换手机号在小程序前端调用wx.login({ success: async (res) { const code res.code; const resp await request(/api/login, { code }); } });后端拿着这个 code 调https://api.weixin.qq.com/sns/jscode2session?appidAPPIDsecretSECRETjs_codeCODEgrant_typeauthorization_code返回结构是{ openid: 用户唯一标识, session_key: 会话密钥, unionid: 开放平台唯一标识 }这个接口只解决“用户是谁”的问题不解决“用户的手机号是什么”的问题。手机号属于额外敏感信息微信单独包装了一个组件不会在jscode2session里返回。4.2 获取手机号组件开发实战现在小程序获取手机号几乎都用下面这种方式button open-typegetPhoneNumber bindgetphonenumberonGetPhoneNumber 微信一键登录 /button前端事件处理async onGetPhoneNumber(e) { if (e.detail.errMsg ! getPhoneNumber:ok) { return; } // e.detail.code 是手机号动态令牌不是手机号本身 const phoneCode e.detail.code; const resp await request(/api/getPhone, { code: phoneCode }); }注意e.detail里现在已经不会直接给你手机号了只有一个临时code。这个 code 需要后端再调一次微信接口POST https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_tokenACCESS_TOKEN请求体{ code: 手机号动态令牌 }响应里才有phone_info里面包含phoneNumber、purePhoneNumber、countryCode等。整个链路用语言描述就是用户点按钮 - 微信给前端一个临时 code - 前端传给后端 - 后端用 access_token 换手机号。这里的前端 code 有效期很短通常是 5 分钟后端拿了之后立刻用掉别存。4.3 手机号快速验证组件的兼容问题2023 年起微信主推“手机号快速验证组件”主要规范就是open-typegetPhoneNumber。但不同基础库版本对返回内容有差异老版本可能返回encryptedData和iv新版本返回code。如果你的项目里还有老代码后段要兼容两种返回不然一升级基础库手机号就拉不到了。同时注意这个能力绝大多数情况只对非个人主体小程序开放。个人主体小程序申请不到该权限即使把按钮写对了用户点击后也会提示“该小程序不能使用手机号快捷验证”。4.4 小程序用户标识的最佳实践openid、unionid、手机号三级小程序里最稳妥的用户体系是三级openid用于当前小程序的确认身份。unionid用于跨小程序、公众号、网页应用的身份合并。手机号用于业务联系和账号找回。不要试图用手机号当主键因为用户可能换绑手机号也不要用 openid 当唯一账号因为同一个用户在公众号和小程序里的 openid 完全不同。我在项目里的做法是user_account表存 unioniduser_openid表存各端的 openiduser_phone表存手机号。哪怕用户把手机号换掉身份还是稳的。5. 微信登录列表出现多个名字删除与解绑的前后关系搜索热词里有一条很现实“微信登录小程序有好几个名字怎么删除”。这里我分开说如果是普通用户想清理授权记录那是手机端操作如果是开发者想清掉自己测试环境里没用的应用那是后台操作。5.1 你看到的“好几个名字”到底来自哪里你在微信里看到的授权列表名称来自小程序的“显示名称”不是代码里的项目名。如果你同时是多个小程序的开发者同一个用户点过多个测试小程序授权列表就会积累很多条。更常见的是一个小程序同时存在开发版、体验版、正式版它们在手机上的名称有时候完全一样。比如你一边在开发者工具里预览一边扫码体验版一边又进正式版微信不会把它们合并因为凭据不同所以你会看到“同一个名字”出现好几次。5.2 个人用户在微信端删除授权记录如果你是普通用户路径是微信 - 我 - 设置 - 个人信息与权限 - 授权管理 - 找到对应小程序 - 删除授权。删除之后小程序再想登录时会重新弹身份确认框但不会清掉小程序本地的用户数据。你在小程序里已经产生过的订单、记录都还在只是下次登录会多一步确认。这个动作本质上叫“移除授权”不是“注销小程序账号”。5.3 开发者清理不用的测试小程序注销、解绑、改名如果你是小程序开发者想清理没用的“名字”入口在微信公众平台的小程序后台删除后台上传的版本在“管理 - 版本管理”里把开发版和体验版移除。解绑开放平台如果这个小程序绑定了开放平台账号在“设置 - 基本设置 - 关联设置”里主动解除关联。注销小程序如果整个小程序都不想要了可以提交注销申请。注销前先确认没有线上业务而且主体信息要符合微信注销条件。注意“删除小程序”不是点个按钮就立即消失微信对注销有审核期中间还能用原账号登录恢复。生产环境里千万别在高峰期做这个操作。5.4 多环境小程序命名规范建议我个人的教训是同一套代码往往要拉多个环境我建议在小程序后台把“显示名称”区分开。比如正式版叫“XX商城”体验版叫“XX商城体验版”开发版用“XX商城开发版”。有人担心改名影响品牌其实体验版和开发版只给内部人员看名字带后缀很安全反而能避免“同名混淆”问题。项目里也可以把wx.request的 baseUrl 按照环境变量区分在小程序启动时读取编译类型自动指向对应后端环境。这样登录之后打开的是哪个环境从名字上就能一眼看出来。6. 高频问题排查这些现象看起来像 BUG其实是配置和缓存聊完流程和代码最后一节专门讲排查方法。下面这些问题我都遇到过每一个都差点让我怀疑微信接口是不是偷偷改了。6.1 redirect_uri 参数错误不一定是参数写错小程序里搜“微信扫码登录”相关报错最多见的绝对是redirect_uri参数错误。此时先别急着翻代码直接在微信开放平台后台看两个地方回调域名是否和请求域名一致协议 域名。回调地址中的redirect_uri参数是否被完整编码。我就遇到过前端把http://localhost:8080填进去后端注册的回调域名是http://localhost:8080看起来一致但微信开放平台强制要求回调域名不能带端口导致扫码确认后微信一直报参数错误。这种问题只能把回调请求转发到一个无端口路径上解决开发环境用内网穿透或网关映射。6.2 code 过期或重复使用时的错误码微信的 code 有效期很短网页扫码登录一般是 5 分钟小程序wx.login的 code 也是 5 分钟且只能消费一次。遇到40029 invalid code时先查是不是同一个 code 被请求了多次。我会在后端加一层 Redis 缓存key 为wx:code:{md5(code)}存在就直接拒绝这样能避免并发场景下的重复消费。40163 code已经使用过这个错误码同样是在告诉你别再拿旧 code 换 token。出现这种报错经常是因为前端发起多次登录请求每次都把同一个 code 包在请求体里后端又把 code 当成会更换的数据不设防地重放消费。6.3 小程序获取手机号失败与主体类型限制如果用户点击按钮后提示“无法获取手机号”先把小程序后台的“服务类目”和主体类型查清楚。企业、个体户、事业单位等非个人主体基本都能申请个人主体直接没有这个能力。其次检查基础库版本项目里用的一些老 API 在新版微信里已经不支持了。还有一点容易被忽略获取手机号接口需要后端先通过client_credential接口拿到access_token这个 token 是“小程序全局接口调用凭据”不是用户维度 token。我在代码里专门做了缓存避免每次请求都重复获取否则接口调用频率很容易触发限流。6.4 session_key 失效与登录态保持的边界小程序里有个session_key它不和前端直接暴露但后端每次解密用户数据时要依赖它。如果session_key过期解密出来的数据就是乱码或直接报错。微信官方规定session_key有效期没有明说但通常和wx.login的 code 有效期、token 刷新策略相关。我的建议是后端不要把 session_key 存到 Redis 里当永久会话用。每次需要解密用户信息时尽量让前端重新wx.login换来新的 code再换取新的 session_key。同时用户自己的登录态比如我方签发的 token应该和微信 session_key 分离不要因为微信端 session_key 过期就把已登录用户踢下线。6.5 最后一点排错方法论微信登录链路长前后端各有一截出问题时先站在“数据流”角度画一条线前端发起位置 - 微信服务器 - 回调地址 - 后端接口 - 数据库。哪里日志断了问题基本就在哪里。我每次排查会先把回调参数完整打出来包括code、state、errmsg然后对照微信文档的开放平台接口能力和错误码基本十分钟内能定位。如果还是查不出最后一步就是“拆”把前端先改成固定 code 测试后端单独模拟一次微信请求。只要代码逻辑没动问题大概率出在签名、域名、端口或者缓存上而不是微信本身。微信登录真正难的不是调通而是把每一步的边界条件都想清楚。
返回列表