一、灵魂拷问:OAuth 到底解决了什么问题?

1.1 没有 OAuth 的黑暗时代

假设你注册了一个叫"今日头条"的 App,后来又想用"今日头条"的账号一键登录"知乎"。

在没有 OAuth 的年代,流程是这样的:

  1. 你在知乎登录页输入今日头条的用户名 + 密码

  2. 知乎拿着你的账号密码,模拟浏览器请求今日头条的接口,获取你的个人信息。

  3. 知乎显示"登录成功"。

问题出在哪?

你把家的万能钥匙(密码)交给了一个陌生人(知乎)。 知乎不仅知道了你在头条的所有信息,还拥有了随时冒充你登录头条的能力。更可怕的是,如果你在头条和支付宝用了同一个密码……

这就是经典的 "密码共享反模式"(Password Anti-Pattern)

1.2 OAuth 的终极答案

OAuth(开放授权)的核心思想是:

在不暴露用户密码的前提下,颁发一张有时效、有范围、可撤销的"临时通行证"(令牌),让第三方应用能够有限制地访问用户资源。

用一句话概括:授权 ≠ 认证。OAuth 管的是"你能做什么"(授权),而不是"你是谁"(认证)——当然,加上 OIDC 后它也管认证,这是后话。


二、OAuth 2.0 的"门禁卡"哲学(文科生也能秒懂)

想象你要进一个高档别墅区送外卖:

OAuth 角色 现实映射 职责
资源拥有者(Resource Owner) 业主(你) 拥有小区内财产(数据)的人
客户端(Client) 外卖员(第三方 App) 想要进入小区取/送东西的应用
授权服务器(Authorization Server) 物业前台 核验身份、发放临时访客卡
资源服务器(Resource Server) 业主家的智能快递柜 存放用户实际数据的地方

一次完美的 OAuth 授权流程如下:

  1. 业主(你)告诉物业:"等会儿有个外卖员来取我的包裹,你给他放行。"

  2. 物业核实业主身份后,生成一张临时访客卡(Access Token)。

  3. 业主把访客卡转交给外卖员。

  4. 外卖员刷访客卡进门,全程不知道业主家的门锁密码

  5. 访客卡有严格限制:

    • 时效性:30 分钟后自动失效。

    • 范围性:只能刷开 8 号楼的单元门,刷不开地下车库(Scope 权限范围)。

    • 可撤销:业主随时可以给物业打电话挂失这张卡。

你看,OAuth 就是这么朴实无华。 我们再来对应一下专业术语:

  • 临时访客卡 = Access Token(访问令牌)

  • 30 分钟失效 = expires_in(有效期)

  • 只能刷 8 号楼 = Scope(权限范围)

  • 挂失 = Token Revocation(令牌撤销)


三、核心概念扫盲(背下来,面试稳了)

3.1 四大角色

角色 英文 核心职责
资源拥有者 Resource Owner 授权的主体,通常是"用户本人"
客户端 Client 请求访问资源的第三方应用(如:CSDN、知乎)
授权服务器 Authorization Server 认证用户身份、颁发 Access Token 的官方系统(如:微信后台)
资源服务器 Resource Server 实际存储用户数据的服务器,通过校验 Token 来决定是否返回数据

注意:授权服务器和资源服务器可以是同一台服务器,也可以是分离的。在大型互联网公司(如 Google、微信),它们通常是独立的微服务集群。

3.2 两个核心令牌

令牌 英文 说明
访问令牌 Access Token 用于调用资源服务器 API 的凭证。有效期短(通常 1~2 小时)。
刷新令牌 Refresh Token 用于在 Access Token 过期后换取新的 Access Token。有效期长(通常 7~30 天),必须安全存储

3.3 权限范围(Scope)

Scope 是 OAuth 实现"最小权限原则"的关键。举个例子,你用微信登录某个小游戏:

  • scope=profile:允许读取你的昵称和头像。

  • scope=email:允许读取你的邮箱地址。

  • scope=friends:允许读取你的好友列表(小游戏一般不会申请这个)。

