Chapter 08

认证与安全

实现 JWT 认证中间件、CORS 跨域、令牌桶限流、CSRF 防护与 bcrypt 密码哈希,把服务安全地暴露给外部。

8.1 安全相关名词

认证 vs 授权
认证(Authentication)确认「你是谁」;授权(Authorization)确认「你能做什么」。登录是认证,角色权限是授权。
JWT(JSON Web Token)
由 header.payload.signature 三段组成的自包含令牌,服务端用密钥签名,无需存储会话即可校验,适合无状态 API。
Session
有状态会话:服务端存 session 数据,客户端只持 cookie 中的 session ID,适合传统 Web 与需要即时失效的场景。
CORS(跨域资源共享)
浏览器同源策略下,跨域请求需服务端返回 Access-Control-Allow-* 响应头显式放行。
限流(Rate Limiting)
限制单位时间请求数,防刷防打爆,常用令牌桶/漏桶算法。
CSRF(跨站请求伪造)
诱导已登录用户在第三方站点触发请求。用 CSRF token 或 SameSite cookie 防护。
bcrypt
专为密码设计的慢哈希算法,自带盐(salt)与可调 cost,抵御彩虹表与暴力破解。

8.2 密码哈希 bcrypt

import "golang.org/x/crypto/bcrypt"

// 注册时:哈希存库(绝不明文存密码!)
func HashPassword(pw string) (string, error) {
    b, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
    return string(b), err
}

// 登录时:比对
func CheckPassword(hash, pw string) bool {
    return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)) == nil
}

8.3 签发 JWT

import "github.com/golang-jwt/jwt/v5"

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

func GenToken(uid uint, role, secret string) (string, error) {
    claims := Claims{
        UserID: uid, Role: role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
        },
    }
    t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return t.SignedString([]byte(secret))
}

8.4 JWT 认证中间件

func JWTAuth(secret string) gin.HandlerFunc {
    return func(c *gin.Context) {
        auth := c.GetHeader("Authorization")
        // 期望格式:Bearer <token>
        if !strings.HasPrefix(auth, "Bearer ") {
            c.AbortWithStatusJSON(401, gin.H{"error": "missing token"})
            return
        }
        tokenStr := strings.TrimPrefix(auth, "Bearer ")

        var claims Claims
        token, err := jwt.ParseWithClaims(tokenStr, &claims,
            func(t *jwt.Token) (any, error) {
                // 校验签名算法,防 alg=none 攻击
                if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
                    return nil, errors.New("unexpected signing method")
                }
                return []byte(secret), nil
            })
        if err != nil || !token.Valid {
            c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"})
            return
        }
        c.Set("userID", claims.UserID)
        c.Set("role", claims.Role)
        c.Next()
    }
}

// 授权:基于角色
func RequireRole(role string) gin.HandlerFunc {
    return func(c *gin.Context) {
        if c.GetString("role") != role {
            c.AbortWithStatusJSON(403, gin.H{"error": "forbidden"})
            return
        }
        c.Next()
    }
}
JWT 无法即时失效

签发后到期前一直有效,「登出/封号立即生效」需要额外机制:短时效 access token + refresh token,或维护一个 Redis 黑名单/白名单。密钥务必从环境变量注入,切勿硬编码进代码或提交仓库。

8.5 CORS 跨域

import "github.com/gin-contrib/cors"

r.Use(cors.New(cors.Config{
    AllowOrigins:     []string{"https://gufacode.com"}, // 别用 * 配 credentials
    AllowMethods:     []string{"GET", "POST", "PUT", "DELETE"},
    AllowHeaders:     []string{"Origin", "Content-Type", "Authorization"},
    AllowCredentials: true,
    MaxAge:           12 * time.Hour,
}))

8.6 令牌桶限流

import "golang.org/x/time/rate"

// 全局:每秒 100 个令牌,突发 200
func RateLimit(r rate.Limit, burst int) gin.HandlerFunc {
    limiter := rate.NewLimiter(r, burst)
    return func(c *gin.Context) {
        if !limiter.Allow() {
            c.AbortWithStatusJSON(429, gin.H{"error": "too many requests"})
            return
        }
        c.Next()
    }
}
// 按 IP/用户限流可用 map[string]*rate.Limiter + 定期清理,或 Redis 分布式限流

8.7 其他安全清单

威胁防护
SQL 注入用 GORM/参数化查询,绝不拼字符串
XSS模板自动转义;API 返回 JSON 由前端转义
CSRFSameSite=Lax/Strict cookie 或 csrf token
敏感信息泄露响应过滤密码哈希字段,用 json:"-" 排除
明文传输强制 HTTPS,HSTS 头,反代终止 TLS
暴力破解登录接口限流 + 失败锁定 + 验证码

8.8 小结