OAuth 2.0 总结:从“门禁卡“哲学到 Gin 生产级实战
一、灵魂拷问:OAuth 到底解决了什么问题?
1.1 没有 OAuth 的黑暗时代
假设你注册了一个叫"今日头条"的 App,后来又想用"今日头条"的账号一键登录"知乎"。
在没有 OAuth 的年代,流程是这样的:
-
你在知乎登录页输入今日头条的用户名 + 密码。
-
知乎拿着你的账号密码,模拟浏览器请求今日头条的接口,获取你的个人信息。
-
知乎显示"登录成功"。
问题出在哪?
你把家的万能钥匙(密码)交给了一个陌生人(知乎)。 知乎不仅知道了你在头条的所有信息,还拥有了随时冒充你登录头条的能力。更可怕的是,如果你在头条和支付宝用了同一个密码……
这就是经典的 "密码共享反模式"(Password Anti-Pattern)。
1.2 OAuth 的终极答案
OAuth(开放授权)的核心思想是:
在不暴露用户密码的前提下,颁发一张有时效、有范围、可撤销的"临时通行证"(令牌),让第三方应用能够有限制地访问用户资源。
用一句话概括:授权 ≠ 认证。OAuth 管的是"你能做什么"(授权),而不是"你是谁"(认证)——当然,加上 OIDC 后它也管认证,这是后话。
二、OAuth 2.0 的"门禁卡"哲学(文科生也能秒懂)
想象你要进一个高档别墅区送外卖:
| OAuth 角色 | 现实映射 | 职责 |
|---|---|---|
| 资源拥有者(Resource Owner) | 业主(你) | 拥有小区内财产(数据)的人 |
| 客户端(Client) | 外卖员(第三方 App) | 想要进入小区取/送东西的应用 |
| 授权服务器(Authorization Server) | 物业前台 | 核验身份、发放临时访客卡 |
| 资源服务器(Resource Server) | 业主家的智能快递柜 | 存放用户实际数据的地方 |
一次完美的 OAuth 授权流程如下:
-
业主(你)告诉物业:"等会儿有个外卖员来取我的包裹,你给他放行。"
-
物业核实业主身份后,生成一张临时访客卡(Access Token)。
-
业主把访客卡转交给外卖员。
-
外卖员刷访客卡进门,全程不知道业主家的门锁密码。
-
访客卡有严格限制:
-
时效性: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,跳转登录
更多推荐

所有评论(0)