OAuth2.0与JWT安全实战:认证授权的正确打开方式

深入讲解OAuth2.0授权框架与JWT令牌的安全实践,包括授权码模式、PKCE扩展、Token刷新策略、JWT签名验证,提供Spring Security、Go的完整实现代码。

引言

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 + 范围限制⭐⭐⭐⭐

关键原则

  1. 始终使用PKCE:即使是机密客户端
  2. 短Access Token:15分钟-1小时
  3. Refresh Token轮换:检测重用攻击
  4. HttpOnly Cookie:防止XSS窃取Token
  5. 验证所有声明:iss、aud、exp、nbf
  6. 最小权限原则:只授予必要的scope
  7. 定期轮换密钥:支持多个kid并行

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页