用户同意授权时,可以清晰地看到"这个 App 要拿走我哪些数据",从而做出知情决策。


四、四种授权模式(Grant Types)详解

OAuth 2.0 定义了 4 种授权模式,适用于不同的应用场景。

模式 英文名称 适用场景 安全性
授权码模式 Authorization Code Web 应用、移动 App(有后端服务器) ⭐⭐⭐⭐⭐(最安全)
隐式模式 Implicit 纯前端单页应用(SPA) ⭐⭐(已弃用
密码模式 Resource Owner Password Credentials 官方第一方 App(如微信官方客户端) ⭐⭐⭐(不推荐第三方使用)
客户端凭证模式 Client Credentials 机器与机器之间的后端通信 ⭐⭐⭐⭐(无用户参与)

4.1 重点:授权码模式(Authorization Code)

这是互联网大厂的唯一选择(微信、Google、GitHub 全部采用此模式)。它的核心设计是 "前后端分离"

  • 前端:只负责获取一次性的"授权码"(Authorization Code)。

  • 后端:用授权码 + 客户端密钥(Client Secret)去换取 Access Token。

为什么要多一步"换码"操作?

因为 Client Secret 绝对不能暴露在浏览器端。如果在前端直接返回 Access Token,容易被浏览器插件或网络中间人截获。而通过后端换取,Token 走的是服务器之间的 HTTPS 通道,安全性大大提高。

4.2 注意:隐式模式(Implicit)已被废弃

OAuth 2.1 草案已经正式废弃了隐式模式。如果你还在用,赶紧迁移到 "授权码模式 + PKCE"(Proof Key for Code Exchange),后者是专门为 SPA 和移动 App 设计的、无需 Client Secret 的安全方案。


五、全网最细:授权码模式完整流程图解

这是本文的核心中的核心,建议对着图看 3 遍。

流程中的关键安全设计:

步骤 安全机制 作用
Step 4 & 8 state 参数 防止 CSRF 攻击。客户端生成随机 state 并存储 session,回调时比对是否一致。
Step 7 授权码(code) 一次性使用,有效期极短(通常 1~5 分钟)。即便被截获,没有 client_secret 也无法换 token。
Step 10 client_secret 仅在服务器间传输,永远不暴露给浏览器。
Step 12 access_token 不包含用户敏感信息(建议使用 opaque token 或加密的 JWT)。

六、实战:用 Go + Gin 搭建完整的 OAuth 2.0 授权中心

理论说完,直接上代码。我们将用 Go 搭建一个完整的 OAuth 2.0 授权中心,支持授权码模式和 Refresh Token,所有代码手写,不依赖第三方 OAuth2 服务端库(只使用 JWT 库进行 Token 签名)。

环境要求:Go 1.21+,MySQL 8.0+,Redis 7.0+(可选,用于存储 Token)

6.1 项目结构

text

oauth2-authorization-server-go/
├── cmd/
│   └── server/
│       └── main.go                    # 程序入口
├── internal/
│   ├── config/
│   │   └── config.go                  # 配置管理
│   ├── model/
│   │   ├── client.go                  # 客户端模型
│   │   ├── user.go                    # 用户模型
│   │   ├── authorization_code.go      # 授权码模型
│   │   └── token.go                   # Token 模型
│   ├── repository/
│   │   ├── client_repo.go             # 客户端数据访问
│   │   ├── user_repo.go               # 用户数据访问
│   │   ├── code_repo.go               # 授权码数据访问
│   │   └── token_repo.go              # Token 数据访问
│   ├── service/
│   │   ├── auth_service.go            # 授权核心逻辑
│   │   ├── token_service.go           # Token 生成/验证
│   │   └── user_service.go            # 用户服务
│   ├── handler/
│   │   ├── authorize_handler.go       # /oauth2/authorize 端点
│   │   ├── token_handler.go           # /oauth2/token 端点
│   │   ├── userinfo_handler.go        # /userinfo 端点
│   │   └── login_handler.go           # 登录页面
│   ├── middleware/
│   │   ├── auth_middleware.go         # Token 鉴权中间件
│   │   └── cors_middleware.go         # CORS 中间件
│   └── util/
│       ├── jwt.go                     # JWT 工具
│       └── crypto.go                  # 加密工具
├── pkg/
│   └── db/
│       └── mysql.go                   # 数据库连接
├── web/
│   └── templates/
│       ├── login.html                 # 登录页面
│       └── authorize.html             # 授权确认页面
├── go.mod
├── go.sum
└── config.yaml                        # 配置文件

6.2 核心依赖(go.mod)

go

module github.com/yourusername/oauth2-auth-server

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/golang-jwt/jwt/v5 v5.2.0
    github.com/google/uuid v1.5.0
    gorm.io/driver/mysql v1.5.2
    gorm.io/gorm v1.25.5
    golang.org/x/crypto v0.18.0
    github.com/go-redis/redis/v8 v8.11.5
    github.com/spf13/viper v1.18.2
)

