什么是 OAuth 2.0:不给密码也能授权的秘密
你肯定遇到过这种情况:打开一个新 App,它说"用微信登录",你点了之后跳转到微信,看到授权页面写着"该应用希望获取你的昵称和头像",你点击同意,然后就自动登录了。
整个过程你没有输入过密码,但这个 App 确实拿到了你的信息。
这就是 OAuth 2.0 在起作用。
它解决了一个听起来很矛盾的问题:怎么在不告诉对方密码的情况下,让对方访问你的东西?
为什么需要 OAuth 2.0
在 OAuth 2.0 出现之前,第三方应用想访问你的数据,只有一个办法:你把账号密码直接给它。
听起来就很不安全对吧?
确实,这种方式有几个大问题:
- 密码泄露给第三方,万一它被黑了,你的账号也完了
- 第三方拿到密码后能做任何事,包括改密码把你踢出去
- 你没办法只授权部分功能,要么全给,要么不给
- 想撤销授权?只能改密码,但这样会把所有第三方应用都踢下线
OAuth 2.0 就是为了解决这些问题而生的。
它引入了一个核心概念:用令牌代替密码。
OAuth 2.0 的核心思路
OAuth 2.0 的设计思路很简单:不直接给密码,而是给一个临时通行证(令牌)。
这个通行证有几个特点:
权限有限。比如只能读你的头像昵称,不能发朋友圈。
有效期短。通常几个小时到几天就过期,就算被盗影响也有限。
可以撤销。你随时可以在授权管理里取消某个 App 的访问,不影响其他应用。
无法修改密码。第三方拿着令牌只能干授权范围内的事,改不了你的密码。
整个授权过程涉及四个角色:
- 资源所有者(你) — 数据的主人
- 客户端(第三方 App) — 想要访问你数据的应用
- 授权服务器 — 负责验证身份和发放令牌,比如微信的授权服务
- 资源服务器 — 存储你数据的地方,比如微信的用户数据库
令牌是怎么拿到的
我们以最常见的"授权码模式"为例,看看一个完整的授权流程。
假设你要用微信登录一个购物 App:
第 1 步:App 发起授权请求
购物 App 构造一个授权 URL,里面包含它的身份信息(client_id)、需要的权限范围(scope)、以及回调地址(redirect_uri)。然后把你重定向到这个 URL。
https://weixin.com/oauth/authorize?
response_type=code&
client_id=shopping_app_123&
redirect_uri=https://shop.com/callback&
scope=userinfo&
state=random_string第 2 步:你在微信授权页面登录并同意
你被跳转到微信的授权页面,输入微信账号密码登录(注意,密码是给微信的,不是给购物 App 的),然后看到授权范围,比如"获取昵称和头像"。你点击同意。
第 3 步:微信返回授权码
微信验证你的身份后,生成一个临时的授权码(authorization code),然后把你重定向回购物 App 的回调地址,授权码附在 URL 参数里:
https://shop.com/callback?code=AUTH_CODE_123&state=random_string这个授权码只能用一次,而且有效期很短,通常只有 10 分钟。
第 4 步:App 用授权码换取令牌
购物 App 的后台服务器拿到授权码后,向微信的令牌接口发送请求,附上自己的 client_id 和 client_secret(密钥),以及刚拿到的授权码:
POST https://weixin.com/oauth/token
client_id=shopping_app_123
client_secret=app_secret_456
grant_type=authorization_code
code=AUTH_CODE_123
redirect_uri=https://shop.com/callback第 5 步:微信返回访问令牌
微信验证授权码和应用身份后,返回一个访问令牌(access_token)和一个刷新令牌(refresh_token):
{
"access_token": "ACCESS_TOKEN_ABC",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "REFRESH_TOKEN_XYZ",
"scope": "userinfo"
}第 6 步:App 用令牌请求你的数据
购物 App 拿着访问令牌,向微信的资源接口请求你的头像和昵称:
GET https://api.weixin.com/user/info
Authorization: Bearer ACCESS_TOKEN_ABC微信验证令牌有效后,返回你的数据。整个过程结束。
用 Go 实现 OAuth 2.0 客户端
下面是一个完整的 Go 代码示例,展示如何实现授权码模式:
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"golang.org/x/oauth2"
)
// 定义 OAuth 2.0 配置
var oauthConfig = &oauth2.Config{
ClientID: "your_client_id",
ClientSecret: "your_client_secret",
RedirectURL: "http://localhost:8080/callback",
Scopes: []string{"read_user", "read_email"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://provider.com/oauth/authorize",
TokenURL: "https://provider.com/oauth/token",
},
}
func main() {
// 路由:跳转到授权页面
http.HandleFunc("/login", handleLogin)
// 路由:处理授权回调
http.HandleFunc("/callback", handleCallback)
fmt.Println("服务运行在 http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
// 处理登录请求,重定向到授权页面
func handleLogin(w http.ResponseWriter, r *http.Request) {
// 生成一个随机 state 用于防止 CSRF 攻击
state := generateRandomState()
// 构造授权 URL
url := oauthConfig.AuthCodeURL(state, oauth2.AccessTypeOffline)
// 将 state 存储到 session(这里简化处理)
http.SetCookie(w, &http.Cookie{
Name: "oauth_state",
Value: state,
Expires: time.Now().Add(10 * time.Minute),
HttpOnly: true,
})
// 重定向到授权页面
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
// 处理授权回调
func handleCallback(w http.ResponseWriter, r *http.Request) {
// 验证 state 参数,防止 CSRF
stateCookie, err := r.Cookie("oauth_state")
if err != nil || stateCookie.Value != r.URL.Query().Get("state") {
http.Error(w, "state 参数不匹配", http.StatusBadRequest)
return
}
// 获取授权码
code := r.URL.Query().Get("code")
if code == "" {
http.Error(w, "授权码缺失", http.StatusBadRequest)
return
}
// 用授权码交换访问令牌
ctx := context.Background()
token, err := oauthConfig.Exchange(ctx, code)
if err != nil {
log.Printf("令牌交换失败: %v", err)
http.Error(w, "获取令牌失败", http.StatusInternalServerError)
return
}
// 使用令牌访问受保护的资源
userInfo, err := getUserInfo(ctx, token)
if err != nil {
log.Printf("获取用户信息失败: %v", err)
http.Error(w, "获取用户信息失败", http.StatusInternalServerError)
return
}
// 返回用户信息
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(userInfo)
}
// 使用访问令牌获取用户信息
func getUserInfo(ctx context.Context, token *oauth2.Token) (map[string]interface{}, error) {
client := oauthConfig.Client(ctx, token)
resp, err := client.Get("https://provider.com/api/user")
if err != nil {
return nil, err
}
defer resp.Body.Close()
var userInfo map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
return nil, err
}
return userInfo, nil
}
// 生成随机 state 字符串
func generateRandomState() string {
// 实际项目中应使用加密随机数生成器
return fmt.Sprintf("state_%d", time.Now().Unix())
}这段代码展示了完整的授权流程:生成授权 URL、处理回调、交换令牌、访问资源。实际项目中还需要处理令牌刷新、错误重试、安全存储等细节。
OAuth 2.0 的安全要点
虽然 OAuth 2.0 比直接给密码安全多了,但如果用不好还是会出问题。几个关键的安全实践:
必须使用 HTTPS
授权码、令牌在网络传输,如果用 HTTP 明文传输,中间人可以轻松截获。
生产环境必须全程 HTTPS。
验证 state 参数
授权请求和回调之间传递一个随机 state 参数,回调时必须验证是否一致。这能防止 CSRF 攻击,避免攻击者伪造回调。
令牌不要暴露在前端
访问令牌应该存在后端 session 里,不要放在浏览器的 localStorage 或 Cookie 中。前端只持有 session ID,令牌只在后端使用。
限制令牌权限范围
申请授权时,只要必需的最小权限(scope)。能用"只读用户信息"就不要申请"读写所有数据"。
设置合理的过期时间
访问令牌有效期不要太长,建议 1-2 小时。长期访问通过刷新令牌(refresh token)来实现,刷新令牌有效期可以设置几天到几个月。
做好令牌存储
令牌是敏感信息,存储时要加密。如果是移动端,存在系统提供的安全存储(iOS Keychain、Android Keystore)里。
常见问题
OAuth 2.0 和 OAuth 1.0 有什么区别?
OAuth 1.0 要求对每个请求做签名,实现复杂且容易出错。
OAuth 2.0 简化了流程,不再要求签名,而是依赖 HTTPS 保证传输安全。
可以说,OAuth 2.0 用 HTTPS 的安全性换取了实现的简单性。
访问令牌和刷新令牌有什么区别?
访问令牌(access token)用来访问资源,有效期短。刷新令牌(refresh token)用来获取新的访问令牌,有效期长。这样即使访问令牌泄露,影响也是有限的,而且不需要用户重新授权就能无缝续期。
OAuth 2.0 能做身份认证吗?
OAuth 2.0 是授权协议,不是认证协议。
它只告诉你"这个令牌有权限访问某些资源",但不保证"持有令牌的人是谁"。
如果要做身份认证(登录),应该用 OpenID Connect,它是基于 OAuth 2.0 的身份认证层。
授权码为什么要分两步交换?
授权码在浏览器前端传输(可能被劫持),但令牌在后端服务器之间交换(相对安全)。
而且授权码只能用一次,即使被截获也无法重放。这个两步设计大大降低了令牌泄露的风险。
令牌被盗了怎么办?
如果访问令牌泄露,第一时间调用令牌撤销接口(revoke endpoint)让它失效。
这也是为什么访问令牌要设置短过期时间——即使没来得及撤销,攻击者能利用的时间窗口也很短。
总结一下
OAuth 2.0 通过引入令牌机制,在不暴露密码的前提下,让用户可以安全地授权第三方应用访问自己的资源。
它的核心优势是:权限可控、风险隔离、可撤销授权。授权码模式通过两步交换,在安全性和易用性之间取得了很好的平衡,是目前最推荐的方式。
开发时记住几个要点:全程 HTTPS、验证 state、令牌后端存储、最小权限原则、设置合理过期时间。把这些做到位,OAuth 2.0 就能成为你应用安全的有力保障。
如果你在使用 OAuth 2.0 时遇到什么问题,或者对某个细节有疑问,欢迎评论区一起讨论!
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!
本文原文链接: https://fiveyoboy.com/articles/what-is-oauth-2-0/
备用原文链接: https://blog.fiveyoboy.com/articles/what-is-oauth-2-0/