3.9.2 OAuth2.0 授权

3.9.2 OAuth2.0 授权 #

OAuth 2.0 是一个开放标准,允许用户授权第三方应用访问他们在另一个服务上的资源,而无需分享他们的凭据。它广泛用于社交登录、API 访问控制等场景。

OAuth 2.0 基础概念 #

角色定义 #

OAuth 2.0 定义了四个角色:

  1. 资源所有者(Resource Owner):通常是用户,拥有受保护资源的实体
  2. 客户端(Client):代表资源所有者请求受保护资源的应用程序
  3. 资源服务器(Resource Server):托管受保护资源的服务器
  4. 授权服务器(Authorization Server):验证资源所有者身份并颁发访问令牌

授权流程类型 #

OAuth 2.0 定义了四种授权流程:

  1. 授权码流程(Authorization Code Flow):最安全的流程,适用于服务端应用
  2. 隐式流程(Implicit Flow):适用于客户端应用,已不推荐使用
  3. 资源所有者密码凭据流程(Resource Owner Password Credentials Flow):适用于高度信任的应用
  4. 客户端凭据流程(Client Credentials Flow):适用于服务间通信

Go 中实现 OAuth 2.0 #

安装依赖 #

go get golang.org/x/oauth2
go get github.com/gin-gonic/gin
go get github.com/google/uuid

OAuth 2.0 服务器实现 #

授权服务器 #

// pkg/oauth/server.go
package oauth

import (
    "crypto/rand"
    "encoding/base64"
    "errors"
    "fmt"
    "net/url"
    "time"

    "github.com/google/uuid"
)

// Client OAuth客户端
type Client struct {
    ID           string   `json:"id"`
    Secret       string   `json:"secret"`
    Name         string   `json:"name"`
    RedirectURIs []string `json:"redirect_uris"`
    Scopes       []string `json:"scopes"`
}

// AuthorizationCode 授权码
type AuthorizationCode struct {
    Code        string    `json:"code"`
    ClientID    string    `json:"client_id"`
    UserID      string    `json:"user_id"`
    RedirectURI string    `json:"redirect_uri"`
    Scope       string    `json:"scope"`
    ExpiresAt   time.Time `json:"expires_at"`
    Used        bool      `json:"used"`
}

// AccessToken 访问令牌
type AccessToken struct {
    Token     string    `json:"token"`
    TokenType string    `json:"token_type"`
    ClientID  string    `json:"client_id"`
    UserID    string    `json:"user_id"`
    Scope     string    `json:"scope"`
    ExpiresAt time.Time `json:"expires_at"`
}

// RefreshToken 刷新令牌
type RefreshToken struct {
    Token     string    `json:"token"`
    ClientID  string    `json:"client_id"`
    UserID    string    `json:"user_id"`
    Scope     string    `json:"scope"`
    ExpiresAt time.Time `json:"expires_at"`
}

// OAuthServer OAuth服务器
type OAuthServer struct {
    clients        map[string]*Client
    authCodes      map[string]*AuthorizationCode
    accessTokens   map[string]*AccessToken
    refreshTokens  map[string]*RefreshToken
}

// NewOAuthServer 创建OAuth服务器
func NewOAuthServer() *OAuthServer {
    return &OAuthServer{
        clients:       make(map[string]*Client),
        authCodes:     make(map[string]*AuthorizationCode),
        accessTokens:  make(map[string]*AccessToken),
        refreshTokens: make(map[string]*RefreshToken),
    }
}

// RegisterClient 注册客户端
func (s *OAuthServer) RegisterClient(client *Client) {
    s.clients[client.ID] = client
}

// GetClient 获取客户端
func (s *OAuthServer) GetClient(clientID string) (*Client, error) {
    client, exists := s.clients[clientID]
    if !exists {
        return nil, errors.New("客户端不存在")
    }
    return client, nil
}

// ValidateRedirectURI 验证重定向URI
func (s *OAuthServer) ValidateRedirectURI(clientID, redirectURI string) error {
    client, err := s.GetClient(clientID)
    if err != nil {
        return err
    }

    for _, uri := range client.RedirectURIs {
        if uri == redirectURI {
            return nil
        }
    }
    return errors.New("无效的重定向URI")
}