6.3 配置文件(config.yaml)

yaml

server:
  port: 9000

database:
  driver: mysql
  dsn: "root:password@tcp(127.0.0.1:3306)/oauth2_db?charset=utf8mb4&parseTime=True&loc=Local"

redis:
  addr: "127.0.0.1:6379"
  password: ""
  db: 0

jwt:
  private_key_path: "./keys/private.pem"   # RSA 私钥
  public_key_path: "./keys/public.pem"     # RSA 公钥
  access_token_ttl: 7200                   # 2 小时(秒)
  refresh_token_ttl: 604800                # 7 天(秒)
  issuer: "http://localhost:9000"

oauth2:
  authorization_code_ttl: 300              # 授权码 5 分钟过期(秒)

6.4 数据模型(Model)

internal/model/user.go - 用户模型(资源拥有者)

go

package model

import (
    "time"
    "gorm.io/gorm"
)

type User struct {
    ID           string    `gorm:"primaryKey;type:varchar(36)" json:"id"`
    Username     string    `gorm:"uniqueIndex;type:varchar(50)" json:"username"`
    PasswordHash string    `gorm:"type:varchar(255)" json:"-"`
    Email        string    `gorm:"type:varchar(100)" json:"email"`
    FullName     string    `gorm:"type:varchar(100)" json:"full_name"`
    CreatedAt    time.Time `json:"created_at"`
    UpdatedAt    time.Time `json:"updated_at"`
}

func (User) TableName() string {
    return "users"
}

internal/model/client.go - 客户端模型

go

package model

import (
    "time"
)

type Client struct {
    ID           string    `gorm:"primaryKey;type:varchar(36)" json:"id"`
    ClientID     string    `gorm:"uniqueIndex;type:varchar(100)" json:"client_id"`
    ClientSecret string    `gorm:"type:varchar(255)" json:"-"`
    ClientName   string    `gorm:"type:varchar(100)" json:"client_name"`
    RedirectURIs string    `gorm:"type:text" json:"redirect_uris"`  // JSON 数组
    Scopes       string    `gorm:"type:text" json:"scopes"`         // JSON 数组
    GrantTypes   string    `gorm:"type:text" json:"grant_types"`    // JSON 数组
    IsActive     bool      `gorm:"default:true" json:"is_active"`
    CreatedAt    time.Time `json:"created_at"`
    UpdatedAt    time.Time `json:"updated_at"`
}

func (Client) TableName() string {
    return "clients"
}

internal/model/authorization_code.go - 授权码模型

go

package model

import (
    "time"
)

type AuthorizationCode struct {
    ID         string    `gorm:"primaryKey;type:varchar(36)" json:"id"`
    Code       string    `gorm:"uniqueIndex;type:varchar(100)" json:"code"`
    ClientID   string    `gorm:"type:varchar(100)" json:"client_id"`
    UserID     string    `gorm:"type:varchar(36)" json:"user_id"`
    RedirectURI string   `gorm:"type:varchar(255)" json:"redirect_uri"`
    Scopes     string    `gorm:"type:text" json:"scopes"`
    State      string    `gorm:"type:varchar(100)" json:"state"`
    ExpiresAt  time.Time `json:"expires_at"`
    IsUsed     bool      `gorm:"default:false" json:"is_used"`
    CreatedAt  time.Time `json:"created_at"`
}

