当前位置: 首页 > news >正文

OAuth2与JWT:现代授权与身份验证实践

OAuth2与JWT:现代授权与身份验证实践

引言

OAuth2和JWT是现代Web应用和API安全的基石。OAuth2解决了"授权"问题,让用户可以授权第三方应用访问自己的资源,而无需分享密码。JWT则提供了一种紧凑、安全的信息传输方式。本文将深入解析OAuth2流程和JWT实现。

一、OAuth2核心概念

1.1 OAuth2角色

  • Resource Owner:资源所有者,即最终用户
  • Client:请求访问权限的第三方应用
  • Authorization Server:授权服务器,负责验证用户身份并发放令牌
  • Resource Server:资源服务器,托管受保护的资源

1.2 OAuth2授权类型

授权类型场景安全性
Authorization Code有后端服务器的应用最高
PKCE移动端/SPA应用
Client Credentials服务间通信
Refresh Token令牌刷新

二、Authorization Code + PKCE实现

2.1 PKCE流程概述

PKCE(Proof Key for Code Exchange)防止授权码被拦截攻击:

1. Client 生成 code_verifier (随机字符串) 2. Client 计算 code_challenge = BASE64URL(SHA256(code_verifier)) 3. 用户被重定向到授权服务器 4. 授权服务器返回 authorization_code 5. Client 用 authorization_code + code_verifier 换取 token 6. 授权服务器验证 code_challenge

2.2 授权服务器实现

