1. JWT 认证

JWT(JSON Web Token)是无状态的认证方案,由三部分组成:

HeadereyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
PayloadeyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIn0
SignatureHMACSHA256(base64(header)+"."+base64(payload), secret)
go get github.com/golang-jwt/jwt/v5
package auth

import (
    "errors"
    "time"
    "github.com/golang-jwt/jwt/v5"
)

type TokenConfig struct {
    AccessSecret  []byte
    RefreshSecret []byte
    AccessTTL     time.Duration // 15 分钟
    RefreshTTL    time.Duration // 7 天
}

type Claims struct {
    UserID int64  `json:"uid"`
    Email  string `json:"email"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

type TokenPair struct {
    AccessToken  string `json:"access_token"`
    RefreshToken string `json:"refresh_token"`
    ExpiresAt    int64  `json:"expires_at"`
}

type JWTService struct {
    cfg TokenConfig
}

func NewJWTService(cfg TokenConfig) *JWTService {
    return &JWTService{cfg: cfg}
}

// 生成 Token 对 (access + refresh)
func (s *JWTService) GenerateTokenPair(userID int64, email, role string) (*TokenPair, error) {
    now := time.Now()
    accessExpires := now.Add(s.cfg.AccessTTL)

    // Access Token
    accessClaims := Claims{
        UserID: userID,
        Email:  email,
        Role:   role,
        RegisteredClaims: jwt.RegisteredClaims{
            Subject:   fmt.Sprintf("%d", userID),
            IssuedAt:  jwt.NewNumericDate(now),
            ExpiresAt: jwt.NewNumericDate(accessExpires),
            Issuer:    "myapp",
        },
    }
    accessToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).
        SignedString(s.cfg.AccessSecret)
    if err != nil {
        return nil, err
    }

    // Refresh Token (只存 userID,权限少)
    refreshClaims := jwt.RegisteredClaims{
        Subject:   fmt.Sprintf("%d", userID),
        IssuedAt:  jwt.NewNumericDate(now),
        ExpiresAt: jwt.NewNumericDate(now.Add(s.cfg.RefreshTTL)),
        Issuer:    "myapp",
    }
    refreshToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).
        SignedString(s.cfg.RefreshSecret)
    if err != nil {
        return nil, err
    }

    return &TokenPair{
        AccessToken:  accessToken,
        RefreshToken: refreshToken,
        ExpiresAt:    accessExpires.Unix(),
    }, nil
}

// 验证 Access Token
func (s *JWTService) ValidateAccessToken(tokenStr string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (any, error) {
        if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("意外的签名方法: %v", t.Header["alg"])
        }
        return s.cfg.AccessSecret, nil
    })
    if err != nil {
        if errors.Is(err, jwt.ErrTokenExpired) {
            return nil, ErrTokenExpired
        }
        return nil, ErrTokenInvalid
    }

    claims, ok := token.Claims.(*Claims)
    if !ok || !token.Valid {
        return nil, ErrTokenInvalid
    }
    return claims, nil
}

var (
    ErrTokenExpired = errors.New("token 已过期")
    ErrTokenInvalid = errors.New("token 无效")
)

Token 刷新流程

客户端 (Client)
服务器 (Server)
— 登录,获取 Token 对 —
POST /api/v1/auth/login
携带用户名 + 密码
{"access_token":"…","refresh_token":"…"}
access 有效期 15 min,refresh 有效期 7 天
— 正常请求(access_token 有效期内)—
GET /api/v1/users
Authorization: Bearer <access_token>
携带 access_token 请求资源
200 OK {"data":[...]}
正常返回数据
— access_token 过期后,用 refresh_token 换新 —
POST /api/v1/auth/refresh
{"refresh_token":"…"}
access_token 已过期,发起刷新
{"access_token":"new_token…"}
返回新 access_token,无需重新登录
GET /api/v1/users
Authorization: Bearer <new_access_token>
继续正常请求
package handler

// POST /api/v1/auth/login
func (h *AuthHandler) Login(c *gin.Context) {
    var req struct {
        Email    string `json:"email"    binding:"required,email"`
        Password string `json:"password" binding:"required"`
    }
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }

    user, err := h.userSvc.FindByEmail(c.Request.Context(), req.Email)
    if err != nil || !checkPassword(req.Password, user.Password) {
        // 注意:不区分"用户不存在"和"密码错误",防止用户枚举
        c.JSON(401, gin.H{"error": "邮箱或密码错误"})
        return
    }

    if !user.Active {
        c.JSON(403, gin.H{"error": "账户已被禁用"})
        return
    }

    tokens, err := h.jwtSvc.GenerateTokenPair(user.ID, user.Email, user.Role)
    if err != nil {
        c.JSON(500, gin.H{"error": "Token 生成失败"})
        return
    }

    // Refresh Token 存入 HttpOnly Cookie (防 XSS)
    c.SetCookie("refresh_token", tokens.RefreshToken, 7*24*3600,
        "/api/v1/auth/refresh", "", true, true)

    c.JSON(200, gin.H{
        "access_token": tokens.AccessToken,
        "expires_at":   tokens.ExpiresAt,
    })
}

// 密码哈希 (bcrypt)
import "golang.org/x/crypto/bcrypt"

func hashPassword(password string) (string, error) {
    b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    return string(b), err
}

func checkPassword(password, hash string) bool {
    return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
}

2. OAuth 2.0 第三方登录

ℹ️

OAuth 2.0 授权码流程

用户点击"用 GitHub 登录" → 跳转 GitHub 授权页 → 用户同意 → GitHub 回调带 code → 服务器用 code 换 access_token → 获取用户信息

go get golang.org/x/oauth2
package oauth

import (
    "golang.org/x/oauth2"
    "golang.org/x/oauth2/github"
    "golang.org/x/oauth2/google"
)

// GitHub OAuth 配置
var githubOAuth = &oauth2.Config{
    ClientID:     os.Getenv("GITHUB_CLIENT_ID"),
    ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
    RedirectURL:  "https://myapp.com/auth/github/callback",
    Scopes:       []string{"user:email"},
    Endpoint:     github.Endpoint,
}

// Google OAuth 配置
var googleOAuth = &oauth2.Config{
    ClientID:     os.Getenv("GOOGLE_CLIENT_ID"),
    ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
    RedirectURL:  "https://myapp.com/auth/google/callback",
    Scopes:       []string{"email", "profile"},
    Endpoint:     google.Endpoint,
}

// Step 1: 重定向到 GitHub 授权页
func (h *OAuthHandler) GithubLogin(c *gin.Context) {
    // state 参数防 CSRF
    state := generateRandomState()
    c.SetCookie("oauth_state", state, 600, "/", "", true, true)

    url := githubOAuth.AuthCodeURL(state, oauth2.AccessTypeOffline)
    c.Redirect(302, url)
}

// Step 2: GitHub 回调处理
func (h *OAuthHandler) GithubCallback(c *gin.Context) {
    // 验证 state 防 CSRF
    state, _ := c.Cookie("oauth_state")
    if c.Query("state") != state {
        c.JSON(400, gin.H{"error": "state 不匹配,可能的 CSRF 攻击"})
        return
    }

    code := c.Query("code")
    if code == "" {
        c.JSON(400, gin.H{"error": "缺少授权码"})
        return
    }

    // 用 code 换 access_token
    token, err := githubOAuth.Exchange(c.Request.Context(), code)
    if err != nil {
        c.JSON(500, gin.H{"error": "Token 交换失败"})
        return
    }

    // 获取 GitHub 用户信息
    client := githubOAuth.Client(c.Request.Context(), token)
    resp, _ := client.Get("https://api.github.com/user")
    defer resp.Body.Close()

    var githubUser struct {
        ID    int64  `json:"id"`
        Login string `json:"login"`
        Email string `json:"email"`
        Name  string `json:"name"`
    }
    json.NewDecoder(resp.Body).Decode(&githubUser)

    // 查找或创建本地用户
    user, err := h.userSvc.FindOrCreateOAuthUser(c.Request.Context(), service.OAuthUserInfo{
        Provider:   "github",
        ProviderID: fmt.Sprintf("%d", githubUser.ID),
        Email:      githubUser.Email,
        Name:       githubUser.Name,
    })
    if err != nil {
        c.JSON(500, gin.H{"error": "用户创建失败"})
        return
    }

    // 生成本系统的 JWT
    tokens, _ := h.jwtSvc.GenerateTokenPair(user.ID, user.Email, user.Role)
    c.Redirect(302, "/dashboard?token="+tokens.AccessToken)
}

3. RBAC 权限控制

go get github.com/casbin/casbin/v2
// model.conf - RBAC 模型定义
// [request_definition]
// r = sub, obj, act
//
// [policy_definition]
// p = sub, obj, act
//
// [role_definition]
// g = _, _
//
// [matchers]
// m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

// policy.csv - 权限规则
// p, admin, /api/v1/users, GET
// p, admin, /api/v1/users, POST
// p, admin, /api/v1/users/:id, DELETE
// p, user,  /api/v1/users/me, GET
// p, user,  /api/v1/users/me, PATCH
// g, alice, admin
// g, bob,   user
package middleware

import (
    "net/http"
    "github.com/casbin/casbin/v2"
    "github.com/gin-gonic/gin"
)

func CasbinMiddleware(enforcer *casbin.Enforcer) gin.HandlerFunc {
    return func(c *gin.Context) {
        role := c.GetString("user_role")  // 来自 JWT 中间件
        path := c.FullPath()              // 路由路径模式, 如 /api/v1/users/:id
        method := c.Request.Method

        ok, err := enforcer.Enforce(role, path, method)
        if err != nil || !ok {
            c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
                "error": "权限不足",
                "required": fmt.Sprintf("%s %s", method, path),
            })
            return
        }
        c.Next()
    }
}

// 动态添加权限
func (h *AdminHandler) GrantPermission(c *gin.Context) {
    var req struct {
        Role   string `json:"role"   binding:"required"`
        Path   string `json:"path"   binding:"required"`
        Method string `json:"method" binding:"required"`
    }
    c.ShouldBindJSON(&req)

    // 运行时动态添加策略
    ok, _ := h.enforcer.AddPolicy(req.Role, req.Path, req.Method)
    if !ok {
        c.JSON(400, gin.H{"error": "权限规则已存在"})
        return
    }
    c.JSON(200, gin.H{"message": "权限添加成功"})
}

4. 安全最佳实践

SQL 注入防护

// ❌ 危险:字符串拼接 SQL
db.Raw("SELECT * FROM users WHERE name = '" + name + "'").Scan(&users)

// ✅ 安全:参数化查询
db.Where("name = ?", name).Find(&users)
db.Raw("SELECT * FROM users WHERE name = ?", name).Scan(&users)

XSS 防护

import (
    "github.com/microcosm-cc/bluemonday"
    "html/template"
)

var sanitizer = bluemonday.UGCPolicy()

func sanitizeInput(input string) string {
    // 允许基本 HTML 标签,移除恶意脚本
    return sanitizer.Sanitize(input)
}

// 渲染 HTML 时使用 template.HTML 安全转义
func renderTemplate(w http.ResponseWriter, data string) {
    t := template.Must(template.New("").Parse(`

{{.}}

`)) t.Execute(w, data) // Go template 默认 HTML 转义 }

请求限流

package middleware

import (
    "net/http"
    "sync"
    "time"
    "github.com/gin-gonic/gin"
    "golang.org/x/time/rate"
)

// 按 IP 限流
type IPRateLimiter struct {
    ips    map[string]*rate.Limiter
    mu     sync.RWMutex
    rps    float64
    burst  int
}

func NewIPRateLimiter(rps float64, burst int) *IPRateLimiter {
    rl := &IPRateLimiter{
        ips:   make(map[string]*rate.Limiter),
        rps:   rps,
        burst: burst,
    }
    // 定期清理过期 limiter
    go rl.cleanup()
    return rl
}

func (rl *IPRateLimiter) getLimiter(ip string) *rate.Limiter {
    rl.mu.RLock()
    limiter, ok := rl.ips[ip]
    rl.mu.RUnlock()
    if ok {
        return limiter
    }
    rl.mu.Lock()
    limiter = rate.NewLimiter(rate.Limit(rl.rps), rl.burst)
    rl.ips[ip] = limiter
    rl.mu.Unlock()
    return limiter
}

func (rl *IPRateLimiter) Middleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        ip := c.ClientIP()
        limiter := rl.getLimiter(ip)
        if !limiter.Allow() {
            c.Header("Retry-After", "1")
            c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
                "error": "请求过于频繁,请稍后再试",
            })
            return
        }
        c.Next()
    }
}

func (rl *IPRateLimiter) cleanup() {
    for range time.Tick(10 * time.Minute) {
        rl.mu.Lock()
        rl.ips = make(map[string]*rate.Limiter)
        rl.mu.Unlock()
    }
}

安全响应头

func SecurityHeadersMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 防止点击劫持
        c.Header("X-Frame-Options", "DENY")
        // 防止 MIME 类型嗅探
        c.Header("X-Content-Type-Options", "nosniff")
        // XSS 保护 (旧浏览器)
        c.Header("X-XSS-Protection", "1; mode=block")
        // HSTS: 强制 HTTPS
        c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        // 内容安全策略
        c.Header("Content-Security-Policy",
            "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'")
        // 引用来源策略
        c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
        // 权限策略
        c.Header("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
        c.Next()
    }
}

敏感数据加密

package crypto

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/base64"
    "io"
)

// AES-GCM 加密 (认证加密,同时保证机密性和完整性)
func Encrypt(key []byte, plaintext string) (string, error) {
    block, err := aes.NewCipher(key) // key 必须是 16, 24 或 32 字节
    if err != nil {
        return "", err
    }

    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return "", err
    }

    nonce := make([]byte, gcm.NonceSize())
    if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
        return "", err
    }

    ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
    return base64.StdEncoding.EncodeToString(ciphertext), nil
}

func Decrypt(key []byte, ciphertextB64 string) (string, error) {
    ciphertext, err := base64.StdEncoding.DecodeString(ciphertextB64)
    if err != nil {
        return "", err
    }

    block, _ := aes.NewCipher(key)
    gcm, _ := cipher.NewGCM(block)

    nonceSize := gcm.NonceSize()
    nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]

    plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
    if err != nil {
        return "", err
    }
    return string(plaintext), nil
}
🚨

安全红线

① 密码必须用 bcrypt/argon2 哈希,绝不明文存储  ② Secret Key 必须从环境变量读取,绝不硬编码  ③ 数据库连接字符串不能出现在日志中  ④ 用户输入必须验证和消毒  ⑤ 生产环境必须开启 HTTPS