func (AuthorizationCode) TableName() string {
    return "authorization_codes"
}

6.5 JWT 工具(internal/util/jwt.go)

这是整个授权中心的核心——Token 的生成与验证。

go

package util

import (
    "crypto/rsa"
    "crypto/x509"
    "encoding/pem"
    "errors"
    "fmt"
    "os"
    "time"

    "github.com/golang-jwt/jwt/v5"
    "github.com/google/uuid"
)

type JWTUtil struct {
    privateKey      *rsa.PrivateKey
    publicKey       *rsa.PublicKey
    accessTokenTTL  int64
    refreshTokenTTL int64
    issuer          string
}

// TokenClaims JWT 自定义 Claims
type TokenClaims struct {
    UserID string   `json:"user_id"`
    Scope  []string `json:"scope"`
    ClientID string `json:"client_id"`
    jwt.RegisteredClaims
}

// TokenPair Token 对
type TokenPair struct {
    AccessToken  string `json:"access_token"`
    RefreshToken string `json:"refresh_token"`
    TokenType    string `json:"token_type"`
    ExpiresIn    int64  `json:"expires_in"`
    Scope        string `json:"scope,omitempty"`
}

// NewJWTUtil 创建 JWT 工具实例
func NewJWTUtil(privateKeyPath, publicKeyPath string, accessTTL, refreshTTL int64, issuer string) (*JWTUtil, error) {
    privateKey, err := loadPrivateKey(privateKeyPath)
    if err != nil {
        return nil, fmt.Errorf("加载私钥失败: %w", err)
    }

    publicKey, err := loadPublicKey(publicKeyPath)
    if err != nil {
        return nil, fmt.Errorf("加载公钥失败: %w", err)
    }

    return &JWTUtil{
        privateKey:      privateKey,
        publicKey:       publicKey,
        accessTokenTTL:  accessTTL,
        refreshTokenTTL: refreshTTL,
        issuer:          issuer,
    }, nil
}

// GenerateTokenPair 生成 Access Token + Refresh Token
func (j *JWTUtil) GenerateTokenPair(userID, clientID string, scopes []string) (*TokenPair, error) {
    now := time.Now()

    // 生成 Access Token
    accessClaims := &TokenClaims{
        UserID:   userID,
        Scope:    scopes,
        ClientID: clientID,
        RegisteredClaims: jwt.RegisteredClaims{
            ID:        uuid.New().String(),
            Subject:   userID,
            Issuer:    j.issuer,
            Audience:  []string{clientID},
            IssuedAt:  jwt.NewNumericDate(now),
            ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(j.accessTokenTTL) * time.Second)),
        },
    }

    accessToken, err := jwt.NewWithClaims(jwt.SigningMethodRS256, accessClaims).SignedString(j.privateKey)
    if err != nil {
        return nil, fmt.Errorf("生成 Access Token 失败: %w", err)
    }

    // 生成 Refresh Token(使用不同的 Claims,更长的过期时间)
    refreshClaims := &TokenClaims{
        UserID:   userID,
        ClientID: clientID,
        RegisteredClaims: jwt.RegisteredClaims{
            ID:        uuid.New().String(),
            Subject:   userID,
            Issuer:    j.issuer,
            Audience:  []string{clientID},
            IssuedAt:  jwt.NewNumericDate(now),
            ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(j.refreshTokenTTL) * time.Second)),
        },
    }

    refreshToken, err := jwt.NewWithClaims(jwt.SigningMethodRS256, refreshClaims).SignedString(j.privateKey)
    if err != nil {
        return nil, fmt.Errorf("生成 Refresh Token 失败: %w", err)
    }

    return &TokenPair{
        AccessToken:  accessToken,
        RefreshToken: refreshToken,
        TokenType:    "Bearer",
        ExpiresIn:    j.accessTokenTTL,
        Scope:        strings.Join(scopes, " "),
    }, nil
}