package oauth2 import ( "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "net/http" "net/url" "strings" "sync" "time" "github.com/google/uuid" ) type AuthorizationServer struct { clients map[string]*Client codes map[string]*AuthorizationCode tokens map[string]*Token mu sync.RWMutex issuer string signingKey []byte } type Client struct { ID string Secret string RedirectURIs []string GrantTypes []string } type AuthorizationCode struct { Code string ClientID string RedirectURI string UserID string Scope string ExpiresAt time.Time CodeChallenge string CodeChallengeMethod string } type Token struct { AccessToken string RefreshToken string TokenType string ExpiresIn int ExpiresAt time.Time Scope string UserID string ClientID string } func NewAuthorizationServer(issuer string, signingKey []byte) *AuthorizationServer { return &AuthorizationServer{ clients: make(map[string]*Client), codes: make(map[string]*AuthorizationCode), tokens: make(map[string]*Token), issuer: issuer, signingKey: signingKey, } } func (as *AuthorizationServer) RegisterClient(client *Client) error { as.mu.Lock() defer as.mu.Unlock() if client.ID == "" { client.ID = uuid.New().String() } if client.Secret == "" { secret := make([]byte, 32) rand.Read(secret) client.Secret = base64.URLEncoding.EncodeToString(secret) } as.clients[client.ID] = client return nil } func (as *AuthorizationServer) HandleAuthorization(w http.ResponseWriter, r *http.Request) { clientID := r.URL.Query().Get("client_id") redirectURI := r.URL.Query().Get("redirect_uri") responseType := r.URL.Query().Get("response_type") scope := r.URL.Query().Get("scope") state := r.URL.Query().Get("state") codeChallenge := r.URL.Query().Get("code_challenge") codeChallengeMethod := r.URL.Query().Get("code_challenge_method") if responseType != "code" { as.redirectWithError(w, redirectURI, "unsupported_response_type", state) return } client, exists := as.clients[clientID] if !exists { as.redirectWithError(w, redirectURI, "invalid_client", state) return } if !as.validRedirectURI(redirectURI, client) { as.redirectWithError(w, redirectURI, "invalid_request", state) return } userID := as.authenticateUser(r) if userID == "" { as.renderLoginForm(w, r, clientID, redirectURI, scope, state) return } code := as.generateAuthorizationCode(clientID, redirectURI, userID, scope, codeChallenge, codeChallengeMethod) params := url.Values{} params.Set("code", code) params.Set("state", state) http.Redirect(w, r, redirectURI+"?"+params.Encode(), http.StatusFound) } func (as *AuthorizationServer) renderLoginForm(w http.ResponseWriter, r *http.Request, clientID, redirectURI, scope, state string) { w.Header().Set("Content-Type", "text/html") fmt.Fprintf(w, ` <html> <body> <h1>Login</h1> <form method="post"> <input type="hidden" name="client_id" value="%s"> <input type="hidden" name="redirect_uri" value="%s"> <input type="hidden" name="scope" value="%s"> <input type="hidden" name="state" value="%s"> <input type="hidden" name="action" value="login"> <input type="hidden" name="return_to" value="%s"> <p>Email: <input type="text" name="email"></p> <p>Password: <input type="password" name="password"></p> <button type="submit">Login</button> </form> </body> </html> `, clientID, redirectURI, scope, state, r.URL.RequestURI()) } func (as *AuthorizationServer) authenticateUser(r *http.Request) string { return r.Header.Get("X-User-ID") } func (as *AuthorizationServer) HandleToken(w http.ResponseWriter, r *http.Request) { r.ParseForm() grantType := r.PostForm.Get("grant_type") switch grantType { case "authorization_code": as.handleAuthorizationCodeGrant(w, r) case "refresh_token": as.handleRefreshTokenGrant(w, r) case "client_credentials": as.handleClientCredentialsGrant(w, r) default: http.Error(w, `{"error": "unsupported_grant_type"}`, http.StatusBadRequest) } } func (as *AuthorizationServer) handleAuthorizationCodeGrant(w http.ResponseWriter, r *http.Request) { clientID := r.PostForm.Get("client_id") clientSecret := r.PostForm.Get("client_secret") code := r.PostForm.Get("code") redirectURI := r.PostForm.Get("redirect_uri") codeVerifier := r.PostForm.Get("code_verifier") client, exists := as.clients[clientID] if !exists || client.Secret != clientSecret { as.tokenError(w, "invalid_client") return } as.mu.Lock() authCode, ok := as.codes[code] if ok { delete(as.codes, code) } as.mu.Unlock() if !ok || authCode.Expired() { as.tokenError(w, "invalid_grant") return } if authCode.RedirectURI != redirectURI { as.tokenError(w, "invalid_grant") return } if authCode.CodeChallenge != "" { if !as.verifyCodeChallenge(codeVerifier, authCode.CodeChallenge, authCode.CodeChallengeMethod) { as.tokenError(w, "invalid_grant") return } } token := as.generateToken(authCode.UserID, clientID, authCode.Scope) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(token) } func (as *AuthorizationServer) handleRefreshTokenGrant(w http.ResponseWriter, r *http.Request) { refreshToken := r.PostForm.Get("refresh_token") as.mu.RLock() token, exists := as.tokens[refreshToken] as.mu.RUnlock() if !exists || token.Expired() { as.tokenError(w, "invalid_grant") return } newToken := as.generateToken(token.UserID, token.ClientID, token.Scope) as.mu.Lock() delete(as.tokens, refreshToken) as.mu.Unlock() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(newToken) } func (as *AuthorizationServer) handleClientCredentialsGrant(w http.ResponseWriter, r *http.Request) { clientID := r.PostForm.Get("client_id") clientSecret := r.PostForm.Get("client_secret") scope := r.PostForm.Get("scope") client, exists := as.clients[clientID] if !exists || client.Secret != clientSecret { as.tokenError(w, "invalid_client") return } token := as.generateToken("", clientID, scope) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(token) } func (as *AuthorizationServer) generateAuthorizationCode(clientID, redirectURI, userID, scope, codeChallenge, codeChallengeMethod string) string { code := generateRandomString(32) as.mu.Lock() as.codes[code] = &AuthorizationCode{ Code: code, ClientID: clientID, RedirectURI: redirectURI, UserID: userID, Scope: scope, ExpiresAt: time.Now().Add(10 * time.Minute), CodeChallenge: codeChallenge, CodeChallengeMethod: codeChallengeMethod, } as.mu.Unlock() return code } func (as *AuthorizationServer) generateToken(userID, clientID, scope string) *Token { accessToken := generateRandomString(32) refreshToken := generateRandomString(32) expiresIn := 3600 token := &Token{ AccessToken: accessToken, RefreshToken: refreshToken, TokenType: "Bearer", ExpiresIn: expiresIn, ExpiresAt: time.Now().Add(time.Duration(expiresIn) * time.Second), Scope: scope, UserID: userID, ClientID: clientID, } as.mu.Lock() as.tokens[accessToken] = token as.tokens[refreshToken] = token as.mu.Unlock() return token } func (as *AuthorizationServer) verifyCodeChallenge(verifier, challenge, method string) bool { switch method { case "S256": hash := sha256.Sum256([]byte(verifier)) computed := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash[:]) return computed == challenge case "plain": return verifier == challenge default: return false } } func (as *AuthorizationServer) validRedirectURI(uri string, client *Client) bool { for _, validURI := range client.RedirectURIs { if validURI == uri { return true } } return false } func (as *AuthorizationServer) redirectWithError(w http.ResponseWriter, redirectURI, error, state string) { if redirectURI == "" { http.Error(w, error, http.StatusBadRequest) return } params := url.Values{} params.Set("error", error) if state != "" { params.Set("state", state) } http.Redirect(w, &http.Request{}, redirectURI+"?"+params.Encode(), http.StatusFound) } func (as *AuthorizationServer) tokenError(w http.ResponseWriter, error string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{"error": error}) } func (as *AuthorizationServer) ValidateToken(tokenString string) (*Token, error) { as.mu.RLock() defer as.mu.RUnlock() token, exists := as.tokens[tokenString] if !exists || token.Expired() { return nil, fmt.Errorf("invalid or expired token") } return token, nil } func (c *AuthorizationCode) Expired() bool { return time.Now().After(c.ExpiresAt) } func (t *Token) Expired() bool { return time.Now().After(t.ExpiresAt) } func generateRandomString(length int) string { bytes := make([]byte, length) rand.Read(bytes) return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(bytes) }

三、JWT实现与使用

3.1 JWT结构

JWT由三部分组成:Header.Payload.Signature

package jwt import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "fmt" "strings" "time" ) var ( ErrInvalidToken = errors.New("invalid token") ErrExpiredToken = errors.New("token has expired") ErrInvalidClaims = errors.New("invalid claims") ) type Header struct { Alg string `json:"alg"` Typ string `json:"typ"` } type Claims struct { Issuer string `json:"iss,omitempty"` Subject string `json:"sub,omitempty"` Audience string `json:"aud,omitempty"` ExpiresAt int64 `json:"exp,omitempty"` NotBefore int64 `json:"nbf,omitempty"` IssuedAt int64 `json:"iat,omitempty"` JWTID string `json:"jti,omitempty"` Custom map[string]interface{} `json:"-"` } type Token struct { Raw string Header Header Claims Claims Signature []byte } func (c *Claims) Validate(issuer, audience string, expiry time.Duration) error { now := time.Now().Unix() if c.ExpiresAt > 0 && c.ExpiresAt < now { return ErrExpiredToken } if c.NotBefore > 0 && c.NotBefore > now { return ErrInvalidClaims } if issuer != "" && c.Issuer != issuer { return ErrInvalidClaims } if audience != "" && c.Audience != audience { return ErrInvalidClaims } return nil } type Encoder struct { secretKey []byte algorithm string } func NewEncoder(secretKey []byte) *Encoder { return &Encoder{ secretKey: secretKey, algorithm: "HS256", } } func (e *Encoder) Encode(claims *Claims) (string, error) { if claims.IssuedAt == 0 { claims.IssuedAt = time.Now().Unix() } header := Header{ Alg: e.algorithm, Typ: "JWT", } headerBytes, err := json.Marshal(header) if err != nil { return "", fmt.Errorf("failed to marshal header: %w", err) } claimsBytes, err := json.Marshal(claims) if err != nil { return "", fmt.Errorf("failed to marshal claims: %w", err) } headerB64 := base64.RawURLEncoding.EncodeToString(headerBytes) claimsB64 := base64.RawURLEncoding.EncodeToString(claimsBytes) signingInput := headerB64 + "." + claimsB64 signature := e.sign([]byte(signingInput)) return signingInput + "." + base64.RawURLEncoding.EncodeToString(signature), nil } func (e *Encoder) sign(message []byte) []byte { h := hmac.New(sha256.New, e.secretKey) h.Write(message) return h.Sum(nil) } type Decoder struct { secretKey []byte algorithm string } func NewDecoder(secretKey []byte) *Decoder { return &Decoder{ secretKey: secretKey, algorithm: "HS256", } } func (d *Decoder) Decode(tokenString string) (*Token, error) { parts := strings.Split(tokenString, ".") if len(parts) != 3 { return nil, ErrInvalidToken } headerBytes, err := base64.RawURLEncoding.DecodeString(parts[0]) if err != nil { return nil, ErrInvalidToken } var header Header if err := json.Unmarshal(headerBytes, &header); err != nil { return nil, ErrInvalidToken } claimsBytes, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil { return nil, ErrInvalidToken } var claims Claims if err := json.Unmarshal(claimsBytes, &claims); err != nil { return nil, ErrInvalidToken } signature, err := base64.RawURLEncoding.DecodeString(parts[2]) if err != nil { return nil, ErrInvalidToken } signingInput := parts[0] + "." + parts[1] expectedSig := d.sign([]byte(signingInput)) if !hmac.Equal(signature, expectedSig) { return nil, ErrInvalidToken } return &Token{ Raw: tokenString, Header: header, Claims: claims, Signature: signature, }, nil } func (d *Decoder) sign(message []byte) []byte { h := hmac.New(sha256.New, d.secretKey) h.Write(message) return h.Sum(nil) }