// GenerateAuthorizationCode 生成授权码
func (s *OAuthServer) GenerateAuthorizationCode(clientID, userID, redirectURI, scope string) (string, error) {
    code := generateRandomString(32)
    authCode := &AuthorizationCode{
        Code:        code,
        ClientID:    clientID,
        UserID:      userID,
        RedirectURI: redirectURI,
        Scope:       scope,
        ExpiresAt:   time.Now().Add(10 * time.Minute), // 10分钟有效期
        Used:        false,
    }

    s.authCodes[code] = authCode
    return code, nil
}

// ExchangeCodeForToken 用授权码换取访问令牌
func (s *OAuthServer) ExchangeCodeForToken(code, clientID, clientSecret, redirectURI string) (*AccessToken, *RefreshToken, error) {
    // 验证客户端
    client, err := s.GetClient(clientID)
    if err != nil {
        return nil, nil, err
    }

    if client.Secret != clientSecret {
        return nil, nil, errors.New("客户端密钥错误")
    }

    // 验证授权码
    authCode, exists := s.authCodes[code]
    if !exists {
        return nil, nil, errors.New("授权码不存在")
    }

    if authCode.Used {
        return nil, nil, errors.New("授权码已使用")
    }

    if time.Now().After(authCode.ExpiresAt) {
        return nil, nil, errors.New("授权码已过期")
    }

    if authCode.ClientID != clientID {
        return nil, nil, errors.New("客户端ID不匹配")
    }

    if authCode.RedirectURI != redirectURI {
        return nil, nil, errors.New("重定向URI不匹配")
    }

    // 标记授权码为已使用
    authCode.Used = true

    // 生成访问令牌
    accessToken := &AccessToken{
        Token:     generateRandomString(32),
        TokenType: "Bearer",
        ClientID:  clientID,
        UserID:    authCode.UserID,
        Scope:     authCode.Scope,
        ExpiresAt: time.Now().Add(1 * time.Hour), // 1小时有效期
    }

    // 生成刷新令牌
    refreshToken := &RefreshToken{
        Token:     generateRandomString(32),
        ClientID:  clientID,
        UserID:    authCode.UserID,
        Scope:     authCode.Scope,
        ExpiresAt: time.Now().Add(30 * 24 * time.Hour), // 30天有效期
    }

    s.accessTokens[accessToken.Token] = accessToken
    s.refreshTokens[refreshToken.Token] = refreshToken

    return accessToken, refreshToken, nil
}

// ValidateAccessToken 验证访问令牌
func (s *OAuthServer) ValidateAccessToken(token string) (*AccessToken, error) {
    accessToken, exists := s.accessTokens[token]
    if !exists {
        return nil, errors.New("访问令牌不存在")
    }

    if time.Now().After(accessToken.ExpiresAt) {
        return nil, errors.New("访问令牌已过期")
    }

    return accessToken, nil
}

// RefreshAccessToken 刷新访问令牌
func (s *OAuthServer) RefreshAccessToken(refreshTokenStr, clientID, clientSecret string) (*AccessToken, *RefreshToken, error) {
    // 验证客户端
    client, err := s.GetClient(clientID)
    if err != nil {
        return nil, nil, err
    }

    if client.Secret != clientSecret {
        return nil, nil, errors.New("客户端密钥错误")
    }

    // 验证刷新令牌
    refreshToken, exists := s.refreshTokens[refreshTokenStr]
    if !exists {
        return nil, nil, errors.New("刷新令牌不存在")
    }

    if time.Now().After(refreshToken.ExpiresAt) {
        return nil, nil, errors.New("刷新令牌已过期")
    }

    if refreshToken.ClientID != clientID {
        return nil, nil, errors.New("客户端ID不匹配")
    }

    // 生成新的访问令牌
    newAccessToken := &AccessToken{
        Token:     generateRandomString(32),
        TokenType: "Bearer",
        ClientID:  clientID,
        UserID:    refreshToken.UserID,
        Scope:     refreshToken.Scope,
        ExpiresAt: time.Now().Add(1 * time.Hour),
    }

    // 生成新的刷新令牌
    newRefreshToken := &RefreshToken{
        Token:     generateRandomString(32),
        ClientID:  clientID,
        UserID:    refreshToken.UserID,
        Scope:     refreshToken.Scope,
        ExpiresAt: time.Now().Add(30 * 24 * time.Hour),
    }

    // 删除旧令牌
    delete(s.refreshTokens, refreshTokenStr)

    // 存储新令牌
    s.accessTokens[newAccessToken.Token] = newAccessToken
    s.refreshTokens[newRefreshToken.Token] = newRefreshToken

    return newAccessToken, newRefreshToken, nil
}