// ValidateToken 验证 Token 并返回 Claims
func (j *JWTUtil) ValidateToken(tokenString string) (*TokenClaims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, fmt.Errorf("非预期的签名方法: %v", token.Header["alg"])
        }
        return j.publicKey, nil
    })

    if err != nil {
        return nil, err
    }

    if claims, ok := token.Claims.(*TokenClaims); ok && token.Valid {
        return claims, nil
    }

    return nil, errors.New("无效的 Token")
}

// RefreshAccessToken 用 Refresh Token 换取新的 Access Token
func (j *JWTUtil) RefreshAccessToken(refreshTokenString string) (string, error) {
    claims, err := j.ValidateToken(refreshTokenString)
    if err != nil {
        return "", fmt.Errorf("Refresh Token 验证失败: %w", err)
    }

    // 生成新的 Access Token
    now := time.Now()
    newClaims := &TokenClaims{
        UserID:   claims.UserID,
        Scope:    claims.Scope,
        ClientID: claims.ClientID,
        RegisteredClaims: jwt.RegisteredClaims{
            ID:        uuid.New().String(),
            Subject:   claims.UserID,
            Issuer:    j.issuer,
            Audience:  []string{claims.ClientID},
            IssuedAt:  jwt.NewNumericDate(now),
            ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(j.accessTokenTTL) * time.Second)),
        },
    }

    newToken, err := jwt.NewWithClaims(jwt.SigningMethodRS256, newClaims).SignedString(j.privateKey)
    if err != nil {
        return "", fmt.Errorf("生成新 Access Token 失败: %w", err)
    }

    return newToken, nil
}

// 辅助函数:加载 RSA 私钥
func loadPrivateKey(path string) (*rsa.PrivateKey, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }

    block, _ := pem.Decode(data)
    if block == nil {
        return nil, errors.New("无法解码 PEM 块")
    }

    return x509.ParsePKCS8PrivateKey(block.Bytes)
}

// 辅助函数:加载 RSA 公钥
func loadPublicKey(path string) (*rsa.PublicKey, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }

    block, _ := pem.Decode(data)
    if block == nil {
        return nil, errors.New("无法解码 PEM 块")
    }

    pub, err := x509.ParsePKIXPublicKey(block.Bytes)
    if err != nil {
        return nil, err
    }

    rsaPub, ok := pub.(*rsa.PublicKey)
    if !ok {
        return nil, errors.New("不是 RSA 公钥")
    }

    return rsaPub, nil
}

6.6 授权服务(internal/service/auth_service.go)

这是 OAuth 2.0 的核心业务逻辑。

go

package service

import (
    "crypto/rand"
    "encoding/base64"
    "encoding/json"
    "errors"
    "fmt"
    "strings"
    "time"

    "github.com/google/uuid"
    "gorm.io/gorm"

    "github.com/yourusername/oauth2-auth-server/internal/model"
    "github.com/yourusername/oauth2-auth-server/internal/repository"
    "github.com/yourusername/oauth2-auth-server/internal/util"
)

type AuthService struct {
    clientRepo    *repository.ClientRepo
    userRepo      *repository.UserRepo
    codeRepo      *repository.CodeRepo
    tokenRepo     *repository.TokenRepo
    jwtUtil       *util.JWTUtil
    codeTTL       int64
}

func NewAuthService(
    clientRepo *repository.ClientRepo,
    userRepo *repository.UserRepo,
    codeRepo *repository.CodeRepo,
    tokenRepo *repository.TokenRepo,
    jwtUtil *util.JWTUtil,
    codeTTL int64,
) *AuthService {
    return &AuthService{
        clientRepo: clientRepo,
        userRepo:   userRepo,
        codeRepo:   codeRepo,
        tokenRepo:  tokenRepo,
        jwtUtil:    jwtUtil,
        codeTTL:    codeTTL,
    }
}

