OAuth 2.0 / 单点登录:互联网的“信任借条”系统
当你用微信登录一个小程序时,背后发生了什么?
—— 一次关于权限、边界与信任的深度拆解
目录:我们将走过这条路
- 序幕:一个尴尬的早晨
- 第一章:OAuth 2.0——不是登录协议
- 第二章:单点登录(SSO)——一把钥匙开所有门
- 第三章:当OAuth遇到SSO
- 第四章:四种“借钥匙”的姿势
- 第五章:一次完整的微信登录之旅
- 第六章:那些让人头秃的安全细节
- 终章:如何为你的系统选择
序幕:一个尴尬的早晨
场景还原
想象这个早晨:
- 你发现一个新出的“猫咪健康管理”小程序,想试试。
- 小程序让你注册。“又要填邮箱、密码、验证码…”你皱眉。
- 突然看到:“使用微信登录”按钮。
- 点击 → 跳转到微信 → 确认授权 → 回到小程序,已自动登录。
你舒了口气,继续撸猫。但作为开发者,我心里想的是:
“微信凭什么相信这个小程序?小程序又怎么知道我是谁?我的微信密码有没有泄露?如果我不想让它访问我的微信好友列表怎么办?”
这些问题的答案,就是OAuth 2.0和单点登录(SSO)。
核心比喻:图书馆与访客卡
让我们建立一个贯穿全文的比喻:
- 你 = 图书馆读者(用户)
- 微信 = 图书馆总馆(身份提供商,Identity Provider,IdP)
- 猫咪健康小程序 = 分馆或合作书店(服务提供商,Service Provider,SP)
- 你的微信账号密码 = 总馆借书证+密码(主凭证)
- OAuth Token = 总馆开的“访客推荐信”(临时授权)
- 单点登录 = 一张借书证在所有分馆通用
关键洞察:
OAuth解决的是**“分馆如何在不拿到你借书证密码的情况下,确认你是总馆的合法读者”。
SSO解决的是“为什么我用一张借书证就能进出所有分馆”**。
它们经常一起出现,但本质上是两个不同的问题。
第一章:OAuth 2.0——不是登录协议
这是最大的误解!我见过无数文章说“OAuth是一种登录方式”,这不够准确。
OAuth 2.0的真实身份
OAuth 2.0是一个授权框架(Authorization Framework),核心解决:
“如何让一个应用(第三方)在有限范围、有限时间内,代表用户访问另一个应用(资源服务器)的资源,而无需拿到用户的密码。”
用图书馆比喻:
总馆(微信)给分馆(小程序)开一张纸条:“持此纸条者可借阅3本养生类书籍,有效期7天”。
分馆凭纸条借书给你,但纸条上没写你的借书证密码。
OAuth 2.0的核心角色
| 角色 | 官方称呼 | 图书馆比喻 | 微信登录实例 |
|---|---|---|---|
| 资源所有者 | Resource Owner | 读者(你) | 微信用户 |
| 客户端 | Client | 分馆/合作书店 | 猫咪健康小程序 |
| 授权服务器 | Authorization Server | 总馆服务台 | 微信OAuth服务器 |
| 资源服务器 | Resource Server | 总馆藏书库 | 微信API(获取用户信息) |
为什么需要OAuth?三个血泪教训
- 密码泛滥问题2008年以前,很多网站会让你直接输入Gmail密码来“导入联系人”。结果:你的Gmail密码存在几十个小网站数据库里,任何一个被黑,全部遭殃。
- 权限失控问题一个小游戏要求“访问你的全部Gmail邮件”——它真的需要吗?但当时没有“部分授权”概念,要么全给,要么不用。
- 难以撤销问题
给了权限后,想收回怎么办?只能改密码——但这样所有其他服务也失效了。
OAuth 2.0(2012年成为标准)就是为了解决这些问题诞生的。它的前身OAuth 1.0太复杂,就像第一代借书系统要盖五个章,OAuth 2.0简化为“扫码授权”。
第二章:单点登录(SSO)——一把钥匙开所有门
SSO要解决的问题
想象公司内部:
- 邮箱系统:
mail.company.com(要登录) - 报销系统:
expense.company.com(要登录) - 文档系统:
docs.company.com(要登录) - 人力系统:
hr.company.com(要登录)
员工疯了:“我每天要登录四次?密码还要求不一样!”
SSO(Single Sign-On) 就是:一次登录,到处通行。
SSO的核心机制
SSO的实现方式
常见的有三种:
1. 基于Cookie的共享Session
最简单的内网SSO:
# 所有子域名共享顶级域名Cookie
server {
server_name mail.company.com;
location / {
# 设置Cookie domain为 .company.com
add_header Set-Cookie "session_id=abc123; Domain=.company.com; Path=/";
}
}
# expense.company.com 能读到同一个Cookie
2. 中央认证服务(CAS协议)
经典的企业SSO方案:
- 用户访问应用 → 重定向到CAS服务器
- 登录后,CAS发Ticket(临时票证)
- 应用用Ticket向CAS验证
- CAS返回用户信息
3. 基于SAML(安全断言标记语言)
企业级标准,XML-based,配置复杂但功能强大。
关键点:SSO关注的是认证(Authentication),即“你是谁”。
OAuth关注的是授权(Authorization),即“你能做什么”。
第三章:当OAuth遇到SSO
这是最精彩的部分!现实世界中,它们经常联手。
现代SSO的常见实现:OAuth 2.0 + OpenID Connect
OpenID Connect(OIDC) 是建立在OAuth 2.0之上的认证层。你可以理解为:
- OAuth 2.0 = 授权框架(我能做什么)
- OIDC = OAuth 2.0 + 用户身份信息(我是谁)
OIDC的核心扩展
OAuth 2.0原本不关心用户是谁,只关心权限。OIDC加了:
- ID Token:一个JWT,包含用户身份信息(姓名、ID等)
- UserInfo Endpoint:专门获取用户信息的API
- 标准声明字段:
sub(用户ID)、email、name等
// OIDC的ID Token示例(JWT格式)
{
"iss": "https://accounts.google.com", // 签发者
"sub": "110169484474386276334", // 用户唯一ID(关键!)
"aud": "1234987819200.apps.googleusercontent.com", // 受众(客户端ID)
"iat": 1516239022,
"exp": 1516242622,
"email": "user@gmail.com",
"name": "张三",
"picture": "https://lh3.googleusercontent.com/..."
}
为什么这个组合赢了?
- OAuth 2.0成熟:大家都已实现
- JWT标准化:ID Token用JWT,易解析验证
- 移动端友好:比SAML的XML简单太多
- 生态系统:所有大厂(Google、微信、GitHub)都支持
今天你说“用OAuth做SSO”,通常指的是OAuth 2.0 + OIDC方案。
第四章:四种“借钥匙”的姿势
OAuth 2.0定义了四种授权流程(Grant Type),对应不同场景。这是理解OAuth的关键。
1. 授权码模式(Authorization Code)——最安全、最常用
场景:有后端的Web应用(如猫咪健康小程序的后台服务)
步骤:
1. 用户点击“微信登录”
2. 跳转到微信授权页面(带client_id、redirect_uri等参数)
3. 用户确认授权
4. 微信重定向回小程序的redirect_uri,带上**code**(授权码)
5. 小程序后端用code + client_secret向微信换access_token
6. 微信返回access_token
7. 小程序用access_token调用微信API获取用户信息
为什么安全:
client_secret存在后端,不暴露给浏览器code是短期凭证,即使被拦截,没有client_secret也换不了token- 适合保密的客户端(有后端服务器)
2. 隐式授权模式(Implicit)——给纯前端应用
场景:单页面应用(SPA),没有后端或后端不参与认证
步骤:
1. 用户点击登录
2. 跳转到授权页面
3. 用户确认
4. 重定向回应用,**access_token直接出现在URL片段#后面**
5. JavaScript从URL提取token使用
为什么设计成这样:
SPA没有后端,无法安全存储 client_secret,所以跳过“用code换token”的步骤,直接给token。
但有问题:Token在URL中可能被泄露(浏览器历史、Referer头等)。
现代推荐:用授权码模式+PKCE(下面讲)代替隐式模式。
3. 密码模式(Resource Owner Password Credentials)——高度信任时用
场景:自家公司的移动端App登录自家服务
用户直接提供用户名密码给客户端
客户端用这些凭证直接换token
看起来简单,但有风险:客户端能拿到用户明文密码!只适用于绝对信任的客户端,比如:
- 微信官方App登录微信服务
- 公司官方App登录公司内部系统
绝不用于:第三方应用。
4. 客户端凭证模式(Client Credentials)——机器对机器
场景:后台服务访问API,没有用户参与
客户端用client_id + client_secret直接换token
这个token代表客户端自身,不是任何用户
典型用途:
- 定时任务调用API
- 微服务间通信
- 数据分析后台拉取数据
特别嘉宾:PKCE(Proof Key for Code Exchange)
解决的问题:纯前端应用(移动App、SPA)用授权码模式时,如何防止授权码被拦截冒用。
机制(比喻版):
- 你去借书前,先自己想一个暗号(code_verifier),并计算它的“指纹”(code_challenge)。
- 你告诉图书馆:“我要借书,这是我的指纹”。
- 图书馆给你一张借书券(授权码)。
- 你回来时说:“我要用这张券借书,暗号是XXX”。
- 图书馆计算暗号的指纹,比对之前记录的指纹,一致才给书。
技术实现:
// 前端生成
const codeVerifier = generateRandomString(); // 原始暗号
const codeChallenge = sha256(codeVerifier); // 指纹
// 跳转授权时带上challenge
redirectToAuth(`?code_challenge=${codeChallenge}&...`);
// 换token时带上verifier
exchangeCode(`?code=授权码&code_verifier=${codeVerifier}`);
现代最佳实践:
- 原生App → 授权码 + PKCE
- 单页面应用 → 授权码 + PKCE
- 传统Web应用 → 授权码
第五章:一次完整的微信登录之旅
让我们追踪一次真实的“微信登录”流程,把前面所有概念串联起来。
角色设定
- 用户:想用“猫咪健康”小程序
- 微信:身份提供商(IdP)
- 猫咪健康小程序:第三方应用(RP,Relying Party)
前置准备:小程序注册
- 开发者去微信开放平台注册“猫咪健康”小程序。
- 获得
AppID(OAuth中的client_id)和AppSecret(client_secret)。 - 配置授权回调域名(redirect_uri),如
https://cat-health.com/oauth/callback。
第一幕:前端发起授权
用户在小程序点击“微信登录”:
// 前端构造授权URL
const authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?
appid=APPID&
redirect_uri=${encodeURIComponent('https://cat-health.com/oauth/callback')}&
response_type=code&
scope=snsapi_userinfo& // 请求获取用户信息权限
state=random_string`; // 防CSRF
// 跳转(微信内嵌浏览器直接跳,外部浏览器显示二维码)
window.location.href = authUrl;
关键参数:
scope=snsapi_userinfo:请求获取用户头像、昵称等state:随机字符串,防止CSRF攻击(回调时校验)
第二幕:用户授权
- 跳转到微信授权页面(可能在微信内打开,也可能显示二维码)。
- 页面显示:“猫咪健康申请获得你的以下信息:昵称、头像、地区”。
- 用户点击“允许”。
- 关键心理:用户知道没在输微信密码,授权的是有限信息。
第三幕:微信回调
微信重定向到小程序的回调地址:
https://cat-health.com/oauth/callback?
code=CODE_HERE&
state=SAME_STATE_AS_BEFORE
第四幕:后端换Token
前端把code传给后端,后端处理:
def exchange_code_for_token(code):
# 安全提醒:这段代码在后端运行!
params = {
'appid': APP_ID,
'secret': APP_SECRET, # 关键!前端不知道这个
'code': code,
'grant_type': 'authorization_code'
}
# 请求微信服务器
resp = requests.post(
'https://api.weixin.qq.com/sns/oauth2/access_token',
params=params
)
data = resp.json()
# 返回:{"access_token": "...", "expires_in": 7200,
# "refresh_token": "...", "openid": "USER_UNIQUE_ID"}
return data
获得两个关键东西:
access_token:访问用户资源的凭证(2小时过期)openid:用户在此小程序内的唯一标识(不变)
第五幕:获取用户信息
def get_user_info(access_token, openid):
resp = requests.get(
'https://api.weixin.qq.com/sns/userinfo',
params={
'access_token': access_token,
'openid': openid,
'lang': 'zh_CN'
}
)
user_data = resp.json()
# {"openid": "...", "nickname": "猫奴张三",
# "sex": 1, "province": "北京",
# "city": "北京", "country": "中国",
# "headimgurl": "https://..."}
return user_data
第六幕:建立自己的会话
重要:小程序不会一直用微信的access_token,而是:
- 用openid在自己数据库创建/查找用户记录
- 生成自己的session或JWT token
- 返回给前端,后续用这个token访问自己的API
def create_local_session(wechat_user_data):
# 1. 查找或创建本地用户
user = User.find_or_create(
openid=wechat_user_data['openid'],
defaults={
'nickname': wechat_user_data['nickname'],
'avatar': wechat_user_data['headimgurl']
}
)
# 2. 创建本地会话(比如JWT)
local_token = jwt.encode({
'user_id': user.id,
'exp': datetime.now() + timedelta(days=7)
}, LOCAL_SECRET_KEY)
# 3. 返回给前端
return {
'token': local_token,
'user': {
'id': user.id,
'nickname': user.nickname
}
}
全程总结
用户 → 小程序 → 微信授权页 → 用户同意 → 微信返回code
→ 小程序后端用code+secret换token → 用token获取用户信息
→ 用openid关联本地用户 → 创建本地会话 → 用户登录完成
用户感知:点一下“允许”,就登录了。
实际发生:6个步骤,3次服务器间通信,全程没碰微信密码。
第六章:那些让人头秃的安全细节
OAuth/SSO设计精妙,但实现时容易踩坑。我踩过,希望你避开。
安全威胁1:CSRF攻击(授权阶段)
场景:
- 攻击者构造恶意链接:
https://idp.com/authorize?client_id=attacker&redirect_uri=attack.com - 诱骗已登录用户点击
- 用户不知不觉为攻击者的应用授权
防御:用 state参数
- 发起授权时生成随机state,存session
- 回调时验证state是否匹配
- 必须做!我第一个OAuth实现忘了这个,被安全测试打爆。
安全威胁2:授权码拦截攻击
场景:纯前端应用(SPA),授权码在URL中,可能被恶意JS读取。
防御:PKCE(前面讲了)
- 即使code被偷,没有code_verifier也换不了token
- 现代SPA/移动App强制要求
安全威胁3:重定向URI验证不严
场景:
- 攻击者注册合法应用,redirect_uri设为
https://victim.com/callback - 诱导用户授权
- 授权码送到victim.com,攻击者可能通过其他漏洞获取
防御:
- IdP必须严格验证redirect_uri与注册时完全匹配
- 可以注册多个URI,但不能接受任意URI
安全威胁4:Token泄露
场景:access_token被泄露(日志、错误信息、客户端存储不当)。
防御:
- 短期token(如2小时)
- refresh_token机制(可撤销)
- token绑定客户端(jti声明、client_id验证)
安全威胁5:IdP钓鱼
场景:恶意应用模仿真实IdP界面,骗取用户授权。
防御:
- 用户教育(检查域名、证书)
- OAuth 2.1草案要求PKCE对所有客户端
- 使用知名IdP(用户熟悉其界面)
最佳实践清单
必须做的:
- 所有流量HTTPS
- 验证state参数(防CSRF)
- 严格验证redirect_uri
- 使用PKCE(对公开客户端)
- token短期有效 + 可刷新
应该做的:
- 记录所有授权日志
- 提供授权撤销界面
- 支持细粒度scope(不要总是请求全部权限)
- 定期轮换client_secret
不要做的:
- 把token存在localStorage(除非是短期且SPA无其他选择)
- 将access_token传给不可信的前端代码
- 使用隐式授权模式(除非遗留系统)
- 信任未经验证的redirect_uri参数
第七章:实战中的设计抉择
当你要为自己的系统设计认证方案时,问这几个问题:
问题1:我是资源提供方(IdP)还是资源消费方(RP)?
情况A:我要让用户用微信/Google登录我的应用
→ 实现OAuth Client,选择合适的授权流程。
情况B:我要提供API让第三方接入
→ 实现OAuth Authorization Server,决定支持哪些grant_type。
问题2:我的客户端类型?
// 决策树
if (有后端服务器 && 可安全存储secret) {
return "授权码模式";
} else if (纯前端SPA || 移动App) {
return "授权码模式 + PKCE";
} else if (完全信任的自家客户端) {
return "密码模式(谨慎)";
} else if (后台服务无用户) {
return "客户端凭证模式";
}
问题3:需要用户身份信息吗?
- 只需要授权API访问 → 纯OAuth 2.0
- 需要知道用户是谁 → OAuth 2.0 + OIDC
问题4:内部系统还是面向公众?
- 内部系统SSO:考虑SAML(企业集成)或OIDC
- 面向消费者:支持社交登录(微信、Google、GitHub OAuth)
- B2B场景:可能两者都需要
终章:如何为你的系统选择
简化决策表
| 你的场景 | 推荐方案 | 理由 |
|---|---|---|
| 企业内部多个系统 | OIDC-based SSO | 一次登录,标准现代 |
| 消费者Web应用 | 社交登录(微信/Google OAuth) | 降低注册门槛 |
| 移动应用 | OAuth 2.0 + PKCE | 移动端安全最佳实践 |
| 微服务架构 | OAuth 2.0客户端凭证 + JWT | 服务间认证 |
| 对外提供API | OAuth 2.0授权码模式 | 第三方接入标准 |
| 快速原型 | 直接用Auth0/Cognito等身份云 | 别重复造轮子 |
我的亲身建议
-
不要从零实现OAuth服务器除非你是大厂或有特殊需求,用现成的:
- 开源:Keycloak、ORY Hydra
- 云服务:Auth0、AWS Cognito、Okta
我第一个月自己实现OAuth服务器,第二个月发现边界情况巨多,第三个月换Keycloak。
-
理解原理,但使用库
# 好 from authlib.integrations.flask_client import OAuth oauth = OAuth(app) oauth.register('wechat', client_id=..., client_secret=...) # 不好 def parse_oauth_response(request): # 自己解析所有参数、验证state、处理错误... # 容易遗漏安全细节 -
设计时考虑退出策略如果未来要换IdP(比如从自建换到Auth0),抽象认证层:
# 抽象接口 class AuthProvider: def get_login_url(self) -> str: ... def exchange_code(self, code: str) -> UserInfo: ... def logout(self, token: str) -> bool: ... # 具体实现 class WeChatAuth(AuthProvider): ... class GoogleAuth(AuthProvider): ... class CustomOIDC(AuthProvider): ... -
永远站在用户角度思考
- 权限请求要合理(scope最小化)
- 提供清晰的“为什么需要这个权限”解释
- 让用户容易查看和管理已授权应用
- 一键撤销所有授权
最后的核心洞察
OAuth和SSO的精髓,其实在技术之外:
它们建立了一种“可控的信任”关系。
在现实世界,你不会把家门钥匙给快递员,但会允许他进小区(门禁卡)、放快递在驿站(代收点)。OAuth就是数字世界的“临时通行证”系统。
而SSO,就像公司的工牌——一张卡能打开你有权限的所有门,但一旦丢失,安保中心能立即冻结它。
好的身份系统,应该是:
- 对用户透明:流程顺畅,权限清晰
- 对开发者友好:文档完整,SDK好用
- 对安全严谨:防御深度,监控完备
下一步探索方向
如果你已经理解这些,可以继续深入:
- OAuth 2.1:当前草案,合并了安全最佳实践(PKCE必须、隐式模式移除等)
- OIDC进阶:动态客户端注册、会话管理、前端信道注销
- 企业场景:SAML与OIDC的桥接、SCIM用户同步协议
- 隐私增强:OAuth 2.0 for Browser-Based Apps(BRA)规范
- 硬件与物联网:设备流(Device Flow)授权
这就是OAuth和SSO的故事。从“为什么需要”到“如何工作”再到“如何用好”,我希望这篇文章让你看到了技术设计背后的人性思考。
毕竟,所有技术最终服务的都是人。