ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

微信登录与OAuth2.0原理详解:扫码、小程序到C#后端接入

微信登录与OAuth2.0原理详解:扫码、小程序到C#后端接入 你有没有想过一个很诡异的事情你打开京东、淘宝、各种App点一下微信登录输入的是微信的密码或者干脆扫码京东却知道你就是你。更离谱的是京东自始至终没有拿到你的微信密码甚至连你的微信号都没看见它就敢放你进到购物车、订单页、收货地址这些核心数据里。这个问题被身边人问过无数次我刚入行后端时也好奇过后来把OAuth2.0和微信开放平台的文档翻透才发现登录和授权本来就是两码事。这篇文章就从这个问题出发把微信登录、扫码登录、小程序登录获取手机号的原理彻底讲透最后再给一份C#后端接入微信登录验证的完整实现方案。很多人以为第三方登录是微信把账号信息直接告诉京东这是最大的误解。微信公众号、小程序、App登录这套体系核心协议是OAuth2.0。它解决的核心问题就是客户端如何在用户不交出密码的前提下安全地获取用户身份信息。理解这一点你再看京东的微信登录就完全不会觉得神奇了。1. 先说结论京东压根不需要知道你的微信密码1.1 登录与授权两个被混为一谈的动作传统账号密码登录的逻辑很简单你注册时给京东一个密码登录时再把密码提交给京东京东把你提交的密码和数据库里的哈希值比对一致就放行。这个模型里密码只有一个持有方就是京东用户和京东之间是直接信任关系。但你的微信账号和密码是微信体系内的重要凭证。京东如果要知道你的微信密码就意味着你必须在京东的页面里输入微信密码微信完全不知情整个链条里密码会被京东服务器记录、传输、存储哪怕京东再小心只要它收到过你的密码就存在泄露风险。这显然不能接受。微信登录换了一套思路微信作为你的身份提供方先确认你是不是你然后给京东一个受控的凭证京东用这个凭证去微信换取你的部分公开信息。整个过程里你不需要向京东透露密码京东也拿不到密码。这个凭证就是OAuth2.0里的授权码和访问令牌。用生活里的例子类比传统密码登录是你把家门钥匙交给超市老板老板拿着钥匙去你家确认你住这里微信登录是你小区的门卫大爷替你做担保他朝超市老板点个头说这人我认识是1栋的住户老板就放你进超市但门卫不会把钥匙给老板也不会告诉老板你家有几道锁、保险柜在哪个位置。1.2 京东到底从微信拿到了什么通过微信登录京东通常能拿到的是微信授权页面里明确公示给你的那些信息一般包括头像、昵称、性别、地区以及一个针对京东这个应用唯一的openid。注意这里的openid不是一个你能直接读懂的微信号也不是手机号它是一串类似oXk8s5j9yG2...的字母数字专门给京东用来识别你这个用户。京东拿不到的是你的微信号、微信密码、聊天记录、通讯录、朋友圈内容、好友列表。这些信息不在OAuth2.0的授权范围内微信也不会开放这些数据给第三方应用。这里有一个很关键的设计同一个用户在不同应用里的openid是不同的。你在京东的openid是A在一个叫某某优惠券的App里的openid是B两者完全不一致这样第三方应用之间没法通过openid串通起来追踪你的全网行为。如果某个开发者同时运营多个应用想要识别同一个微信用户必须通过微信开放平台的unionid机制那是另一个话题但单就京东能不能通过微信登录顺藤摸瓜看到你的微信社交关系这个问题答案是否定的协议层面就堵死了。2. OAuth2.0的设计思路把验证身份这件事外包出去2.1 OAuth2.0到底是什么OAuth2.0不是一个加密算法也不是一段特定的代码它是一个授权协议标准定义在RFC 6749中。它规定了一套角色分工和交互流程让第三方应用可以在用户本人同意的情况下受限制地访问用户在另一个平台上托管的资源。说到OAuth2.0绕不开四个角色角色对应到微信登录场景资源所有者你微信用户客户端京东App或京东网页授权服务器微信开放平台后台资源服务器微信保存用户头像、昵称等信息的服务器整个流程的目标可以浓缩成一句话让京东客户端从微信授权服务器拿到一个凭证再用这个凭证去访问你在微信资源服务器上的用户信息。2.2 为什么要有授权码而不是直接把令牌扔给京东OAuth2.0有四种授权模式微信网页登录采用的是其中安全性最高的授权码模式。授权码模式里有一个关键中间品code。为什么不能微信直接把access_token通过浏览器重定向返回给京东因为浏览器太容易泄密了。你想想access_token一旦发到浏览器端它就可能出现在历史记录、网络请求日志、浏览器插件能读到的环境里而且浏览器端根本无法保证这个token会被京东自己拿到还是被别的脚本截获。授权码模式的设计是微信先把一个有效期极短、只能用一次的code通过浏览器传给京东京东拿到code之后在服务端用这个code配合自己的AppSecret向微信的后端接口换取access_token。整个换token的过程发生在服务器与服务器之间浏览器参与不进来AppSecret也不会暴露给前端。这里的生活类比是你去酒店前台前台给你一张只限当天使用、只能进一次健身房的门卡而不是直接把房卡打印出来贴在电梯口。你通过正规渠道前台换到真正的门卡而那张临时纸条即使被捡到也换不了什么东西。2.3 密码模式为什么在这里行不通OAuth2.0里其实有一种密码模式允许客户端直接拿用户名和密码去授权服务器换token。这种模式设计初衷是给自家开发的第一方应用用的因为它要求客户端必须可信到能接收用户密码。第三方登录如果走密码模式那就完全违背初衷了。你让用户在京东页面上输入微信的账号密码京东就成了微信密码的经手方所有安全设计全部失效。这也是为什么微信开放平台从来没有把微信公众号登录做成密码模式的原因之一。微信登录用的授权码模式本质上就是微信对外说你可以信任我但我不把底牌交给你。3. 微信网页登录完整链路拆解从点击到回跳3.1 点下微信登录后后台发生了什么先说微信开放平台网站应用的扫码登录。整个流程从用户点击按钮那一刻开始就变成了一串HTTP请求和重定向。第一步京东的后端需要生成一个授权链接大概长这样https://open.weixin.qq.com/connect/qrconnect?appidwx1234567890abcdefredirect_urihttps%3A%2F%2Fwww.jd.com%2Fapi%2Fwechat%2Fcallbackresponse_typecodescopesnsapi_loginstateabc123xyz#wechat_redirect这个链接里的参数很关键拆开看appid京东在微信开放平台申请到的应用唯一标识。redirect_uri微信确认用户身份之后把用户浏览器重定向回京东的回调地址必须URL编码。response_typecode告诉微信京东要的是授权码。scopesnsapi_login授权范围这里表示网页登录场景。state京东自己生成的随机字符串防止CSRF攻击后面细说。第二步用户访问这个链接后微信服务器会展示一个二维码页面。你用手机微信扫码手机上会弹出确认授权页列出京东准备获取的权限头像、昵称、性别、地区等。第三步你点确认授权后微信后台会把浏览器重定向回京东回调地址并且在URL上附带两个参数https://www.jd.com/api/wechat/callback?codeo6ZGv9X3bN9mQFt0stateabc123xyz注意此时京东只拿到了一个code和一个state。第四步京东后端收到请求后先用state验证请求是自己发起的再用code去微信后端换取access_token。换取的接口是GET https://api.weixin.qq.com/sns/oauth2/access_token?appidAPPIDsecretAPPSECRETcodeCODEgrant_typeauthorization_code这一步务必放在服务端执行因为请求里带了AppSecretAppSecret一旦泄露进浏览器等同于把微信应用的管理权限拱手让人。第五步京东用返回的access_token和openid调用用户信息接口GET https://api.weixin.qq.com/sns/userinfo?access_tokenACCESS_TOKENopenidOPENID然后拿到昵称、头像、性别、城市等公开信息。第六步京东拿openid去自己的数据库里查有没有绑定记录没有就创建一个新用户有就直接登录然后给用户签发京东自己的登录态比如Session或JWT。从此以后用户再访问京东时走的全是京东自己的会话体系不需要再经过微信。3.2 state参数是白拿的吗为什么必须校验很多粗心的开发者拿到回调地址一看有code就赶紧去换token完全忽略state。这非常危险。state的设计意图是让发起授权的客户端判断这个回调是不是我自己发起的流程。它应该是一个随机字符串在生成授权链接时由后端生成存入Session或Redis用户被重定向回来时后端取出state对比不一致就拒绝后续流程。如果不校验state攻击者可以构造这样一个场景他诱导你在他的网站上点一个授权链接这个链接的redirect_uri是京东的回调地址你会被带到一个微信授权页你一旦点击同意微信就会把code发到京东的回调地址。此时京东后端如果能把这个code和攻击者控制的账号关联起来那攻击者的账号就绑定了你的微信身份你后续在京东上的数据可能会被攻击者看到。这种攻击叫登录CSRF本质上是借你的授权绑他的账号。所以state不是可有可无它是一道必须认真对待的安全防线。实际项目中我是用Guid随机生成设置5到10分钟过期换token之前先校验。3.3 为什么code用一次就作废code是一次性的这一点在微信官方文档里写得很明确。换取access_token成功之后同一个code再次使用微信会返回类似invalid code的错误。业务上这是为了防止重放攻击。code在URL里传输如果被中间人截获一旦它能重复使用攻击者就可以拿着这个code去冒充用户换token。设成一次性之后截获的价值远低于有效期内成功截获并立即使用的难度攻击窗口被压缩到几分钟甚至更短。还有一个容易被忽略的细节code会在日志里留痕。很多团队为了方便排查问题把回调URL上的所有参数都打进了日志code就被记录下来了。我后来在日志系统里加了URL参数脱敏code、token一律打码没必要因为排查问题给安全埋雷。4. 微信生态里的三个登录变种扫码、App内授权、小程序4.1 网页扫码登录和App内微信登录的差别网页端微信登录走的是snapi_login用户看到二维码用手机扫描确认。App内微信登录走的是微信SDK的授权流程用户在App里点微信登录系统调起微信App用户在微信里确认授权然后通过App间跳转把code传回你的App。这两种方式底层都是同一个OAuth2.0授权码模式区别只在怎么调起授权和怎么把code传回来。如果做C#后端前端的事情对你来说是透明的你只需要等前端把code传到后端即可。要注意的是移动端App接入微信SDK时需要在微信开放平台创建移动应用并且做应用签名校验这和网站应用的流程是分开审核的。4.2 小程序登录wx.login拿到code后端换session_key小程序里的登录体验不太一样。你打开一个小程序它默认会静默地调用wx.login拿到一个临时code然后把code传给后端。后端再调用微信的jscode2session接口换取openid和session_key。GET https://api.weixin.qq.com/sns/jscode2session?appidAPPIDsecretSECRETjs_codeCODEgrant_typeauthorization_code这里有个重要差异网页登录换来的是access_token小程序登录换来的是session_key。session_key的用途是解密小程序端加密数据比如老版本获取手机号时需要用它解密这个密钥绝对不能返回给前端。同时session_key的有效期是不确定的用户如果频繁调wx.login旧的session_key可能会失效后端要能容忍这种情况。小程序登录和网页授权码模式还有个体验上的不同很多小程序里用户打开就登录了没有显式的授权页。这是因为小程序本身在微信生态内微信已经把用户登录状态通过code传递给你你不需要再让用户点一次确认授权。但获取用户头像昵称、手机号这类敏感信息时仍然需要用户主动授权。4.3 小程序获取手机号新旧两种方式怎么选小程序里获取手机号是常见需求因为手机号可以直接关联真实身份和做风控。以前的做法是页面放一个button设置open-typegetPhoneNumber用户点击后前端会拿到encryptedData和iv连同一个code一起交给后端后端用session_key做AES解密取出手机号。这个方案不是不能用但加解密细节很多网上教程里key和iv的取法版本五花八门非常容易踩坑。现在微信官方已经推荐了新方案利用getPhoneNumber返回的code后端直接调用官方接口拿手机号全程不需要自己解密。新方案流程前端按钮open-typegetPhoneNumber触发用户授权后拿到code。前端把code传给后端。后端拿到access_token这里的access_token是接口调用凭据不是用户token请求POST https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_tokenACCESS_TOKEN请求体里带上{code: 前端传来的code}。接口直接返回用户的手机号信息。这个新方案大大降低了开发门槛也避免了session_key解密过程中因为版本差异导致的兼容性问题。我现在的项目里如果只要求拿手机号一律走新接口只有需要获取其他加密数据时才考虑老方案而且会严格封装解密工具类避免到处复制粘贴。4.4 unionid多应用识别同一用户的钥匙微信生态里还有一个概念必须提unionid。同一微信用户在公众号、小程序、App、网站应用里的openid都不一样如果你运营多个应用想判断这是同一个用户就需要借助unionid。要拿到unionid前提是这些应用都绑定在同一个微信开放平台账号下并且用户同时授权过这些应用。C#后端在处理登录时判断逻辑通常是先查openid再查unionid都没有就新建账号。很多电商平台多端打通的帐号体系就是靠unionid实现的。不过京东这种体量的平台一般会引导用户绑定手机号来统一跨端身份unionid只是辅助手段。5. C#后端实现微信登录验证一份能跑的代码5.1 开发前准备AppID、AppSecret、回调地址动手写代码之前先到微信开放平台注册账号创建网站应用拿到两样东西AppID和AppSecret。AppID相当于应用的身份证号可以出现在前端AppSecret相当于应用的管理员密码只能保存在后端服务器、配置文件或密钥管理服务里绝不能放进前端代码或Git仓库。同时要在开放平台配置授权回调域。网站的授权回调域通常填你的域名比如https://www.example.com。注意很多开发者最后报错redirect_uri参数错误就是后台配置的域名和授权链接里redirect_uri的域名不一致差一个端口、一个http/https都过不了。本地开发时通常需要把本地服务映射到外网才能收到微信的异步回调因为微信服务器无法访问你的localhost。我用.NET开发时常用.NET Aspire自带的内网穿透或者用一些内网穿透工具把本地端口映射成一个HTTPS域名然后把授权回调域临时改成这个域名才能走完整流程。5.2 第一步生成授权链接并保存state在ASP.NET Core里后端要提供一个接口生成授权链接。模型中存储state时我用一个小类public class OAuthState { public string State { get; set; } public DateTime ExpireAt { get; set; } }生成链接的逻辑很简单public IActionResult StartWechatLogin() { var state Convert.ToBase64String(Guid.NewGuid().ToByteArray()) .TrimEnd().Replace(, -).Replace(/, _); var redirectUri Url.Action(WechatCallback, Auth, null, Request.Scheme); var encodedRedirectUri Uri.EscapeDataString(redirectUri); var url $https://open.weixin.qq.com/connect/qrconnect?appid{_wechatConfig.AppId}redirect_uri{encodedRedirectUri}response_typecodescopesnsapi_loginstate{state}#wechat_redirect; HttpContext.Session.SetString(wechat_oauth_state, state); return Redirect(url); }这里的redirect_uri必须用ASP.NET Core根据请求生成保证域名和实际被打到的地址一致否则又是redirect_uri参数错误。5.3 第二步回调接口处理code并换token用户扫码同意后微信会带着code和state回到你的回调地址。此时后端要做的第一件事不是换token而是校验statepublic async TaskIActionResult WechatCallback(string code, string state) { var expectedState HttpContext.Session.GetString(wechat_oauth_state); if (string.IsNullOrEmpty(expectedState) || state ! expectedState) { return BadRequest(state校验失败); } var client _httpClientFactory.CreateClient(); var tokenUrl $https://api.weixin.qq.com/sns/oauth2/access_token?appid{_wechatConfig.AppId}secret{_wechatConfig.AppSecret}code{code}grant_typeauthorization_code; var tokenResponse await client.GetFromJsonAsyncWechatTokenResponse(tokenUrl); if (tokenResponse null || !string.IsNullOrEmpty(tokenResponse.ErrCode)) { return BadRequest($换取token失败: {tokenResponse?.ErrMsg}); } var userInfoUrl $https://api.weixin.qq.com/sns/userinfo?access_token{tokenResponse.AccessToken}openid{tokenResponse.OpenId}; var userInfo await client.GetFromJsonAsyncWechatUserInfo(userInfoUrl); var user await _userService.FindOrCreateUserByOpenId(userInfo.OpenId, userInfo); var jwt GenerateJwt(user.Id); return Ok(new { token jwt, user user }); }注意几个坑GetFromJsonAsync需要配置JSON反序列化时忽略大小写微信返回的错误结构里同时包含errcode和errmsg而成功结构里是access_token、expires_in、openid两个模型最好都处理。另外code参数在回调URL里可能是codexxx有些场景code会变成oauth_code尽量兼容解析。5.4 第三步小程序jscode2session和获取手机号小程序端调用wx.login拿到code后传给后端后端代码和网页登录换token类似只是接口地址换成了jscode2sessionpublic async TaskIActionResult MiniProgramLogin(string code) { var client _httpClientFactory.CreateClient(); var url $https://api.weixin.qq.com/sns/jscode2session?appid{_wechatConfig.AppId}secret{_wechatConfig.AppSecret}js_code{code}grant_typeauthorization_code; var session await client.GetFromJsonAsyncJsCode2SessionResult(url); if (session null || session.OpenId null) { return BadRequest(jscode2session失败); } var user await _userService.FindOrCreateUserByOpenId(session.OpenId, null); return Ok(new { token GenerateJwt(user.Id) }); }这里得到的session_key不要丢如果需要用老方案解密手机号它才是核心密钥。如果采用新方案获取手机号需要先获取接口调用凭据access_token再调wxa/business/getuserphonenumber。access_token是用AppSecret去换的全局票据有缓存机制通常用https://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credentialappidAPPIDsecretSECRET有效时间约7200秒。项目中我会把它缓存在内存里带一个提前5分钟的过期预留避免每个手机号请求都重新去换token。public async TaskPhoneNumberResult GetPhoneNumber(string phoneCode) { var accessToken await GetAccessTokenAsync(); var client _httpClientFactory.CreateClient(); var response await client.PostAsJsonAsync( $https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token{accessToken}, new { code phoneCode }); var result await response.Content.ReadFromJsonAsyncPhoneNumberResult(); return result; }这个接口返回的手机号数据是加密的还是明文实际返回的是JSON里嵌套的phone_info里面直接就是purePhoneNumber和countryCode前端用户授权后你就能拿到明文手机号。拿到后务必脱敏存储至少把中间四位打码完整号码要加密存储或放到专门的敏感信息表里。5.5 老方案解密手机号的参考实现如果因为兼容旧版本必须自己解密AES解密的核心代码可以这样写public string DecryptPhoneData(string encryptedData, string sessionKey, string iv) { using var aes Aes.Create(); aes.KeySize 128; aes.BlockSize 128; aes.Mode CipherMode.CBC; aes.Padding PaddingMode.PKCS7; aes.Key Convert.FromBase64String(sessionKey).Take(16).ToArray(); aes.IV Convert.FromBase64String(iv); ICryptoTransform decryptor aes.CreateDecryptor(aes.Key, aes.IV); byte[] encryptedBytes Convert.FromBase64String(encryptedData); byte[] plainBytes decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length); return Encoding.UTF8.GetString(plainBytes); }解密后得到的JSON通常长这样{ phoneNumber: 13800138000, purePhoneNumber: 13800138000, countryCode: 86, watermark: { timestamp: 1700000000, appid: wx1234567890abcdef } }这里重点说一下网上那些互相矛盾的实现有的说key取sessionKey前16字节有的说直接用Base64解码全部字节还有的说IV是sessionKey的前16字节。老实讲这些版本在不同时期、不同场景下都出现过能跑通的情况根源是微信文档更新过、SDK版本不一致、以及很多人复制粘贴时改了一半。我的建议是优先用官方最新的getuserphonenumber接口不用碰解密真要用老接口就以你当前挂载的微信SDK版本对应的官方文档为准并在代码里写注释注明文档版本号避免后人踩坑。6. 常见问题与避坑实录6.1 redirect_uri参数错误反复出现怎么查先别急着改代码按顺序排查第一授权回调域是否和回调URL的域名完全一致注意必须一模一样包括https、端口、是否带www第二redirect_uri是否做了URL编码如果直接用HttpUtility.UrlEncode编码注意空格会变成微信要求%20通常用Uri.EscapeDataString更稳第三回调地址是否可以被公网访问微信服务器不像浏览器它无法访问内网地址。我自己踩过一次特别隐蔽的坑后台配置的域名是example.com但前端生成链接时不小心带上了https://www.example.com多了一个www直接报错。6.2 换token时提示code无效或已被使用这个错误有两种常见场景。第一种是用户或测试脚本手动刷新了回调页面同一个code被提交了两次第一次成功后第二次必然报错需要在前端或者后端做幂等处理比如在state里记录一个已消费标记。第二种是code确实过期了授权码有效期很短如果你在用户授权之后设置了一个很慢的中间跳转流程很可能走到后端时已经超时。处理方式是一律重新发起授权不要尝试去救这个code。6.3 明明扫码授权了却拿不到nickname和头像微信网页登录默认的scopesnsapi_login在低版本或部分场景下只能拿到openid和头像要拿到昵称、性别、地区需要确保授权链接里的scope包含snsapi_userinfo并且回调换取access_token之后再去调userinfo接口。如果用户曾拒绝过授权微信会在授权页让用户重新确认此时如果用户在微信设置里关闭了允许第三方应用获取信息即使在授权页点了同意userinfo接口也可能返回空数据。这种问题大多发生在老微信版本上处理办法是拉取失败时降级显示为空引导用户手动补填。6.4 微信返回的access_token过期了用户这边掉线吗很多开发者容易把微信的access_token当成自己应用的登录态直接存进Cookie或本地缓存这是不对的。access_token的有效期通常是7200秒两小时后过期但用户不可能两小时就重新登录一次。正确做法是用微信返回的openid先找到或创建自己的用户再签发自己的登录态比如JWT有效期由你决定。微信的access_token只是你用来拉取用户信息的一次性凭证业务会话不依赖它。数据库里如果需要持久化access_token必须加密存储并且设置过期时间字段方便定期清理。6.5 拿手机号之前先想想隐私合规最后聊聊合规。手机号属于敏感个人信息现在主流App在获取用户手机号前都必须弹窗告知使用目的并取得用户明示同意。小程序里的getPhoneNumber组件本身就是一个授权动作但你在后端存储时仍然要遵循最小必要原则能不存就不存必须存就脱敏、加密、设权限。微信登录也是一样你拿到的昵称、头像、地区虽然属于公开信息但不能拿来随意做用户画像分析或在第三方之间共享。授权页面已经向用户展示了你要获取什么如果后端实际拉取的范围超出了展示范围一旦被用户投诉或应用市场抽查到轻则下架整改重则关闭开放平台权限。接入任何第三方登录安全都只是一部分合规意识才是决定这一个功能能不能长期存活的关键。接入微信登录这几年我最大的感受是OAuth2.0本身并不复杂难的是那些藏在细节里的安全校验和边界情况。一个state没校验可能被人绑号一个code打到日志可能泄露身份一个session_key随手传给前端加密保护就形同虚设。设计第三方登录方案时不妨多问自己几个问题如果这个code被截获会怎样如果这个token泄露会怎样如果这条用户数据被恶意读取会怎样。把这些问题在纸上过一遍再对照官方文档看代码你会发现很多坑其实都写在文档里只是刚动手时没耐心看而已。
返回列表