// ValidateClient 验证客户端
func (s *AuthService) ValidateClient(clientID, redirectURI string) (*model.Client, error) {
    client, err := s.clientRepo.FindByClientID(clientID)
    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, errors.New("客户端不存在")
        }
        return nil, err
    }

    if !client.IsActive {
        return nil, errors.New("客户端已被禁用")
    }

    // 校验 redirect_uri 是否匹配
    var redirectURIs []string
    if err := json.Unmarshal([]byte(client.RedirectURIs), &redirectURIs); err != nil {
        return nil, errors.New("客户端配置错误")
    }

    matched := false
    for _, uri := range redirectURIs {
        if uri == redirectURI {
            matched = true
            break
        }
    }
    if !matched {
        return nil, errors.New("redirect_uri 不匹配")
    }

    return client, nil
}

// ValidateClientSecret 验证客户端密钥(用于 Token 端点)
func (s *AuthService) ValidateClientSecret(clientID, clientSecret string) (*model.Client, error) {
    client, err := s.clientRepo.FindByClientID(clientID)
    if err != nil {
        return nil, errors.New("客户端不存在")
    }

    // 使用 bcrypt 比较(假设密码已用 bcrypt 存储)
    if err := util.ComparePassword(client.ClientSecret, clientSecret); err != nil {
        return nil, errors.New("客户端密钥错误")
    }

    if !client.IsActive {
        return nil, errors.New("客户端已被禁用")
    }

    return client, nil
}

// GenerateAuthorizationCode 生成授权码
func (s *AuthService) GenerateAuthorizationCode(clientID, userID, redirectURI, state string, scopes []string) (string, error) {
    // 生成随机的授权码
    codeBytes := make([]byte, 32)
    if _, err := rand.Read(codeBytes); err != nil {
        return "", err
    }
    code := base64.URLEncoding.EncodeToString(codeBytes)

    scopesJSON, _ := json.Marshal(scopes)

    authCode := &model.AuthorizationCode{
        ID:          uuid.New().String(),
        Code:        code,
        ClientID:    clientID,
        UserID:      userID,
        RedirectURI: redirectURI,
        Scopes:      string(scopesJSON),
        State:       state,
        ExpiresAt:   time.Now().Add(time.Duration(s.codeTTL) * time.Second),
        IsUsed:      false,
    }

    if err := s.codeRepo.Create(authCode); err != nil {
        return "", err
    }

    return code, nil
}

// ExchangeCodeForToken 用授权码换取 Token
func (s *AuthService) ExchangeCodeForToken(code, clientID, clientSecret, redirectURI string) (*util.TokenPair, error) {
    // 1. 验证客户端
    client, err := s.ValidateClientSecret(clientID, clientSecret)
    if err != nil {
        return nil, err
    }

    // 2. 查询授权码
    authCode, err := s.codeRepo.FindByCode(code)
    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, errors.New("授权码不存在")
        }
        return nil, err
    }

    // 3. 校验授权码
    if authCode.IsUsed {
        return nil, errors.New("授权码已被使用")
    }
    if time.Now().After(authCode.ExpiresAt) {
        return nil, errors.New("授权码已过期")
    }
    if authCode.ClientID != clientID {
        return nil, errors.New("客户端不匹配")
    }
    if authCode.RedirectURI != redirectURI {
        return nil, errors.New("redirect_uri 不匹配")
    }

    // 4. 标记授权码为已使用
    if err := s.codeRepo.MarkAsUsed(authCode.ID); err != nil {
        return nil, err
    }

    // 5. 解析 Scopes
    var scopes []string
    if err := json.Unmarshal([]byte(authCode.Scopes), &scopes); err != nil {
        scopes = []string{}
    }

    // 6. 生成 Token
    tokenPair, err := s.jwtUtil.GenerateTokenPair(authCode.UserID, clientID, scopes)
    if err != nil {
        return nil, err
    }

    // 7. 存储 Token(用于撤销和审计)
    // 注意:JWT 本身是自包含的,存储是为了支持撤销和统计
    tokenRecord := &model.Token{
        ID:            uuid.New().String(),
        AccessToken:   tokenPair.AccessToken,
        RefreshToken:  tokenPair.RefreshToken,
        UserID:        authCode.UserID,
        ClientID:      clientID,
        Scopes:        string(scopesJSON),
        AccessExpiresAt: time.Now().Add(time.Duration(s.jwtUtil.accessTokenTTL) * time.Second),
        RefreshExpiresAt: time.Now().Add(time.Duration(s.jwtUtil.refreshTokenTTL) * time.Second),
        IsRevoked:     false,
    }
    _ = s.tokenRepo.Create(tokenRecord) // 非关键路径,忽略错误

    return tokenPair, nil
}

