name: oauth-implementation description: 实现安全的OAuth 2.0、OpenID Connect(OIDC)、JWT认证和SSO集成。用于构建Web和移动应用的安全认证系统。
OAuth 实现
概览
实现行业标准的OAuth 2.0和OpenID Connect认证流程,包括JWT令牌、刷新令牌和安全会话管理。
使用场景
- 用户认证系统
- 第三方API集成
- 单点登录(SSO)实现
- 移动应用认证
- 微服务安全
- 社交登录集成
实施示例
1. Node.js OAuth 2.0 服务器
// oauth-server.js - 完整的OAuth 2.0实现
const express = require('express');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const bcrypt = require('bcrypt');
class OAuthServer {
constructor() {
this.app = express();
this.clients = new Map();
this.authorizationCodes = new Map();
this.refreshTokens = new Map();
this.accessTokens = new Map();
// JWT签名密钥
this.privateKey = process.env.JWT_PRIVATE_KEY;
this.publicKey = process.env.JWT_PUBLIC_KEY;
this.setupRoutes();
}
// 注册OAuth客户端
registerClient(clientId, clientSecret, redirectUris) {
this.clients.set(clientId, {
clientSecret: bcrypt.hashSync(clientSecret, 10),
redirectUris,
grants: ['authorization_code', 'refresh_token']
});
}
setupRoutes() {
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
// 授权端点
this.app.get('/oauth/authorize', (req, res) => {
const { client_id, redirect_uri, response_type, scope, state } = req.query;
// 验证客户端
if (!this.clients.has(client_id)) {
return res.status(400).json({ error: 'invalid_client' });
}
const client = this.clients.get(client_id);
// 验证重定向URI
if (!client.redirectUris.includes(redirect_uri)) {
return res.status(400).json({ error: 'invalid_redirect_uri' });
}
// 验证响应类型
if (response_type !== 'code') {
return res.status(400).json({ error: 'unsupported_response_type' });
}
// 生成授权码
const code = crypto.randomBytes(32).toString('hex');
this.authorizationCodes.set(code, {
clientId: client_id,
redirectUri: redirect_uri,
scope: scope || 'read',
userId: req.user?.id, // 来自会话
expiresAt: Date.now() + 600000 // 10分钟
});
// 带授权码重定向
const redirectUrl = new URL(redirect_uri);
redirectUrl.searchParams.set('code', code);
if (state) redirectUrl.searchParams.set('state', state);
res.redirect(redirectUrl.toString());
});
// 令牌端点
this.app.post('/oauth/token', async (req, res) => {
const { grant_type, code, refresh_token, client_id, client_secret, redirect_uri } = req.body;
// 验证客户端凭据
const client = this.clients.get(client_id);
if (!client || !bcrypt.compareSync(client_secret, client.clientSecret)) {
return res.status(401).json({ error: 'invalid_client' });
}
if (grant_type === 'authorization_code') {
return this.handleAuthorizationCodeGrant(req, res, code, client_id, redirect_uri);
} else if (grant_type === 'refresh_token') {
return this.handleRefreshTokenGrant(req, res, refresh_token, client_id);
}
res.status(400).json({ error: 'unsupported_grant_type' });
});
// 令牌自检端点
this.app.post('/oauth/introspect', (req, res) => {
const { token } = req.body;
try {
const decoded = jwt.verify(token, this.publicKey, { algorithms: ['RS256'] });
res.json({
active: true,
scope: decoded.scope,
client_id: decoded.client_id,
user_id: decoded.sub,
exp: decoded.exp
});
} catch (error) {
res.json({ active: false });
}
});
// 令牌吊销端点
this.app.post('/oauth/revoke', (req, res) => {
const { token, token_type_hint } = req.body;
if (token_type_hint === 'refresh_token') {
this.refreshTokens.delete(token);
} else {
this.accessTokens.delete(token);
}
res.status(200).json({ success: true });
});
}
handleAuthorizationCodeGrant(req, res, code, clientId, redirectUri) {
const authCode = this.authorizationCodes.get(code);
if (!authCode) {
return res.status(400).json({ error: 'invalid_grant' });
}
// 验证授权码
if (authCode.clientId !== clientId || authCode.redirectUri !== redirectUri) {
return res.status(400).json({ error: 'invalid_grant' });
}
if (authCode.expiresAt < Date.now()) {
this.authorizationCodes.delete(code);
return res.status(400).json({ error: 'expired_grant' });
}
// 删除已使用的授权码
this.authorizationCodes.delete(code);
// 生成令牌
const tokens = this.generateTokens(clientId, authCode.userId, authCode.scope);
res.json(tokens);
}
handleRefreshTokenGrant(req, res, refreshToken, clientId) {
const storedToken = this.refreshTokens.get(refreshToken);
if (!storedToken || storedToken.clientId !== clientId) {
return res.status(400).json({ error: 'invalid_grant' });
}
if (storedToken.expiresAt < Date.now()) {
this.refreshTokens.delete(refreshToken);
return res.status(400).json({ error: 'expired_refresh_token' });
}
// 生成新的访问令牌
const tokens = this.generateTokens(clientId, storedToken.userId, storedToken.scope);
res.json(tokens);
}
generateTokens(clientId, userId, scope) {
// 生成访问令牌(JWT)
const accessToken = jwt.sign(
{
sub: userId,
client_id: clientId,
scope: scope,
type: 'access_token'
},
this.privateKey,
{
algorithm: 'RS256',
expiresIn: '1h',
issuer: 'https://auth.example.com',
audience: 'https://api.example.com'
}
);
// 生成刷新令牌
const refreshToken = crypto.randomBytes(64).toString('hex');
this.refreshTokens.set(refreshToken, {
clientId,
userId,
scope,
expiresAt: Date.now() + 2592000000 // 30天
});
return {
access_token: accessToken,
token_type: 'Bearer',
expires_in: 3600,
refresh_token: refreshToken,
scope: scope
};
}
// 保护路由的中间件
authenticate() {
return (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'missing_token' });
}
const token = authHeader.substring(7);
try {
const decoded = jwt.verify(token, this.publicKey, {
algorithms: ['RS256'],
issuer: 'https://auth.example.com',
audience: 'https://api.example.com'
});
req.user = {
id: decoded.sub,
clientId: decoded.client_id,
scope: decoded.scope
};
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'token_expired' });
}
return res.status(401).json({ error: 'invalid_token' });
}
};
}
start(port = 3000) {
this.app.listen(port, () => {
console.log(`OAuth服务器运行在端口${port}`);
});
}
}
// 使用
const oauthServer = new OAuthServer();
// 注册OAuth客户端
oauthServer.registerClient(
'client-app-123',
'super-secret-key',
['https://myapp.com/callback']
);
// 受保护的API端点
oauthServer.app.get('/api/user/profile',
oauthServer.authenticate(),
(req, res) => {
res.json({
userId: req.user.id,
scope: req.user.scope
});
}
);
oauthServer.start(3000);
2. Python OpenID Connect 实现
# oidc_provider.py
from flask import Flask, request, jsonify, redirect
from authlib.integrations.flask_oauth2 import AuthorizationServer
from authlib.integrations.flask_oauth2 import ResourceProtector
from authlib.oauth2.rfc6749 import grants
from authlib.jose import jwt
import secrets
import time
from datetime import datetime, timedelta
app = Flask(__name__)
app.config['SECRET_KEY'] = secrets.token_hex(32)
class OIDCProvider:
def __init__(self):
self.clients = {}
self.authorization_codes = {}
self.access_tokens = {}
self.id_tokens = {}
# RSA密钥用于JWT签名
self.private_key = self._load_private_key()
self.public_key = self._load_public_key()
def _load_private_key(self):
# 从环境或密钥管理服务加载
return """-----BEGIN RSA PRIVATE KEY-----
... 你的私钥 ...
-----END RSA PRIVATE KEY-----"""
def _load_public_key(self):
return """-----BEGIN PUBLIC KEY-----
... 你的公钥 ...
-----END PUBLIC KEY-----"""
def register_client(self, client_id, client_secret, redirect_uris, scopes):
"""注册OIDC客户端"""
self.clients[client_id] = {
'client_secret': client_secret,
'redirect_uris': redirect_uris,
'scopes': scopes,
'response_types': ['code', 'id_token', 'token']
}
def generate_id_token(self, user_id, client_id, nonce=None):
"""生成OpenID Connect ID令牌"""
now = int(time.time())
payload = {
'iss': 'https://auth.example.com',
'sub': user_id,
'aud': client_id,
'exp': now + 3600,
'iat': now,
'auth_time': now,
'nonce': nonce
}
# 添加可选声明
payload.update({
'email': f'{user_id}@example.com',
'email_verified': True,
'name': 'John Doe',
'given_name': 'John',
'family_name': 'Doe',
'picture': 'https://example.com/avatar.jpg'
})
header = {'alg': 'RS256', 'typ': 'JWT'}
return jwt.encode(header, payload, self.private_key)
def generate_access_token(self, user_id, client_id, scope):
"""生成OAuth 2.0访问令牌"""
token = secrets.token_urlsafe(32)
self.access_tokens[token] = {
'user_id': user_id,
'client_id': client_id,
'scope': scope,
'expires_at': datetime.now() + timedelta(hours=1)
}
return token
def verify_token(self, token):
"""验证JWT令牌"""
try:
claims = jwt.decode(token, self.public_key)
claims.validate()
return claims
except Exception as e:
return None
# OIDC端点
provider = OIDCProvider()
@app.route('/.well-known/openid-configuration')
def openid_configuration():
"""OpenID Connect发现端点"""
return jsonify({
'issuer': 'https://auth.example.com',
'authorization_endpoint': 'https://auth.example.com/oauth/authorize',
'token_endpoint': 'https://auth.example.com/oauth/token',
'userinfo_endpoint': 'https://auth.example.com/oauth/userinfo',
'jwks_uri': 'https://auth.example.com/.well-known/jwks.json',
'response_types_supported': ['code', 'id_token', 'token id_token'],
'subject_types_supported': ['public'],
'id_token_signing_alg_values_supported': ['RS256'],
'scopes_supported': ['openid', 'profile', 'email'],
'token_endpoint_auth_methods_supported': ['client_secret_basic', 'client_secret_post'],
'claims_supported': ['sub', 'iss', 'aud', 'exp', 'iat', 'name', 'email']
})
@app.route('/.well-known/jwks.json')
def jwks():
"""JSON Web Key Set端点"""
# 返回公钥格式的JWK
return jsonify({
'keys': [
{
'kty': 'RSA',
'use': 'sig',
'kid': '1',
'n': '...', # 公钥模数
'e': 'AQAB'
}
]
})
@app.route('/oauth/userinfo')
def userinfo():
"""UserInfo端点"""
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({'error': 'invalid_token'}), 401
token = auth_header[7:]
claims = provider.verify_token(token)
if not claims:
return jsonify({'error': 'invalid_token'}), 401
return jsonify({
'sub': claims['sub'],
'email': claims.get('email'),
'name': claims.get('name'),
'picture': claims.get('picture')
})
# 注册示例客户端
provider.register_client(
'sample-app',
'secret123',
['https://myapp.com/callback'],
['openid', 'profile', 'email']
)
if __name__ == '__main__':
app.run(port=3000, debug=True)
3. Java Spring Security OAuth
// OAuth2AuthorizationServerConfig.java
package com.example.oauth;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client-app")
.secret("{bcrypt}$2a$10$...")
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("read", "write")
.redirectUris("https://myapp.com/callback")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(86400)
.and()
.withClient("mobile-app")
.secret("{bcrypt}$2a$10$...")
.authorizedGrantTypes("password", "refresh_token")
.scopes("read")
.accessTokenValiditySeconds(7200);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter());
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("secret-key"); // 使用适当的密钥管理
return converter;
}
}
最佳实践
✅ 做
- 对于公共客户端使用PKCE
- 实施令牌轮换
- 安全存储令牌
- 处处使用HTTPS
- 验证重定向URI
- 实施速率限制
- 使用短期访问令牌
- 记录认证事件
❌ 不做
- 不要在localStorage中存储令牌
- 不要使用隐式流程
- 不要跳过状态参数
- 不要暴露客户端密钥
- 不要允许开放重定向
- 不要使用弱签名密钥
OAuth 2.0 流程
- 授权码:Web应用
- PKCE:移动和SPA应用
- 客户端凭据:服务间
- 刷新令牌:令牌续期