3.2 JWT刷新令牌机制

package jwt import ( "sync" "time" ) type TokenStore struct { mu sync.RWMutex refreshTokens map[string]*RefreshTokenEntry accessTokens map[string]*AccessTokenEntry encoder *Encoder decoder *Decoder } type RefreshTokenEntry struct { TokenID string UserID string ClientID string Scope string IssuedAt time.Time ExpiresAt time.Time UsedCount int Revoked bool } type AccessTokenEntry struct { TokenID string UserID string ClientID string Scope string IssuedAt time.Time ExpiresAt time.Time Revoked bool } func NewTokenStore(secretKey []byte) *TokenStore { return &TokenStore{ refreshTokens: make(map[string]*RefreshTokenEntry), accessTokens: make(map[string]*AccessTokenEntry), encoder: NewEncoder(secretKey), decoder: NewDecoder(secretKey), } } func (ts *TokenStore) GenerateAccessToken(userID, clientID, scope string, expiry time.Duration) (*Token, error) { claims := &Claims{ Subject: userID, Audience: clientID, IssuedAt: time.Now().Unix(), ExpiresAt: time.Now().Add(expiry).Unix(), JWTID: generateTokenID(), } tokenString, err := ts.encoder.Encode(claims) if err != nil { return nil, err } entry := &AccessTokenEntry{ TokenID: claims.JWTID, UserID: userID, ClientID: clientID, Scope: scope, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(expiry), } ts.mu.Lock() ts.accessTokens[claims.JWTID] = entry ts.mu.Unlock() return &Token{Raw: tokenString, Claims: *claims}, nil } func (ts *TokenStore) GenerateRefreshToken(userID, clientID, scope string) (string, time.Time, error) { claims := &Claims{ Subject: userID, Audience: clientID, IssuedAt: time.Now().Unix(), ExpiresAt: time.Now().Add(30 * 24 * time.Hour).Unix(), JWTID: generateTokenID(), } tokenString, err := ts.encoder.Encode(claims) if err != nil { return "", time.Time{}, err } expiresAt := time.Until(time.Now().Add(30 * 24 * time.Hour)) entry := &RefreshTokenEntry{ TokenID: claims.JWTID, UserID: userID, ClientID: clientID, Scope: scope, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(30 * 24 * time.Hour), } ts.mu.Lock() ts.refreshTokens[claims.JWTID] = entry ts.mu.Unlock() return tokenString, expiresAt, nil } func (ts *TokenStore) RefreshAccessToken(refreshTokenString string) (*Token, error) { token, err := ts.decoder.Decode(refreshTokenString) if err != nil { return nil, err } ts.mu.Lock() entry, exists := ts.refreshTokens[token.Claims.JWTID] if !exists || entry.Revoked { ts.mu.Unlock() return nil, ErrInvalidToken } if entry.UsedCount >= 1 { for id := range ts.refreshTokens { if ts.refreshTokens[id].UserID == entry.UserID { ts.refreshTokens[id].Revoked = true } } ts.mu.Unlock() return nil, ErrInvalidToken } entry.UsedCount++ ts.mu.Unlock() return ts.GenerateAccessToken(entry.UserID, entry.ClientID, entry.Scope, time.Hour) } func (ts *TokenStore) RevokeRefreshToken(tokenString string) error { token, err := ts.decoder.Decode(tokenString) if err != nil { return err } ts.mu.Lock() if entry, exists := ts.refreshTokens[token.Claims.JWTID]; exists { entry.Revoked = true } ts.mu.Unlock() return nil } func (ts *TokenStore) ValidateAccessToken(tokenString string) (*AccessTokenEntry, error) { token, err := ts.decoder.Decode(tokenString) if err != nil { return nil, err } ts.mu.RLock() entry, exists := ts.accessTokens[token.Claims.JWTID] ts.mu.RUnlock() if !exists || entry.Revoked { return nil, ErrInvalidToken } if time.Now().After(entry.ExpiresAt) { return nil, ErrExpiredToken } return entry, nil } func (ts *TokenStore) Cleanup() { ticker := time.NewTicker(1 * time.Hour) go func() { for range ticker.C { ts.cleanupExpired() } }() } func (ts *TokenStore) cleanupExpired() { now := time.Now() ts.mu.Lock() for id, entry := range ts.refreshTokens { if now.After(entry.ExpiresAt) { delete(ts.refreshTokens, id) } } for id, entry := range ts.accessTokens { if now.After(entry.ExpiresAt) { delete(ts.accessTokens, id) } } ts.mu.Unlock() } func generateTokenID() string { b := make([]byte, 16) for i := range b { b[i] = byte(time.Now().UnixNano() >> uint(i*8) & 0xff) } return base64.RawURLEncoding.EncodeToString(b) }

