引言
OAuth2.0和JWT是现代Web应用认证授权的核心技术。OAuth2.0定义了安全的授权流程,JWT提供了自包含的令牌格式。本文将深入讲解这两种技术的原理、安全实践和常见陷阱。
OAuth2.0核心概念
四种授权模式
OAuth2.0 授权模式:
┌─────────────────────────────────────────┐
│ 1. 授权码模式(Authorization Code) │
│ 最安全,适用于有后端的Web应用 │
│ │
│ 2. 简化模式(Implicit) │
│ 已废弃,不推荐使用 │
│ │
│ 3. 密码模式(Resource Owner Password) │
│ 已废弃,仅用于遗留系统 │
│ │
│ 4. 客户端凭证模式(Client Credentials) │
│ 服务端到服务端通信 │
└─────────────────────────────────────────┘
授权码模式流程
授权码模式完整流程:
┌────────┐ ┌────────────┐
│ Client │ │ Auth Server│
│(Browser)│ │ │
└───┬────┘ └─────┬──────┘
│ │
│ 1. 请求授权(带code_challenge) │
│ ─────────────────────────────────────▶ │
│ │
│ 2. 用户登录并授权 │
│ ◀───────────────────────────────────── │
│ 重定向到redirect_uri?code=xxx │
│ │
│ 3. 交换Token(带code_verifier) │
│ ─────────────────────────────────────▶ │
│ POST /oauth/token │
│ │
│ 4. 返回Access Token + Refresh Token │
│ ◀───────────────────────────────────── │
│ │
┌───┴────┐ ┌─────┴──────┐
│ Backend│ │Resource API│
│ Server │ │ │
└───┬────┘ └─────┬──────┘
│ │
│ 5. 使用Access Token访问资源 │
│ ─────────────────────────────────────▶ │
│ GET /api/data │
│ Authorization: Bearer <token> │
│ │
│ 6. 返回受保护资源 │
│ ◀───────────────────────────────────── │
PKCE扩展(Proof Key for Code Exchange)
PKCE防止授权码拦截攻击:
┌─────────────────────────────────────────┐
│ 1. 生成code_verifier(随机字符串) │
│ code_verifier = random(43-128字符) │
│ │
│ 2. 计算code_challenge │
│ code_challenge = SHA256(code_verifier)│
│ code_challenge = Base64URL(hash) │
│ │
│ 3. 授权请求携带code_challenge │
│ /authorize?... │
│ &code_challenge=xxx │
│ &code_challenge_method=S256 │
│ │
│ 4. Token请求携带code_verifier │
│ POST /oauth/token │
│ code_verifier=xxx │
│ │
│ 5. 服务器验证 │
│ SHA256(code_verifier) == code_challenge│
│ │
│ 作用:即使授权码被截获,攻击者没有 │
│ code_verifier也无法交换Token │
└─────────────────────────────────────────┘
Spring Security OAuth2实现
授权服务器配置
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Bean
public JwtTokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 生产环境使用RSA密钥对
KeyPair keyPair = new KeyStoreKeyFactory(
new ClassPathResource("keystore.jks"),
"password".toCharArray()
).getKeyPair("jwt");
converter.setKeyPair(keyPair);
return converter;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter())
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
// Token增强器
.tokenEnhancer(tokenEnhancerChain());
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("web-app")
.secret(passwordEncoder.encode("secret"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("read", "write")
.redirectUris("https://web.example.com/callback")
.accessTokenValiditySeconds(3600) // 1小时
.refreshTokenValiditySeconds(86400); // 24小时
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();
}
@Bean
public TokenEnhancerChain tokenEnhancerChain() {
TokenEnhancerChain chain = new TokenEnhancerChain();
chain.setTokenEnhancers(Arrays.asList(
customTokenEnhancer(),
accessTokenConverter()
));
return chain;
}
@Bean
public TokenEnhancer customTokenEnhancer() {
return (accessToken, authentication) -> {
Map<String, Object> additionalInfo = new HashMap<>();
CustomUserDetails user = (CustomUserDetails) authentication.getPrincipal();
additionalInfo.put("user_id", user.getId());
additionalInfo.put("roles", user.getRoles());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
};
}
}
资源服务器配置
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Value("${security.oauth2.resource.jwt.key-uri}")
private String keyUri;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 从授权服务器获取公钥
RestTemplate restTemplate = new RestTemplate();
Map<String, String> key = restTemplate.getForObject(keyUri, Map.class);
converter.setVerifierKey(key.get("value"));
return converter;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources
.resourceId("api")
.tokenStore(tokenStore())
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.antMatchers("/api/**").authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\":\"unauthorized\"}");
});
}
}
JWT(JSON Web Token)
JWT结构
JWT由三部分组成:
┌─────────────────────────────────────────┐
│ Header.Payload.Signature │
│ │
│ 1. Header(头部) │
│ { │
│ "alg": "RS256", │
│ "typ": "JWT", │
│ "kid": "key-id-123" │
│ } │
│ │
│ 2. Payload(载荷) │
│ { │
│ "sub": "user123", │
│ "name": "John Doe", │
│ "roles": ["USER", "ADMIN"], │
│ "iat": 1672531200, │
│ "exp": 1672534800, │
│ "iss": "auth.example.com", │
│ "aud": "api.example.com" │
│ } │
│ │
│ 3. Signature(签名) │
│ RSASHA256( │
│ base64UrlEncode(header) + "." + │
│ base64UrlEncode(payload), │
│ privateKey │
│ ) │
└─────────────────────────────────────────┘
标准声明(Claims):
┌─────────────────────────────────────────┐
│ iss (issuer) 签发者 │
│ sub (subject) 主题(用户ID) │
│ aud (audience) 受众 │
│ exp (expiration) 过期时间 │
│ nbf (not before) 生效时间 │
│ iat (issued at) 签发时间 │
│ jti (JWT ID) 唯一标识 │
└─────────────────────────────────────────┘
JWT签名算法对比
签名算法选择:
┌─────────────────────────────────────────┐
│ 对称算法: │
│ HS256/HS384/HS512 │
│ - 使用共享密钥 │
│ - 速度快 │
│ - 密钥分发困难 │
│ - 适用:单体应用 │
│ │
│ 非对称算法(推荐): │
│ RS256/RS384/RS512(RSA) │
│ - 公私钥对 │
│ - 授权服务器用私钥签名 │
│ - 资源服务器用公钥验证 │
│ - 适用:微服务架构 │
│ │
│ ES256/ES384/ES512(ECDSA) │
│ - 椭圆曲线算法 │
│ - 签名更小 │
│ - 性能更好 │
│ - 适用:移动应用、IoT │
└─────────────────────────────────────────┘
Go JWT实现
Token生成与验证
package jwt
import (
"crypto/rsa"
"time"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
UserID string `json:"user_id"`
Username string `json:"username"`
Roles []string `json:"roles"`
jwt.RegisteredClaims
}
type JWTManager struct {
privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey
issuer string
audience string
}
func NewJWTManager(privateKey *rsa.PrivateKey, publicKey *rsa.PublicKey) *JWTManager {
return &JWTManager{
privateKey: privateKey,
publicKey: publicKey,
issuer: "auth.example.com",
audience: "api.example.com",
}
}
// 生成Access Token
func (m *JWTManager) GenerateAccessToken(userID, username string, roles []string) (string, error) {
now := time.Now()
claims := Claims{
UserID: userID,
Username: username,
Roles: roles,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(1 * time.Hour)),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
Issuer: m.issuer,
Subject: userID,
Audience: jwt.ClaimStrings{m.audience},
ID: generateUUID(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
return token.SignedString(m.privateKey)
}
// 生成Refresh Token
func (m *JWTManager) GenerateRefreshToken(userID string) (string, error) {
now := time.Now()
claims := jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(30 * 24 * time.Hour)), // 30天
IssuedAt: jwt.NewNumericDate(now),
Issuer: m.issuer,
Subject: userID,
ID: generateUUID(),
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
return token.SignedString(m.privateKey)
}
// 验证Token
func (m *JWTManager) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
// 验证算法
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return m.publicKey, nil
}, jwt.WithIssuer(m.issuer), jwt.WithAudience(m.audience))
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, jwt.ErrTokenInvalidClaims
}
中间件实现
package middleware
import (
"context"
"net/http"
"strings"
)
type contextKey string
const UserContextKey contextKey = "user"
type User struct {
ID string
Username string
Roles []string
}
func JWTAuthMiddleware(jwtManager *jwt.JWTManager) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 提取Token
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "missing authorization header", http.StatusUnauthorized)
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "invalid authorization header format", http.StatusUnauthorized)
return
}
tokenString := parts[1]
// 验证Token
claims, err := jwtManager.ValidateToken(tokenString)
if err != nil {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
// 将用户信息存入上下文
user := &User{
ID: claims.UserID,
Username: claims.Username,
Roles: claims.Roles,
}
ctx := context.WithValue(r.Context(), UserContextKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// 权限检查中间件
func RequireRole(roles ...string) func(http.Handler) http.Handler {
roleSet := make(map[string]bool)
for _, role := range roles {
roleSet[role] = true
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(UserContextKey).(*User)
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
// 检查用户是否有任一角色
for _, userRole := range user.Roles {
if roleSet[userRole] {
next.ServeHTTP(w, r)
return
}
}
http.Error(w, "forbidden", http.StatusForbidden)
})
}
}
// 使用示例
func main() {
mux := http.NewServeMux()
// 公开接口
mux.HandleFunc("/api/public", handlePublic)
// 需要认证的接口
protected := http.NewServeMux()
protected.HandleFunc("/api/profile", handleProfile)
// 需要管理员权限的接口
admin := http.NewServeMux()
admin.HandleFunc("/api/admin/users", handleAdminUsers)
// 应用中间件
handler := JWTAuthMiddleware(jwtManager)(protected)
adminHandler := RequireRole("ADMIN")(JWTAuthMiddleware(jwtManager)(admin))
// 路由
mux.Handle("/api/", handler)
mux.Handle("/api/admin/", adminHandler)
http.ListenAndServe(":8080", mux)
}
Token刷新策略
Refresh Token轮换
package token
import (
"context"
"time"
)
type TokenService struct {
jwtManager *JWTManager
tokenStore TokenStore // Redis或数据库
}
type RefreshTokenData struct {
Token string
UserID string
ExpiresAt time.Time
Revoked bool
Family string // Token家族ID
}
// 刷新Token(带轮换)
func (s *TokenService) RefreshToken(ctx context.Context, refreshToken string) (string, string, error) {
// 验证Refresh Token
claims, err := s.jwtManager.ValidateRefreshToken(refreshToken)
if err != nil {
return "", "", err
}
// 从存储中获取Token数据
tokenData, err := s.tokenStore.GetRefreshToken(ctx, refreshToken)
if err != nil {
return "", "", err
}
// 检查是否被撤销
if tokenData.Revoked {
// Token重用检测:撤销整个家族
s.tokenStore.RevokeTokenFamily(ctx, tokenData.Family)
return "", "", errors.New("token reuse detected")
}
// 撤销旧的Refresh Token
s.tokenStore.RevokeRefreshToken(ctx, refreshToken)
// 生成新的Access Token
accessToken, err := s.jwtManager.GenerateAccessToken(
tokenData.UserID,
// ... 其他参数
)
if err != nil {
return "", "", err
}
// 生成新的Refresh Token(同一家族)
newRefreshToken, err := s.jwtManager.GenerateRefreshToken(tokenData.UserID)
if err != nil {
return "", "", err
}
// 存储新的Refresh Token
s.tokenStore.SaveRefreshToken(ctx, &RefreshTokenData{
Token: newRefreshToken,
UserID: tokenData.UserID,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour),
Family: tokenData.Family, // 保持同一家族
})
return accessToken, newRefreshToken, nil
}
// 撤销所有Token(用户登出)
func (s *TokenService) RevokeAllTokens(ctx context.Context, userID string) error {
return s.tokenStore.RevokeAllUserTokens(ctx, userID)
}
安全最佳实践
JWT安全清单
JWT安全最佳实践:
┌─────────────────────────────────────────┐
│ ✅ 推荐: │
│ 1. 使用RS256/ES256非对称算法 │
│ 2. 设置合理的过期时间(1小时内) │
│ 3. 包含jti(唯一标识)便于撤销 │
│ 4. 验证iss、aud、exp等标准声明 │
│ 5. 使用Refresh Token轮换 │
│ 6. 在Token中只存必要信息 │
│ 7. 敏感操作需要重新认证 │
│ │
│ ❌ 避免: │
│ 1. 不要使用HS256(共享密钥) │
│ 2. 不要在Token中存储敏感信息 │
│ 3. 不要设置过长的过期时间 │
│ 4. 不要信任客户端传来的算法 │
│ 5. 不要忽略签名验证 │
│ 6. 不要在URL中传递Token │
│ 7. 不要在前端存储敏感Token │
└─────────────────────────────────────────┘
Token存储策略
// 前端Token存储对比
/*
┌─────────────────────────────────────────┐
│ localStorage: │
│ 优点:持久化,页面刷新不丢失 │
│ 缺点:XSS攻击可读取 │
│ 适用:Refresh Token(配合HttpOnly) │
│ │
│ sessionStorage: │
│ 优点:标签页关闭自动清除 │
│ 缺点:XSS攻击可读取 │
│ 适用:短期Token │
│ │
│ HttpOnly Cookie(推荐): │
│ 优点:JavaScript无法访问,防XSS │
│ 缺点:CSRF风险(需配合SameSite) │
│ 适用:Access Token + Refresh Token │
│ │
│ 内存(变量): │
│ 优点:最安全 │
│ 缺点:页面刷新丢失 │
│ 适用:Access Token(配合自动刷新) │
└─────────────────────────────────────────┘
*/
// 推荐方案:HttpOnly Cookie
// 后端设置Cookie
http.SetCookie(w, &http.Cookie{
Name: "access_token",
Value: accessToken,
Path: "/",
HttpOnly: true, // JavaScript无法访问
Secure: true, // 仅HTTPS
SameSite: http.SameSiteStrictMode, // 防CSRF
MaxAge: 3600,
})
// 前端请求自动携带Cookie
fetch('/api/profile', {
credentials: 'include' // 携带Cookie
});
总结
OAuth2.0与JWT最佳实践
| 场景 | 推荐方案 | 安全级别 |
|---|---|---|
| Web应用 | 授权码 + PKCE + HttpOnly Cookie | ⭐⭐⭐⭐⭐ |
| 移动应用 | 授权码 + PKCE + 内存存储 | ⭐⭐⭐⭐⭐ |
| SPA单页应用 | 授权码 + PKCE + 内存 + 自动刷新 | ⭐⭐⭐⭐ |
| 服务端通信 | 客户端凭证 + JWT | ⭐⭐⭐⭐ |
| 第三方集成 | 授权码 + 短Token + 范围限制 | ⭐⭐⭐⭐ |
关键原则
- 始终使用PKCE:即使是机密客户端
- 短Access Token:15分钟-1小时
- Refresh Token轮换:检测重用攻击
- HttpOnly Cookie:防止XSS窃取Token
- 验证所有声明:iss、aud、exp、nbf
- 最小权限原则:只授予必要的scope
- 定期轮换密钥:支持多个kid并行
延伸阅读
- OAuth 2.0 Security Best Current Practice
- JWT Best Practices
- Spring Security OAuth2
- OWASP JWT Cheat Sheet
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。