// generateRandomString 生成随机字符串
func generateRandomString(length int) string {
    bytes := make([]byte, length)
    rand.Read(bytes)
    return base64.URLEncoding.EncodeToString(bytes)[:length]
}

OAuth 控制器 #

// controllers/oauth.go
package controllers

import (
    "net/http"
    "net/url"
    "strings"

    "github.com/gin-gonic/gin"
    "your-project/pkg/oauth"
)

type OAuthController struct {
    oauthServer *oauth.OAuthServer
}

func NewOAuthController(oauthServer *oauth.OAuthServer) *OAuthController {
    return &OAuthController{
        oauthServer: oauthServer,
    }
}

// AuthorizeRequest 授权请求参数
type AuthorizeRequest struct {
    ResponseType string `form:"response_type" binding:"required"`
    ClientID     string `form:"client_id" binding:"required"`
    RedirectURI  string `form:"redirect_uri" binding:"required"`
    Scope        string `form:"scope"`
    State        string `form:"state"`
}

// TokenRequest 令牌请求参数
type TokenRequest struct {
    GrantType    string `form:"grant_type" binding:"required"`
    Code         string `form:"code"`
    RedirectURI  string `form:"redirect_uri"`
    ClientID     string `form:"client_id" binding:"required"`
    ClientSecret string `form:"client_secret" binding:"required"`
    RefreshToken string `form:"refresh_token"`
}

// Authorize 授权端点
func (ctrl *OAuthController) Authorize(c *gin.Context) {
    var req AuthorizeRequest
    if err := c.ShouldBindQuery(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "invalid_request",
            "error_description": err.Error(),
        })
        return
    }

    // 验证响应类型
    if req.ResponseType != "code" {
        ctrl.redirectWithError(c, req.RedirectURI, req.State, "unsupported_response_type", "不支持的响应类型")
        return
    }

    // 验证客户端
    _, err := ctrl.oauthServer.GetClient(req.ClientID)
    if err != nil {
        ctrl.redirectWithError(c, req.RedirectURI, req.State, "invalid_client", "无效的客户端")
        return
    }

    // 验证重定向URI
    if err := ctrl.oauthServer.ValidateRedirectURI(req.ClientID, req.RedirectURI); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "invalid_request",
            "error_description": "无效的重定向URI",
        })
        return
    }

    // 检查用户是否已登录
    userID, exists := c.Get("user_id")
    if !exists {
        // 重定向到登录页面
        loginURL := "/login?redirect=" + url.QueryEscape(c.Request.URL.String())
        c.Redirect(http.StatusFound, loginURL)
        return
    }

    // 显示授权页面或直接授权(这里简化为直接授权)
    code, err := ctrl.oauthServer.GenerateAuthorizationCode(
        req.ClientID,
        userID.(string),
        req.RedirectURI,
        req.Scope,
    )
    if err != nil {
        ctrl.redirectWithError(c, req.RedirectURI, req.State, "server_error", "服务器错误")
        return
    }

    // 重定向到客户端
    redirectURL, _ := url.Parse(req.RedirectURI)
    query := redirectURL.Query()
    query.Set("code", code)
    if req.State != "" {
        query.Set("state", req.State)
    }
    redirectURL.RawQuery = query.Encode()

    c.Redirect(http.StatusFound, redirectURL.String())
}

// Token 令牌端点
func (ctrl *OAuthController) Token(c *gin.Context) {
    var req TokenRequest
    if err := c.ShouldBind(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "invalid_request",
            "error_description": err.Error(),
        })
        return
    }

    switch req.GrantType {
    case "authorization_code":
        ctrl.handleAuthorizationCodeGrant(c, &req)
    case "refresh_token":
        ctrl.handleRefreshTokenGrant(c, &req)
    default:
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "unsupported_grant_type",
            "error_description": "不支持的授权类型",
        })
    }
}

