OAuth 2.0是一种授权机制,用户通过授权服务器给第三方应用授权,以访问自己的资源(位于资源服务器)。相对于OAuth 1.0,OAuth 2.0简化了对客户端的验证,只保留了获取Authorization Code和Access Token的请求过程。
OpenID Connect是OAuth 2.0的一个扩展,结合OAuth 2.0的授权提供了用户的认证。
常见的认证方式
基于OAuth 2.0的认证/授权协议
| 协议 | 说明 |
|-----------------------------------------|--------------------------------------------|
| OpenID Connect | OAuth 2.0 + 身份认证(ID Token) |
| UMA (User-Managed Access) | OAuth 2.0 + 授权管理(资源所有者控制访问策略) |
| OAuth 2.0 Token Introspection | OAuth 2.0 + 令牌验证标准(RFC 7662) |
| OAuth 2.0 Dynamic Client Registration | OAuth 2.0 + 动态客户端注册(RFC 7591) |
| OAuth 2.0 Authorization Server Metadata | OAuth 2.0 + 服务发现(RFC 8414) |
非OAuth 2.0的认证协议
javascript
| 协议 | 说明 |
|-----------------------|----------------------|
| SAML 2.0 | XML格式,企业级 SSO |
| LDAP/Active Directory | 目录服务认证 |
| Kerberos | 网络认证协议 |
| OpenID 1.0/2.0 | OAuth 2.0前身(已废弃) |
常见的Token格式
- JWT(JSON Web Token)
- Reference Token (随机字符串)
- SAML Token(XML 格式)
- MAC Token(OAuth 1.0专用,已废弃)
结合Authorization Code + PKCE的OpenID Connect的认证与授权
PKCE (Proof Key for Code Exchange),通过 code_verifier 和 code_challenge 确保只有原始客户端能获得Access Token。
客户端准备
javascript
// 客户端生成 code_verifier 和 code_challenge
const code_verifier = generateRandomString(43); // 43-128 字符
const code_challenge = base64UrlEncode(sha256(code_verifier));
// 保存 code_verifier(sessionStorage 或内存)
sessionStorage.setItem("code_verifier", code_verifier);
用户点击登录
客户端向认证/授权服务器发出HTTP请求如下:
javascript
GET /authorize HTTP/1.1
Host: auth.server.com
response_type=code&
client_id=s6BhdRkqt3&
redirect_uri=https://client.example.com/callback&
scope=openid profile email&
state=af0ifjsldkj&
code_challenge=dGhpcyBpcyBhIGNoYWxsZW5nZQ&
code_challenge_method=S256
认证/授权服务器对用户进行认证/授权
- 认证/授权服务器显示登录页面
- 用户输入用户名/密码
- 认证/授权服务器验证凭证
- 认证/授权服务器显示授权页面(请求用户授权)
- 用户点击 "同意"授权
认证/授权服务器生成Authorization Code,并缓存code_challenge与Authorization Code的关联。
认证/授权服务器重定向
发出HTTP请求如下:
javascript
HTTP/1.1 302 Found
Location: https://client.example.com/callback?
code=SplxlOBeZQQYbYS6WxSbIA&
state=af0ifjsldkj
返回一次性Authorization Code给客户端。
客户端换取令牌
发出HTTP请求如下:
javascript
POST /token HTTP/1.1
Host: auth.server.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
redirect_uri=https://client.example.com/callback&
client_id=s6BhdRkqt3&
code_verifier=dGhpcyBpcyBhIGNoYWxsZW5nZQ
使用一次性Authorization Code和code_verifier请求Token。
认证/授权服务器通过Authorization Code找到对应的code_challenge,然后验证code_verifier有效后,返回如下:
javascript
{
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
其中ID Token是给客户端使用验证用户身份的,根据OpenID Connect规范的要求必须采用JWT格式。
Access Token是给资源服务器鉴定用户的访问权限的,用于请求资源服务器,可以是JWT,也可以是Reference Token。JWT是自包含的,鉴权时无需调用认证/授权服务器;Reference Token的鉴权需要调用认证/授权服务器。Keycloak默认使用Reference Token。JWT是无状态、自包含的,签发后无法主动失效,只能等待过期失效。Reference Token可以根据需要随时注销。
ID Token和Access Token的有效期都较短。
Refresh Token有效期较长,缓存在Redis中,用于生成新的Access Token。Refresh Token过期后,需要重新认证。
客户端验证 ID Token
javascript
// verify ID Token
const payload = verifyJwt(idToken, {
issuer: "https://auth.server.com",
audience: "s6BhdRkqt3",
nonce: storedNonce,
jwksUri: "https://auth.server.com/oauth/jwks"
});
// save to session
sessionStorage.setItem("user_id", payload.sub);
sessionStorage.setItem("user_name", payload.name);
sessionStorage.setItem("access_token", accessToken);
客户端会缓存ID Token,在必要的时候(如敏感请求、用户重新登录等)随时检查ID Token是否过期。ID Token过期后,可以使用Refresh Token请求新的ID Token,此时用户无需登录。
客户端使用Access Token访问资源服务器的资源
发出HTTP请求如下:
javascript
GET /api/user/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA
资源服务器向认证/授权服务器验证Access Token(仅Reference Token需要, JWT不需要)
JWT是自包含的,无需验证,但只能到期失效。Reference Token需要验证,但便于随时注销。
发出HTTP验证请求如下:
javascript
POST /introspect HTTP/1.1
Host: auth.server.com
Authorization: Basic base64(api-client:secret)
token=2YotnFZFEjr1zCsicMWpAA
认证/授权服务器返回"active": true表示有效,示例如下:
javascript
{
"active": true,
"scope": "profile email",
"client_id": "s6BhdRkqt3",
"username": "taiyangdao@example.com",
"token_type": "Bearer",
"exp": 1716123456,
"iat": 1716120456,
"sub": "123456789"
}
资源服务器看到有效Access Token,返回被请求的资源
javascript
{
"id": 123,
"name": "taiyangdao",
"email": "taiyangdao@example.com"
}
刷新Access Token
Access Token过期后,需要通过Refresh Token请求新的Access Token,HTTP请求如下:
javascript
POST /token HTTP/1.1
Host: auth.server.com
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=tGzv3JOkF0XG5Qx2TlKWIA&
client_id=s6BhdRkqt3&
client_secret=secret
认证/授权服务器返回如下:
javascript
{
"access_token": "new_access_token_xyz",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "new_refresh_token_xyz"
}
每次使用Refresh Token,都会创建新的Refresh Token,但是其有效期仍然从最初认证时间计算。如果Refresh Token过期,则用户必须重新登录。