// RefreshToken 刷新 Access Token
func (s *AuthService) RefreshToken(refreshToken, clientID, clientSecret string) (*util.TokenPair, error) {
    // 1. 验证客户端
    if _, err := s.ValidateClientSecret(clientID, clientSecret); err != nil {
        return nil, err
    }

    // 2. 验证 Refresh Token
    claims, err := s.jwtUtil.ValidateToken(refreshToken)
    if err != nil {
        return nil, errors.New("Refresh Token 无效")
    }

    // 3. 检查 Token 是否被撤销
    revoked, err := s.tokenRepo.IsRefreshTokenRevoked(refreshToken)
    if err != nil {
        return nil, err
    }
    if revoked {
        return nil, errors.New("Refresh Token 已被撤销")
    }

    // 4. 生成新的 Access Token
    newAccessToken, err := s.jwtUtil.RefreshAccessToken(refreshToken)
    if err != nil {
        return nil, err
    }

    // 5. 返回新的 Token Pair(Refresh Token 不变)
    return &util.TokenPair{
        AccessToken:  newAccessToken,
        RefreshToken: refreshToken,
        TokenType:    "Bearer",
        ExpiresIn:    s.jwtUtil.accessTokenTTL,
        Scope:        strings.Join(claims.Scope, " "),
    }, nil
}

// AuthenticateUser 验证用户凭据(登录)
func (s *AuthService) AuthenticateUser(username, password string) (*model.User, error) {
    user, err := s.userRepo.FindByUsername(username)
    if err != nil {
        return nil, errors.New("用户名或密码错误")
    }

    if err := util.ComparePassword(user.PasswordHash, password); err != nil {
        return nil, errors.New("用户名或密码错误")
    }

    return user, nil
}

6.7 核心 Handler - 授权端点(internal/handler/authorize_handler.go)

go

package handler

import (
    "encoding/json"
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"

    "github.com/yourusername/oauth2-auth-server/internal/service"
)

type AuthorizeHandler struct {
    authService *service.AuthService
}

func NewAuthorizeHandler(authService *service.AuthService) *AuthorizeHandler {
    return &AuthorizeHandler{authService: authService}
}

// Authorize GET /oauth2/authorize
// 处理授权请求,重定向到登录页或授权确认页
func (h *AuthorizeHandler) Authorize(c *gin.Context) {
    clientID := c.Query("client_id")
    redirectURI := c.Query("redirect_uri")
    responseType := c.Query("response_type")
    scope := c.Query("scope")
    state := c.Query("state")

    // 1. 基础参数校验
    if clientID == "" || redirectURI == "" || responseType == "" {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request", "error_description": "缺少必要参数"})
        return
    }

    if responseType != "code" {
        c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported_response_type", "error_description": "仅支持 code 类型"})
        return
    }

    // 2. 验证客户端
    client, err := h.authService.ValidateClient(clientID, redirectURI)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_client", "error_description": err.Error()})
        return
    }

    // 3. 检查用户是否已登录(通过 session/cookie)
    userID, exists := c.Get("user_id")
    if !exists {
        // 未登录,保存授权请求参数到 session,跳转登录

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