// handleAuthorizationCodeGrant 处理授权码授权
func (ctrl *OAuthController) handleAuthorizationCodeGrant(c *gin.Context, req *TokenRequest) {
    if req.Code == "" || req.RedirectURI == "" {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "invalid_request",
            "error_description": "缺少必需参数",
        })
        return
    }

    accessToken, refreshToken, err := ctrl.oauthServer.ExchangeCodeForToken(
        req.Code,
        req.ClientID,
        req.ClientSecret,
        req.RedirectURI,
    )
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "invalid_grant",
            "error_description": err.Error(),
        })
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "access_token":  accessToken.Token,
        "token_type":    accessToken.TokenType,
        "expires_in":    int(accessToken.ExpiresAt.Sub(time.Now()).Seconds()),
        "refresh_token": refreshToken.Token,
        "scope":         accessToken.Scope,
    })
}

// handleRefreshTokenGrant 处理刷新令牌授权
func (ctrl *OAuthController) handleRefreshTokenGrant(c *gin.Context, req *TokenRequest) {
    if req.RefreshToken == "" {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "invalid_request",
            "error_description": "缺少刷新令牌",
        })
        return
    }

    accessToken, refreshToken, err := ctrl.oauthServer.RefreshAccessToken(
        req.RefreshToken,
        req.ClientID,
        req.ClientSecret,
    )
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "invalid_grant",
            "error_description": err.Error(),
        })
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "access_token":  accessToken.Token,
        "token_type":    accessToken.TokenType,
        "expires_in":    int(accessToken.ExpiresAt.Sub(time.Now()).Seconds()),
        "refresh_token": refreshToken.Token,
        "scope":         accessToken.Scope,
    })
}

// redirectWithError 错误重定向
func (ctrl *OAuthController) redirectWithError(c *gin.Context, redirectURI, state, errorCode, errorDescription string) {
    redirectURL, _ := url.Parse(redirectURI)
    query := redirectURL.Query()
    query.Set("error", errorCode)
    query.Set("error_description", errorDescription)
    if state != "" {
        query.Set("state", state)
    }
    redirectURL.RawQuery = query.Encode()

    c.Redirect(http.StatusFound, redirectURL.String())
}

OAuth 2.0 客户端实现 #

// pkg/oauth/client.go
package oauth

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"

    "golang.org/x/oauth2"
)

// OAuthClient OAuth客户端
type OAuthClient struct {
    config *oauth2.Config
}

// NewOAuthClient 创建OAuth客户端
func NewOAuthClient(clientID, clientSecret, authURL, tokenURL, redirectURL string, scopes []string) *OAuthClient {
    config := &oauth2.Config{
        ClientID:     clientID,
        ClientSecret: clientSecret,
        RedirectURL:  redirectURL,
        Scopes:       scopes,
        Endpoint: oauth2.Endpoint{
            AuthURL:  authURL,
            TokenURL: tokenURL,
        },
    }

    return &OAuthClient{
        config: config,
    }
}

// GetAuthURL 获取授权URL
func (c *OAuthClient) GetAuthURL(state string) string {
    return c.config.AuthCodeURL(state, oauth2.AccessTypeOffline)
}

// ExchangeCode 用授权码换取令牌
func (c *OAuthClient) ExchangeCode(ctx context.Context, code string) (*oauth2.Token, error) {
    return c.config.Exchange(ctx, code)
}

// RefreshToken 刷新令牌
func (c *OAuthClient) RefreshToken(ctx context.Context, token *oauth2.Token) (*oauth2.Token, error) {
    tokenSource := c.config.TokenSource(ctx, token)
    return tokenSource.Token()
}

// GetUserInfo 获取用户信息
func (c *OAuthClient) GetUserInfo(ctx context.Context, token *oauth2.Token) (map[string]interface{}, error) {
    client := c.config.Client(ctx, token)

    resp, err := client.Get("http://localhost:8080/api/v1/user")
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("获取用户信息失败: %d", resp.StatusCode)
    }

    var userInfo map[string]interface{}
    if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
        return nil, err
    }

    return userInfo, nil
}

第三方登录集成 #