四、实战:API网关集成

package gateway import ( "net/http" "strings" "github.com/4jiang_style/csdn-users/jwt" ) type AuthMiddleware struct { decoder *jwt.Decoder skipPaths []string } func NewAuthMiddleware(secretKey []byte) *AuthMiddleware { return &AuthMiddleware{ decoder: jwt.NewDecoder(secretKey), skipPaths: []string{"/health", "/metrics", "/oauth/token"}, } } func (m *AuthMiddleware) Handler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if m.shouldSkip(r.URL.Path) { next.ServeHTTP(w, r) return } 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 || strings.ToLower(parts[0]) != "bearer" { http.Error(w, "invalid authorization header format", http.StatusUnauthorized) return } token, err := m.decoder.Decode(parts[1]) if err != nil { http.Error(w, "invalid token", http.StatusUnauthorized) return } if err := token.Claims.Validate("", "", 0); err != nil { http.Error(w, "token expired or invalid", http.StatusUnauthorized) return } r.Header.Set("X-User-ID", token.Claims.Subject) r.Header.Set("X-Token-ID", token.Claims.JWTID) next.ServeHTTP(w, r) }) } func (m *AuthMiddleware) shouldSkip(path string) bool { for _, skipPath := range m.skipPaths { if strings.HasPrefix(path, skipPath) { return true } } return false }

