前言
我们今天看到某网站可以“使用 GitHub(或微信、微博)账号登录”,无需在该网站重新注册一个账号。这就是第三方登录,而背后的原理是一个名为 OAuth 2.0 的开放授权协议。
今天,我们就以“使用 GitHub 登录”为例,深入剖析 OAuth 2.0 的工作流程,理解 state
、code
和 token
这些关键概念的作用,看看它是如何安全地帮助我们授权,同时又保护我们的密码不被泄露的。
一、什么是 OAuth 2.0?
OAuth 2.0 是一个关于授权(Authorization) 的开放标准。注意,它不是认证(Authentication),这两者有细微,但重要的区别:
- 认证(Authentication):是“证明你是谁”的过程,比如输入密码验证你是账号的主人。(登录)
- 授权(Authorization):是“授予你特定权限”的过程,比如允许“某音乐App”访问你的微信头像和昵称。(授权)
OAuth 2.0 通过引入一个授权层(Authorization Layer),将“客户端”(A 网站)和“资源所有者”(你)分开。你无需提供密码,而是通过一个访问令牌(Access Token) 来授权,这个令牌有明确的权限范围和有效期,大大提升了安全性。
在 OAuth 2.0 的流程中,主要涉及四个角色:
- 资源所有者 (Resource Owner):就是你,用户。
- 客户端 (Client):就是想要使用你数据的应用,比如那个想用 GitHub 登录的 A 网站。
- 授权服务器 (Authorization Server):GitHub 专门负责处理登录和授权同意环节的服务器。用户在这里认证,并批准客户端的授权请求。
- 资源服务器 (Resource Server):GitHub 存放用户资源的服务器(如用户信息、仓库信息等)。它接收 Access Token 并返回受保护的资源。
对于 GitHub 来说,授权服务器和资源服务器通常是同一家,但在协议上是分开的概念。
二、OAuth 2.0 授权码流程(Authorization Code Flow)详解
“使用 GitHub 登录”采用的是 OAuth 2.0 中最安全、最常用的 授权码模式(Authorization Code Grant)。这个模式的核心是先拿到一个一次性的授权码(Code),再用这个码去换访问令牌(Access Token),令牌才是访问资源的钥匙。
为了让整个过程更加清晰,我们结合下面的流程图来逐步解析:
下面是每个步骤的详细说明:
步骤 1:用户点击“使用 GitHub 登录”
你在 A 网站点击这个按钮,A 网站(客户端)的登录流程就开始了。
步骤 2:重定向到授权服务器
A 网站会将你的浏览器重定向到 GitHub 的授权服务器的特定地址(如 https://github.com/login/oauth/authorize
),并附带一系列查询参数,这些参数至关重要:
-
client_id
: 必需。这是 A 网站在 GitHub 上注册 OAuth App 时获得的身份标识。GitHub 靠这个 ID 知道是哪个应用在请求授权。 -
redirect_uri
: 可选但强烈建议。这是授权成功后,GitHub 应该将用户送回哪个地址(A 网站的回调地址)。必须在 GitHub OAuth App 设置中预先配置好,否则会被拒绝,这是一个重要的安全措施。 -
scope
: 可选。定义了你希望授予的权限范围。例如user:email
表示只获取读用户邮箱的权限,read:user
是获取读用户信息的权限。多个 scope 用空格分隔。 -
state
: 强烈推荐。一个随机生成的、不可猜测的字符串。它是防止CSRF(跨站请求伪造)攻击的核心。A 网站生成这个字符串并暂时存起来(比如存在 Session 里),然后发送给 GitHub。当 GitHub 跳转回来时,会原封不动地带回这个state
。A 网站需要验证回来的state
和之前发送的是否一致,以此证明这个回调请求确实来源于刚才的授权流程,而不是恶意攻击者伪造的。
一个完整的请求 URL 可能长这样:
https://github.com/login/oauth/authorize?client_id=abc123&redirect_uri=https://a-site.com/callback&scope=user:email&state=xyzABC123randomString
步骤 3:用户认证与授权
此时,你离开了 A 网站,来到了完全由 GitHub 控制的页面。如果你没有登录 GitHub,会先要求你输入 GitHub 的账号密码(注意:密码是输入给 GitHub 的,A 网站完全看不到!)。登录成功后,GitHub 会向你展示一个授权请求页面,列出 A 网站请求的权限(由 scope
定义),并询问你是否批准。
步骤 4:颁发授权码(Code)
如果你点击了“Authorize”(批准),GitHub 授权服务器就会将你的浏览器重定向回步骤 2 中指定的 redirect_uri
,并在 URL 后附加两个重要参数:
-
code
: 授权码。这是一个一次性的、有效期很短(通常几分钟) 的代码。它本身不代表任何权限,只是一个用来交换 Token 的凭证。 -
state
: GitHub 会将之前 A 网站发来的state
原样返回。
此时,浏览器的地址栏可能是:https://a-site.com/callback?code=xyz789&state=xyzABC123randomString
步骤 5:用授权码换访问令牌(Access Token)
这是最关键的一步,并且是在 A 网站的后端服务器与 GitHub 授权服务器的后端之间安全地进行的,用户的浏览器不参与。 A 网站的后端接收到回调请求后,会做以下几件事:
-
验证 State:比对收到的
state
参数和之前在 Session 中存储的值是否一致,以防止 CSRF 攻击。一致后才继续后续流程。 -
发送请求换取 Token:A 网站的后端向 GitHub 的令牌端点(如
https://github.com/login/oauth/access_token
)发起一个 POST 请求(而非跳转),并携带以下信息:-
client_id
: A 网站的 ID。 -
client_secret
: 这是 A 网站的密码,在注册 OAuth App 时获得。这个参数必须保密,绝不能在前端代码中泄露! 这也是为什么换取 Token 必须在后端进行。 -
code
: 刚刚从回调 URL 中拿到的一次性授权码。 -
redirect_uri
: 必须与请求code
时使用的redirect_uri
完全一致。
-
GitHub 授权服务器收到请求后,会验证 client_id
, client_secret
, code
和 redirect_uri
是否全部有效且匹配。如果一切正常,它会返回一个 JSON 响应,其中最重要的就是:
-
access_token
: 访问令牌。这就是我们最终想要的“万能钥匙”。A 网站的后端现在可以用这个令牌去访问 GitHub 资源服务器上你授权范围内的资源了。 - (可选)
refresh_token
: 刷新令牌。用于在access_token
过期后,获取新的access_token
,而无需用户再次点击授权。
步骤 6:使用访问令牌获取资源
现在,A 网站的后端就可以拿着这把“钥匙”(Access Token)去向 GitHub 的资源服务器(API)请求你的信息了。例如,它想获取你的公开信息,可以发起一个 API 请求:
GET https://api.github.com/user
并在请求头中加入:Authorization: token gho_your_access_token_here
GitHub 资源服务器会验证 Token 的有效性和权限范围,如果通过,就会返回你的用户信息(ID、登录名、头像等)。
步骤 7:完成登录
A 网站的后端收到你的 GitHub 用户信息后,会用它来识别你的身份。
- 如果这是你第一次通过 GitHub 登录,A 网站通常会使用你的 GitHub ID 和用户名等信息,为你自动创建一个本地账号并直接登录。
- 如果你之前已经关联过,则直接登录你已有的账号。
最后,A 网站会返回一个它自己网站的登录会话(Session 或它自己的 JWT Token)给你的浏览器,从而完成整个“第三方登录”流程。
三、除了授权码模式,还有别的模式吗?
有的,授权码模式(Authorization Code Grant)是 OAuth 2.0 核心规范(RFC 6749)中定义的四种标准授权模式之一。
你可以把 OAuth 2.0 框架想象成一个“工具箱”,里面提供了不同的“工具”(即授权模式)来应对不同的场景,而授权码模式是最初设计中最核心、最安全的模式。
授权模式 (Grant Type) | 适用场景 | 典型例子 |
---|---|---|
授权码模式 (Authorization Code) | 最常用、最安全的模式,适用于有后端服务器的 Web 应用。令牌不会暴露给浏览器或用户。 | 任何网站上的“用XX登录”功能。 |
隐式模式 (Implicit) | (已过时) 旧式用于纯前端/单页面应用(SPA)的模式,令牌直接返回给浏览器。因安全隐患已被 PKCE 扩展取代。 | 旧版 JS 应用。 |
密码模式 (Resource Owner Password Credentials) | 用户直接将用户名和密码交给客户端应用。非常不推荐,仅用于受信任的高权限应用(如官方客户端)。 | 自家公司出的移动App。 |
客户端凭证模式 (Client Credentials) | 用于机器对机器的认证,客户端代表自己而不是用户去获取资源。 | 一个后台服务调用另一个API获取数据,与具体用户无关。 |
四、配置参数 OAUTH_GENERIC
有哪些呢?
OAUTH_GENERIC
这个名称通常出现在一些支持通用 OAuth 2.0 提供商的软件或平台中(例如 Portainer, Home Assistant, Grafana, Jenkins 等)。因为世界上有成千上万个服务实现了 OAuth 2.0,这些软件不可能为每一个都做预配置。
因此,它们提供了一个“通用”或“自定义”的配置选项,让你可以手动填入任何兼容 OAuth 2.0 的服务的参数。
以下是 OAUTH_GENERIC
或类似自定义配置中常见的核心参数:
参数名称 | 说明 | 举例/如何获取 |
---|---|---|
Provider Name | 给你的这个OAuth配置起个名字,用于标识。 |
My Company OAuth , Internal GitLab
|
Client ID (client_id ) |
必需。在授权服务器注册应用时获得的公开标识。 | a1b2c3d4e5f6g7h8i9j0 |
Client Secret (client_secret ) |
必需。在授权服务器注册应用时获得的密钥,必须保密。 | a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 |
Authorization URL | 必需。授权端点的URL,用户在这里登录和授权。 | https://sso.mycompany.com/oauth/authorize |
Token URL | 必需。令牌端点的URL,应用后端用code来这里换token。 | https://sso.mycompany.com/oauth/token |
User Information URL | (通常必需)。获取用户身份信息的API端点。这是 OpenID Connect 的概念,但通用OAuth也支持。 | https://sso.mycompany.com/oauth/userinfo |
Scope (scope ) |
可选。定义请求的权限范围。对于登录,通常需要能识别用户的scope,如 openid profile email 。 |
五、授权码模式的精妙之处总结
- 密码永不泄露:用户始终只在 GitHub 的页面上输入密码,A 网站无法接触到。
-
权限最小化:A 网站只能获取你明确授权(
scope
)的信息,而不能为所欲为。 -
核心安全措施:
-
state
参数:防御 CSRF 攻击,是客户端必须实现的安全检查。 -
授权码(Code)交换令牌(Token):关键的令牌(Token)是通过后端安全通道交换的,不会通过浏览器地址栏暴露。即使有人拦截到了
code
,因为没有client_secret
,他也无法换到access_token
。 -
client_secret
的保密:这是客户端身份的关键证明,必须保存在服务器端,绝不能写在移动应用或前端 JavaScript 代码中。
-