GitHub OAuth 集成 #

// pkg/oauth/github.go
package oauth

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"

    "golang.org/x/oauth2"
    "golang.org/x/oauth2/github"
)

// GitHubOAuth GitHub OAuth客户端
type GitHubOAuth struct {
    config *oauth2.Config
}

// NewGitHubOAuth 创建GitHub OAuth客户端
func NewGitHubOAuth(clientID, clientSecret, redirectURL string) *GitHubOAuth {
    config := &oauth2.Config{
        ClientID:     clientID,
        ClientSecret: clientSecret,
        RedirectURL:  redirectURL,
        Scopes:       []string{"user:email"},
        Endpoint:     github.Endpoint,
    }

    return &GitHubOAuth{
        config: config,
    }
}

// GetAuthURL 获取GitHub授权URL
func (g *GitHubOAuth) GetAuthURL(state string) string {
    return g.config.AuthCodeURL(state)
}

// ExchangeCode 用授权码换取令牌
func (g *GitHubOAuth) ExchangeCode(ctx context.Context, code string) (*oauth2.Token, error) {
    return g.config.Exchange(ctx, code)
}

// GitHubUser GitHub用户信息
type GitHubUser struct {
    ID        int    `json:"id"`
    Login     string `json:"login"`
    Name      string `json:"name"`
    Email     string `json:"email"`
    AvatarURL string `json:"avatar_url"`
}

// GetUserInfo 获取GitHub用户信息
func (g *GitHubOAuth) GetUserInfo(ctx context.Context, token *oauth2.Token) (*GitHubUser, error) {
    client := g.config.Client(ctx, token)

    resp, err := client.Get("https://api.github.com/user")
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("获取用户信息失败: %d", resp.StatusCode)
    }

    var user GitHubUser
    if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
        return nil, err
    }

    return &user, nil
}

社交登录控制器 #

// controllers/social.go
package controllers

import (
    "context"
    "net/http"

    "github.com/gin-gonic/gin"
    "your-project/pkg/oauth"
)

type SocialController struct {
    githubOAuth *oauth.GitHubOAuth
}

func NewSocialController(githubOAuth *oauth.GitHubOAuth) *SocialController {
    return &SocialController{
        githubOAuth: githubOAuth,
    }
}

// GitHubLogin GitHub登录
func (ctrl *SocialController) GitHubLogin(c *gin.Context) {
    state := generateRandomString(32)

    // 将state存储到session中(这里简化处理)
    c.SetCookie("oauth_state", state, 600, "/", "", false, true)

    authURL := ctrl.githubOAuth.GetAuthURL(state)
    c.Redirect(http.StatusFound, authURL)
}

// GitHubCallback GitHub回调
func (ctrl *SocialController) GitHubCallback(c *gin.Context) {
    // 验证state参数
    state := c.Query("state")
    cookieState, err := c.Cookie("oauth_state")
    if err != nil || state != cookieState {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "无效的state参数",
        })
        return
    }

    // 清除state cookie
    c.SetCookie("oauth_state", "", -1, "/", "", false, true)

    // 获取授权码
    code := c.Query("code")
    if code == "" {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "缺少授权码",
        })
        return
    }

    // 用授权码换取令牌
    ctx := context.Background()
    token, err := ctrl.githubOAuth.ExchangeCode(ctx, code)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{
            "error": "令牌交换失败",
        })
        return
    }

    // 获取用户信息
    githubUser, err := ctrl.githubOAuth.GetUserInfo(ctx, token)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{
            "error": "获取用户信息失败",
        })
        return
    }

    // 这里可以将GitHub用户信息与本地用户关联
    // 或创建新用户账户

    c.JSON(http.StatusOK, gin.H{
        "message": "登录成功",
        "user":    githubUser,
        "token":   token.AccessToken,
    })
}

OAuth 2.0 中间件 #

// middleware/oauth.go
package middleware

import (
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"
    "your-project/pkg/oauth"
)

