首页 / 资讯中心 / 文章详情

OAuth 2.0与第三方登录:从Token到授权码的完整安全解析

OAuth 2.0与第三方登录:从Token到授权码的完整安全解析 ★ FEATURED ARTICLE
你有没有过这样的疑惑在某电商App上点“用社交账号登录”页面跳到了一个社交平台的授权页上面写着“某电商App申请获取你的昵称、头像”你点了同意然后瞬间回到了那个电商App并且已经登录成功了。整个过程你从头到尾没有在电商App里输入过社交平台的密码。那么问题来了——电商App到底是怎么确认“你就是你”的它为什么不需要你的社交平台密码因为电商App拿到手的根本就不是你的账号密码而是一张由社交平台签发的“入场券”。这套东西在行业里有个正经名字叫OAuth 2.0授权协议。它要做的事概括成一句话让第三方应用没法拿到你的密码也能替你完成登录。这篇文章不会讲一堆晦涩的RFC文档我会从原理、完整流程、安全边界到实际开发中踩过的坑把这个机制彻底掰开讲清楚看完你能顺手写一个第三方登录的Demo也知道该怎么判断一个第三方登录安不安全。1. 密码是钥匙Token是访客卡先搞清楚“授权”和“交出密码”的区别想理解这套机制核心要转过一个弯登录这件事并不一定需要“你的密码”才能证明“你是你”。第三方应用需要的只是“你被我这次授权了”而密码只是证明“你有权授权”的一种手段但不是唯一手段。1.1 密码这个东西暴露一次就等于全盘皆输你可以把密码想象成你自己家里的全套钥匙。别人要进你家帮你取一份文件如果你把整套钥匙都给他他能开你家大门、卧室门、保险柜门甚至能配一把新钥匙。最糟糕的是他配了新钥匙之后你家的钥匙体系并没有失效安全隐患会一直存在。密码也是这样。如果某电商App拿到了你的社交平台密码它就相当于拥有了你社交账号的完整控制权能看你的聊天记录、能改你的资料、能用你的名义发消息而且这种权限是永久的、不可追溯的、无法撤销的除非你去改密码而且改了也不一定保证它没存过。更麻烦的是电商App那边把它存进自己的数据库一旦数据库泄露你的密码就在黑市上流通了。密码这东西暴露一次就是全盘皆输。那电商App到底需要什么其实它只需要知道三件事你是社交平台上的某个合法用户、你的昵称头像等基础资料、你确实愿意让我用这个身份登录。至于你的密码、你的聊天记录、你的好友列表它根本就无关要。1.2 访问令牌Token不是一个“新密码”而是一张“受限通行证”OAuth给出的方案是让社交平台给电商App发一张“访客卡”这张卡在行业里叫访问令牌Access Token。和密码这种“全套钥匙”相比它有四个最根本的区别它是受限的卡上写了能访问哪些数据比如只有昵称、头像不能碰聊天记录和好友列表。这个限制叫“作用域”scope。它是会过期的通常几十分钟到几天就失效无法永久使用。过期时间是Token本身携带的属性。它是可撤销的用户可以随时到社交平台的设置页里“取消该应用的授权”一经撤销这张卡立刻作废。它是可以追溯的Token由社交平台签发平台侧有完整记录一旦发现某个应用滥用权限可以精准吊销它。打个比方你去亲戚家小区借住几天物业给你办了一张临时门禁卡只允许每天21点前刷卡进入只能坐电梯到指定的楼层卡过期自动失效。你的亲戚不需要把家里的全套钥匙给你物业那边想不让你进了随时可以刷卡销掉。这套机制对亲戚用户、对你应用、对物业社交平台三方都安全。所以回到标题的问题电商App不需要你的社交平台密码是因为它真正需要的根本不是密码而是一张够用的、临时的、可收回的通行证。社交平台用密码来验证“你是你”然后把这张通行证发给电商App密码始终留在社交平台这个“保险柜”里谁都拿不走。2. 从点击“社交账号登录”到回跳首页一次授权跳转的完整幕后流程知道了原理再看刚才那个操作背后的完整链路。我用最经典的授权码模式来拆解这也是目前第三方登录用得最多的实现方式。2.1 第一步到第三步从发起跳转到用户点“同意”完整的流程从你点击“用社交账号登录”开始。整个交互过程可以拆成五个阶段电商App跳转到社交平台的授权页。你在社交平台的页面上输入账号密码注意这时页面域名是社交平台的官方域名。社交平台验证你的身份并向你展示“电商App将获得你的昵称、头像等权限”等你点“同意”。社交平台把浏览器重定向回电商App的回调地址并在地址栏里带一个“授权码”code。电商App的后端拿这个授权码去社交平台换一个访问令牌再用令牌拉取你的资料。这五步里最容易让人迷惑的是第2步输入密码的页面明明不是电商App的页面怎么还能登录回电商App因为浏览器或App内WebView这里相当于一个信使先把你的请求送到社交平台社交平台验证完身份、取得你的授权后再通过浏览器把你“送回去”。我们用浏览器地址栏里的参数来看第1步到第3步到底发生了什么。电商App构造这样一个URL让浏览器跳转https://auth.social.com/oauth/authorize? response_typecode client_idapp_8f3a2b redirect_urihttps%3A%2F%2Fmall.example.com%2Fcallback scopeprofile state6b7d9c这里每个参数都有明确含义response_typecode表示我想用“授权码”的方式走流程。client_id电商App在社交平台注册时拿到的应用标识相当于这个应用的“身份证号”。redirect_uri授权完成后回跳的地址。社交平台会严格校验它必须和注册时填写的地址一致不然就拒绝。scope申请权限列表这里是profile即基础资料是最小化申请。state一个随机字符串用来防跨站请求伪造后面细讲。你在社交平台的页面上输完账号密码、点了同意授权之后社交平台并没有直接把你送回去而是先做了内部记录某某用户在什么时间授权了某某应用哪些权限。然后它才把浏览器重定向到电商App的回调地址并且在地址栏里附加了一个很短的一次性授权码。2.2 第四步到第五步授权码换Token以及为什么不能省掉这一步这就是整个流程的精髓所在社交平台为什么不直接在地址栏里把访问令牌给你而是先给一个授权码因为浏览器地址栏太暴露了——它会出现在浏览历史里、日志里、代理服务器里甚至你旁边的同事都能瞥见。如果Token直接出现在URL里拿到它的人就能直接访问你的用户数据。所以OAuth设计了授权码这个“用完即丢”的中间凭证。授权码有这些特点有效期极短通常只有几分钟。只能使用一次换完Token立刻作废。必须配合电商App在社交平台注册时拿到的“应用密钥”client_secret一起使用。换取动作必须发生在电商App的后端服务器而不是浏览器里。第5步的换Token请求代码上长这样POST https://auth.social.com/oauth/token Content-Type: application/x-www-form-urlencoded grant_typeauthorization_code client_idapp_8f3a2b client_secretyour_app_secret_here codereceived_auth_code redirect_urihttps%3A%2F%2Fmall.example.com%2Fcallback社交平台验证授权码、应用密钥、回调地址三样东西都没问题后返回一个JSON{ access_token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..., token_type: Bearer, expires_in: 7200, refresh_token: dGVzdC1yZWZyZXNoLXRva2VuLTEyMzQ1Ng, scope: profile }从这以后电商App就拿到了门票。后续拉取你的昵称头像时只要在HTTP请求头里带上这张TokenGET https://api.social.com/v1/userinfo Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...社交平台看到Token查了一下发现这是刚才那个用户授权签发的于是把用户资料返回给电商App。电商App拿这份资料在自己的系统里建账号或者绑定已有账号登录完成。2.3 浏览器地址栏里的参数都在表达什么我把上面流程里浏览器地址栏的变化串一组真实感场景来还原用户点击后地址栏变成https://auth.social.com/oauth/authorize?response_typecodeclient_idapp_8f3a2bredirect_urihttps%3A%2F%2Fmall.example.com%2Fcallbackscopeprofilestate6b7d9c用户同意后地址栏再变成https://mall.example.com/callback?codeAUTH_CODE_XYZ123state6b7d9c注意这里没有access_token只有一个code。而且用户端浏览器完全带不动TokenToken是通过电商App后端和社交平台后端之间直接通信拿到的。这也解释了为什么很多开发教程里反复强调回调地址里收到code之后换Token的操作必须在服务端不能在浏览器端做。如果你图方便在前端直接拿着code去向社交平台发请求你的应用密钥就暴露在前端代码里等于把保险柜钥匙贴在了门上。实战里我见过有人直接把client_secret写进前端代码结果被爬虫抓到密钥拿去冒充应用身份刷接口。做第三方登录第一课就是密钥永远只存在于服务端。3. 对它安全、对你放心Token为什么比密码更合格这里要正面回答很多人的疑问Token也是我登录后拿到的东西凭什么它比密码更安全这要分几个层面来讲每一层都是OAuth设计者深思熟虑的结果。3.1 最小授权原则一张牌里只写它能进哪扇门权限的最小化是Token安全性的第一层。社交平台签发的Token里scope字段决定它能访问什么数据。电商App在发起授权请求时申请的是profile那就只能读昵称、头像如果它申请的是friends、messages社交平台会在授权页上明确列出这些权限让你亲自勾选同意。但你用密码登录社交平台时可没有这种限制密码一输入所有权限全部拥有了从聊天记录到支付信息毫无保留。所以密码是一种“无限授权”而Token是一种“有限授权”。权限越少泄露后的危害越小这是安全领域最核心的原则之一。3.2 泄露之后的损失面密码是一把钥匙Token只是一张月卡我们对比一下密码泄露和Token泄露分别会发生什么指标密码泄露Token泄露影响范围账号所有功能全部失守只影响被授权的服务持续时长永久直到用户改密码到期自动失效用户控制只能改密码未必能全部收回可在授权管理页一键撤销平台追溯很难区分是谁在用可通过客户端ID与日志追溯二次扩散可用来撞库其他平台只能访问限定接口上面的对比已经很直观Token是“月卡”用完或到期就没了密码是“万能钥匙”只要还在流通风险就一直存在。关键还在于Token泄露不等于密码泄露——攻击者拿Token只能访问一个应用授权范围内的那点数据拿不到整个账号。3.3 跨平台拖库风险被从根上掐断了再补一个很多普通用户没意识到的点在第三方登录出现之前很多小网站为了省事会导流用户“用社交账号登录”的变种做法——让用户把社交平台的账号密码直接填在自己的登录框里然后后端拿这个密码去调用社交平台接口登录。这种方式叫“密码代理”它有一个致命问题小网站的数据库一旦被拖走用户社交平台密码也就跟着一起没了。而现在电商App自己根本没有密码社交平台的密码只存在于社交平台那边就算电商App被拖库拖出来顶多是用户的Token还是受到严格限制的过期Token。这里也回答了一个潜在困惑为什么我注册了电商App它却显示“未绑定手机号也能登录”因为电商App要建立的是你在这个平台的本地账号体系它把社交平台返回的用户唯一标识比如open_id与你在它平台的本地账号做映射。登录态的本质是这个映射关系成立而不是“知道你的密码”。3.4 责任边界变得清晰了最后是责任的划分。密码登录模式下一旦出现盗号纠纷平台方很难说清楚到底是谁的责任是你的密码在某个第三方网站泄露了还是官方被撞库了而OAuth模式下社交平台的授权记录在平台侧清清楚楚什么时间、哪个App、申请了哪些权限、由哪个用户确认的。这既方便平台审计也方便用户在管理页里自主查看和撤销授权从机制上厘清了三方的责任边界。4. 接入方视角我踩过的第三方登录的坑如果你是一个开发者看完上面这些原理接下来更想知道的是真要在自己的项目里接入第三方登录会碰到哪些坑这部分我总结自己对接授权的经验按“最常出事”的优先级排列。4.1 最不起眼却致命的三件事回调地址、state、code第一件事回调地址redirect_uri的匹配规则。社交平台校验回调地址时是全字符串精确匹配还是只匹配域名前缀不同平台策略不太一样。多数平台要求域名一致且路径完全一致。实际最容易踩的坑是你注册时填了https://mall.example.com/callback结果本地调试时想把它改成https://localhost:8080/callback如果平台不允许配置多个回调地址请求就会被直接拒绝。第二件事state参数的校验。我见过不少团队在联调时嫌麻烦直接把state参数删了。这是非常危险的。state是一个随机值放在发起跳转前存到用户会话里回调时比对是否一致。它的作用挡住“登录劫持”攻击攻击者先诱导你在不知情的情况下完成授权然后把他的code塞到你的回调URL里后端如果用这个code换Token就把你自己的账号绑定到了攻击者的社交账号上。没有state参数这种攻击几乎无法防御。第三件事code必须用一次就丢。授权码是一次性的社交平台后端会对使用过的code直接拒绝。所以哪怕收到相同code的重复请求也要按错误处理绝不要去“容错”。如果有人反复拿同一个code来请求你的接口这本身就是扫描行为应当记录并关注。下面是我个人常用的后端处理伪代码接任何支持OAuth的平台都通用# 伪代码演示回调接口逻辑 def oauth_callback(request): code request.GET.get(code) state request.GET.get(state) if state ! request.session.get(oauth_state): return error(state mismatch, possible CSRF attack) if not code or already_used(code): return error(invalid code) token_resp exchange_code_for_token(code) # 后端去换Token userinfo fetch_userinfo(token_resp.access_token) # 拉取用户资料 create_or_update_local_user(userinfo, token_resp) # 绑定或创建本地账号 return redirect_to_home()注意already_used(code)不仅仅是为了防重放也是排查异常请求的好信号。如果同一个code短时间内多次出现已经不只是业务逻辑问题而是有人在试探你的接口。4.2 拿到Token之后存哪里、怎么续期Token拿到手只是第一步后续管理才是大头。这里有几个必须明确的点用户侧Token不要放localStorage。浏览器里的localStorage容易被XSS脚本读走而Token一旦被读走等同于账号权限被拿。建议放在HttpOnly的Cookie里让JavaScript读不到或者走内存缓存加短期Cookie组合。服务端要做Token映射。第三方平台签发的Token不是给你无限期保存的它有过期时间。你在本地要保存access_token、refresh_token、expires_at三件套而不是只存一个access_token。过期之前你要通过refresh_token去换新换完刷新过期时间。用户解除授权要兜底。用户随时可能在社交平台那边取消授权你这边刷新Token时会收到invalid_grant错误。这时候要做的是清除本地关联关系引导用户重新走一次授权登录不要卡在一个含糊的报错里。密钥轮换。如果应用密钥泄露要能在管理后台立即重置重置后旧密钥即刻失效所有用旧密钥的Token换取请求都会被拒绝。核心原则是把Token当作一种“短命凭证”来治理绝对不要把它当永久的登录态存起来。市面上很多“登录失效”“账号异常”的投诉大部分都是Token存储和刷新策略没做好导致的。4.3 用户取消授权/授权失败时你的App在做什么还有一类问题容易被忽视交互异常。用户可能在授权页点了“拒绝”也可能中途退出了授权页。授权失败时社交平台依然会把浏览器重定向回你的回调地址只不过地址栏没有code而是带了一个erroraccess_denied参数。如果你没有处理好这个分支用户就会看到“跳转后一片空白”或者“莫名其妙报错”。正确的做法是回调接口第一步判断有没有error有的话引导用户回到登录页并给一个温和的提示如果能识别出具体的错误码比如invalid_scope把开发信息记录到日志里方便排查是不是自己申请权限时配错了。这一步做得好联调期能省下一大半沟通成本因为很多“第三方登录打不开”的工单本质上是错误分支没写好。5. 作为普通用户怎么识别一次“正经”的第三方登录道理都讲完了最后落到你日常使用的那一刻。不是所有“社交账号登录”都值得你毫无保留地信任尤其是碰到山寨页面钓鱼的时候。这里给一条普通用户也能用的安全自查清单。5.1 五步识别山寨授权页第一看地址栏域名。社交平台的授权页一定出现在它自己的官方域名下面不会是电商App的域名更不会是一些乱七八糟的中间跳转域名。如果你看到域名明显不对比如auth-s0cial.com这种拿数字0混淆字母o的基本可以断定是钓鱼。第二看协议。正规授权页必然是全链路的HTTPS浏览器地址栏有锁形图标。但如果一个“授权页”连HTTPS都没有或者锁形图标被浏览器标红警告千万停手。第三看页面内容。正规授权页会明确告知你哪个应用、申请哪些权限、将用于什么目的。如果页面语焉不详只有一个孤零零的“登录”按钮拒绝理由很充足。第四看输入框的位置。你输入密码的页面必须是你自己的社交平台官方页面而不是某个嵌套在不知名页面里的iframe弹窗。理论上正规授权页的密码输入框和页面整体都是同一个域名不会出现“页面是A域名、密码框却指到B域名”的情况。第五看登录之后能否在平台侧查看到这次授权记录。现在主流社交平台都在设置里提供“授权管理/第三方应用”入口。你授权完之后去里面看看如果明明授权过却查不到记录或者记录里的权限和你当初看到的不一致立即撤销并考虑改密码。5.2 授权页上的那些字不是白写的权限提示怎么看授权页上写“申请获取你的昵称、头像”不是走形式它在OS层面就是scope的翻译。普通用户最需要瞪大眼睛的是权限范围本身如果只申请基础资料用于“登录并创建账号”这是正常的最小授权。如果申请读你的好友列表、发消息、修改个人资料你要想一想一个用社交账号登录的电商App为什么要给别人发送消息为什么要读好友列表除非它在做“邀请好友”等明确功能否则一律谨慎。如果一个App只登录用却申请了“获取你的手机号和实名信息”这类高敏权限你又不想给尽量拒绝。正常的登录流程里手机号绑定是平台自己的业务不该成搭着第三方登录的便车强行索要。我自己在判断一个应用值不值得授权时有一个习惯先把所有它申请的scope列出来一条条问“这和我接下来要做的事有没有关系”凡是风马牛不相及的直接撤回授权。5.3 一个典型的“钓鱼授权页”长什么样结合实战我拆一个相对高明的钓鱼骗局给你看它的过程是攻击者先在某个免费网页托管平台上搭建一个静态页面页面域名极其接近正规社交平台的授权页。攻击者在自己的App里把“社交账号登录”按钮的跳转地址指向这个钓鱼页面。你点了按钮弹出一个和正规授权页很像的页面上面写着“某某应用申请获取你的昵称头像”底下是一个登录框。你在钓鱼页面上输入了账号密码攻击者后台立刻把你的密码明文存下来了。你会发现这类骗局从头到尾没有任何“破解”它只是利用了你的疏忽——输入的页面不在官方域名下。所以识别方法本质上就一条任何需要你输入密码的页面都必须确定它是密码所属平台的官方页面。第三方应用永远不需要也不应该让你在一个外来页面里输入别的平台的密码。6. 再往前走一步OAuth管“授权”OpenID Connect管“你是谁”如果看到这里你已经理解了第三方登录的完整链路那离下一个阶段只有一步之遥OAuth其实并不是为“登录”而生的它最初是为了解决“授权第三方访问API”的问题。你真正用的“登录”实际上是在OAuth之上又叠了一层OpenID ConnectOIDC协议。6.1 授权和认证是两回事很多人容易混淆这两个概念授权Authorization决定第三方应用能访问哪些资源。OAuth管的是这个告诉系统“允许这个App看你的昵称头像”。认证Authentication证明“你就是你”。用户名加密码、短信验证码、生物识别都算认证手段。OAuth本身并不负责告诉你“用户身份是什么”它只负责传递授权凭证。所以你会看到一种微妙的情况OAuth授权成功之后第三方应用拿到Token但它仍不知道Token背后主人的唯一标识是怎么跨平台统一的。为了补上这一块OIDC在OAuth的框架里增加了一个JWT格式的id_token里面明确写了用户是谁、身份提供方是谁、签发给哪个客户端、何时过期。第三方应用拿到id_token之后才真正完成“认证”这一步。6.2 OIDC怎么把身份信息打包交给应用OIDC的核心是引入了一个叫id_token的东西它是用JSON Web TokenJWT格式签发的。一个JWT长这样下面是一个示意结构eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9 .eyJzdWIiOiI4NDI5MzQ3MSIsImF1ZCI6ImFwcF84ZjNhMmIiLCJpc3MiOiJodHRwczovL2F1dGguc29jaWFsLmNvbSIsImV4cCI6MTcwOTg4Mjc2MH0 .signature把它解码后的载荷大致是{ sub: 84293471, aud: app_8f3a2b, iss: https://auth.social.com, exp: 1709882760 }这里的sub是用户在社交平台的唯一标识aud是接收这个Token的客户端IDiss是签发方地址exp是过期时间。第三方应用验证签名合法、aud是自己的ID、未过期之后就可以声明“这个用户确实是社交平台认证过的那位”然后绑定本地账号。6.3 一次“全套登录”里OAuth和OIDC分别在哪一步出场实际上你在登录时走的完整流程是OAuth负责跑通授权和Token这层管道OIDC在Token这一层额外携带了身份信息。社交平台返回的响应里既包含access_tokenOAuth的凭证也包含id_tokenOIDC的身份凭证第三方应用会用id_token完成登录态的建立用access_token去拉取头像昵称等详细资料。两者分工合起来就是你看到的那一个按钮的完整智力支撑。这块内容算是一个“进阶彩蛋”日常接触第三方登录的开发者和做安全研究的读者可以深入进去学一学。理解OIDC也就理解了很多企业做单点登录SSO时为什么长得和第三方登录几乎一模一样——本质都是同一套联邦身份体系在发挥价值。最后说点我的个人体会。做了这些年授权对接最深的感受是OAuth这套设计的精妙之处不在“省事”而在于把“信任”和“权限”彻底分离了。用户的密码属于平台授权的结果属于用户自己应用只收到一张可以随时吊销的限时访客卡。这种边界感恰恰是当今互联网账号体系里最值得善待的东西。如果你是在自己的项目里接入第三方登录我的建议是先花一个小时把授权码模式的完整时序画清楚再去翻平台的接入文档你会发现文档里那些参数不再是孤立的而是这张访客卡从签发到校验的每一步轨迹。真正跑通一遍之后以后再遇到类似问题你大概会和我一样第一反应不再是“怎么连上”而是“这个Token能干什么、什么时候失效、该在哪里存、用户能不能随时收回”。把这些边界管住了第三方登录才能既好用又让人放心。
阅读完成 · 觉得有帮助?
咨询建站