五、总结

OAuth2和JWT是现代API安全的核心标准:

  1. OAuth2授权类型选择

    • 有后端服务:Authorization Code + PKCE
    • SPA/移动端:PKCE是必需的
    • 服务间:Client Credentials
  2. JWT安全要点

    • 始终验证签名
    • 检查令牌过期时间
    • 验证Issuer和Audience
    • 使用HTTPS传输
  3. 刷新令牌策略

    • 限制刷新令牌使用次数
    • 检测令牌重用攻击
    • 定期轮换刷新令牌
  4. 安全最佳实践

    • 令牌存放在安全位置,避免XSS泄露
    • 实现令牌撤销机制
    • 记录审计日志

掌握这些核心技术,能够构建安全可靠的现代认证授权系统。

http://www.jsqmd.com/news/806105/

相关文章:

  • 二手房翻新的进口内墙漆选择与安全标准
  • 机载雷达ISAR成像运动补偿算法【附代码】
  • Web安全:CSRF跨站请求伪造详解
  • KMeans核心原理与关键代码实现
  • 2026南京钢管租赁技术指南与合规供应商盘点:方管租赁/江苏盘扣租赁/江苏钢管租赁/盘扣式脚手架租赁/脚手架钢管/选择指南 - 优质品牌商家
  • Pytorch图像去噪实战(七十二):Alertmanager告警实战,接口错误率和GPU显存异常自动通知
  • 面试自我介绍别背简历:数据人应该讲清楚这 3 件事
  • 杭州房屋租赁首选:专业的房屋租赁排名靠前的
  • 达梦 8 数组类型使用测试
  • 酒店餐饮企业公司注册服务优质机构推荐 - 优质品牌商家
  • Windows安卓应用安装神器:APK-Installer终极使用指南
  • 从Demo到生产:构建高可用AI智能体的工程化实践
  • 2026年高评价吨袋自动包装机推荐 附核心参数对比 - 优质品牌商家
  • AgentLint:AI助手配置文件质量检查工具,提升开发效率与安全性
  • MCP Pool:基于Model Context Protocol构建AI助手与SaaS数据桥接方案
  • 私有化部署验签引擎:大型企业数据安全与合规的终极方案
  • 5分钟掌握数据主权:WeChatMsg微信聊天记录导出与永久保存完全指南
  • 2026年5月更新:电缆回收企业选型指南,宁波皓诚再生资源有限公司深度解析 - 2026年企业推荐榜
  • goxam 界面不刷新的问题,需要滚轮或者重新加载才能显示
  • 2026年靠谱代账品牌盘点:公司注册流程/成都公司注册/无地址公司注册/电商代账/从资质到售后的硬核筛选 - 优质品牌商家
  • WindowResizer终极教程:3分钟掌握Windows窗口大小调整神器
  • 为什么92%的前端团队仍在手动运行tsc?Claude实时类型推导已悄然替代——48小时紧急迁移手册
  • 2026年知名的线控EPS带转向器总成/台州转向器主流厂家对比评测 - 品牌宣传支持者
  • Python-pptx进阶玩法:给你的PPT图表加上动态数据,告别静态截图
  • 2026年方管租赁专业选型指南:附近盘扣租赁站及电话、附近钢管租赁站及电话、南京盘扣租赁、南京钢管租赁、扣件租赁选择指南 - 优质品牌商家
  • MS35629 具有静音和堵转检测功能的 36V、1.5A 两相 S/D 步进电机驱动芯片
  • 第二篇:基于 RA-ECO-RA4M2 开发板的四轮小车运动控制评测
  • 【Claude 3.5 Sonnet深度解析】:5大颠覆性新功能实测对比,开发者必须立即掌握的AI生产力跃迁指南
  • 0304光刻机突围全景:第三卷 双工件台+纳米级精密运动控制 国产精度优化方案
  • 怎样快速免费完成QQ音乐格式转换:完整实用手册