// OAuthMiddleware OAuth认证中间件
func OAuthMiddleware(oauthServer *oauth.OAuthServer) gin.HandlerFunc {
    return func(c *gin.Context) {
        // 从Header中获取Authorization
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "unauthorized",
                "error_description": "缺少访问令牌",
            })
            c.Abort()
            return
        }

        // 检查Bearer前缀
        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "invalid_token",
                "error_description": "令牌格式错误",
            })
            c.Abort()
            return
        }

        // 验证访问令牌
        accessToken, err := oauthServer.ValidateAccessToken(parts[1])
        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "invalid_token",
                "error_description": err.Error(),
            })
            c.Abort()
            return
        }

        // 将令牌信息存储到上下文
        c.Set("client_id", accessToken.ClientID)
        c.Set("user_id", accessToken.UserID)
        c.Set("scope", accessToken.Scope)

        c.Next()
    }
}

// RequireScope 作用域验证中间件
func RequireScope(requiredScope string) gin.HandlerFunc {
    return func(c *gin.Context) {
        scope, exists := c.Get("scope")
        if !exists {
            c.JSON(http.StatusForbidden, gin.H{
                "error": "insufficient_scope",
                "error_description": "无法获取令牌作用域",
            })
            c.Abort()
            return
        }

        scopes := strings.Split(scope.(string), " ")
        for _, s := range scopes {
            if s == requiredScope {
                c.Next()
                return
            }
        }

        c.JSON(http.StatusForbidden, gin.H{
            "error": "insufficient_scope",
            "error_description": "权限不足",
        })
        c.Abort()
    }
}

完整示例 #

// main.go
package main

import (
    "log"

    "github.com/gin-gonic/gin"
    "your-project/controllers"
    "your-project/middleware"
    "your-project/pkg/oauth"
)

func main() {
    // 初始化OAuth服务器
    oauthServer := oauth.NewOAuthServer()

    // 注册客户端
    client := &oauth.Client{
        ID:           "test-client",
        Secret:       "test-secret",
        Name:         "测试客户端",
        RedirectURIs: []string{"http://localhost:3000/callback"},
        Scopes:       []string{"read", "write"},
    }
    oauthServer.RegisterClient(client)

    // 初始化GitHub OAuth
    githubOAuth := oauth.NewGitHubOAuth(
        "your-github-client-id",
        "your-github-client-secret",
        "http://localhost:8080/auth/github/callback",
    )

    // 初始化控制器
    oauthController := controllers.NewOAuthController(oauthServer)
    socialController := controllers.NewSocialController(githubOAuth)

    // 初始化路由
    r := gin.Default()

    // OAuth 2.0 端点
    r.GET("/oauth/authorize", oauthController.Authorize)
    r.POST("/oauth/token", oauthController.Token)

    // 社交登录
    r.GET("/auth/github", socialController.GitHubLogin)
    r.GET("/auth/github/callback", socialController.GitHubCallback)

    // 受保护的API
    api := r.Group("/api/v1")
    api.Use(middleware.OAuthMiddleware(oauthServer))
    {
        api.GET("/user", func(c *gin.Context) {
            userID, _ := c.Get("user_id")
            c.JSON(200, gin.H{
                "user_id": userID,
                "message": "用户信息",
            })
        })

        // 需要特定作用域的接口
        writeAPI := api.Group("/write")
        writeAPI.Use(middleware.RequireScope("write"))
        {
            writeAPI.POST("/data", func(c *gin.Context) {
                c.JSON(200, gin.H{"message": "数据已写入"})
            })
        }
    }

    log.Println("OAuth服务器启动在 :8080")
    r.Run(":8080")
}

OAuth 2.0 最佳实践 #

1. 安全考虑 #

  • 使用 HTTPS:所有 OAuth 流程必须使用 HTTPS
  • 状态参数:使用 state 参数防止 CSRF 攻击
  • 短期令牌:访问令牌应该有较短的有效期
  • 安全存储:客户端密钥必须安全存储

2. 令牌管理 #

  • 令牌撤销:提供令牌撤销机制
  • 作用域控制:实现细粒度的权限控制
  • 令牌刷新:合理使用刷新令牌机制

3. 错误处理 #

  • 标准错误码:使用 OAuth 2.0 标准错误码
  • 详细日志:记录详细的操作日志
  • 用户友好:提供用户友好的错误信息

OAuth 2.0 为现代应用提供了标准化的授权解决方案,通过合理的实现可以构建安全、可扩展的认证授权系统。