Gin 框架中如何实现认证和授权?
Gin 框架里做认证和授权,核心思路就一条:用中间件拦截请求,在 handler 执行前完成身份校验和权限判断。下面从实际场景出发,把几种主流方案讲清楚。
认证和授权到底在解决什么问题
认证(Authentication)回答“你是谁”,授权(Authorization)回答“你能干什么”。两者经常被混在一起说,但在实现上应该分开:先确认身份,再判断权限。Gin 的中间件链天然支持这种分层——一个中间件管认证,另一个管授权,各司其职。
JWT 认证:无状态方案的首选
JWT 是前后端分离项目里用得最多的认证方式。好处是服务端不用存 session,水平扩容没有负担。
安装依赖
bashgo get github.com/golang-jwt/jwt/v5
定义 Claims 和 Token 工具函数
goimport ( "errors" "github.com/golang-jwt/jwt/v5" "time" ) var jwtSecret = []byte("your-secret-key") type Claims struct { UserID uint `json:"user_id"` Username string `json:"username"` Role string `json:"role"` jwt.RegisteredClaims } func GenerateToken(userID uint, username, role string) (string, error) { claims := Claims{ UserID: userID, Username: username, Role: role, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)), IssuedAt: jwt.NewNumericDate(time.Now()), Issuer: "your-app-name", }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(jwtSecret) } func ParseToken(tokenString string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.New("unexpected signing method") } return jwtSecret, nil }) if err != nil { return nil, err } if claims, ok := token.Claims.(*Claims); ok && token.Valid { return claims, nil } return nil, errors.New("invalid token") }
这里有几个容易踩的坑:签名方法校验不能省,否则攻击者可以用 none 算法伪造 token;过期时间别设太长,2 小时是比较合理的默认值,配合 refresh token 机制来续期。
JWT 认证中间件
gofunc JWTAuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少 Authorization 头"}) c.Abort() return } tokenString := strings.TrimPrefix(authHeader, "Bearer ") if tokenString == authHeader { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization 格式错误,需要 Bearer token"}) c.Abort() return } claims, err := ParseToken(tokenString) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "token 无效或已过期"}) c.Abort() return } c.Set("user_id", claims.UserID) c.Set("username", claims.Username) c.Set("role", claims.Role) c.Next() } }
中间件把解析出的用户信息存进 gin.Context,后面的 handler 和授权中间件都能拿到。
Token 刷新怎么做
实际项目里不能让用户每隔两小时就重新登录。常见做法是签发一对 token:access token 短期(2小时),refresh token 长期(7天)。access token 过期后,客户端拿 refresh token 换新的 access token。
gofunc RefreshToken(c *gin.Context) { refreshToken := c.PostForm("refresh_token") claims, err := ParseToken(refreshToken) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "refresh token 无效"}) return } // 检查 refresh token 是否在有效期内,且未被撤销 // 实际项目中应该查 Redis 确认 refresh token 没被吊销 newToken, err := GenerateToken(claims.UserID, claims.Username, claims.Role) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "生成 token 失败"}) return } c.JSON(http.StatusOK, gin.H{"access_token": newToken}) }
Session 认证:传统但有场景
如果项目是传统的服务端渲染页面,Session 认证反而更简单——不用管 token 存储,浏览器 cookie 自动带上 session id。
安装依赖
bashgo get github.com/gin-contrib/sessions go get github.com/gin-contrib/sessions/cookie go get github.com/gin-contrib/sessions/redis
配置 Session 中间件
开发环境用 cookie store 就够了,生产环境建议换 Redis store,避免重启丢 session。
goimport ( "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" ) func SetupSession(r *gin.Engine) { store := cookie.NewStore([]byte("secret-key-change-in-production")) store.Options(sessions.Options{ MaxAge: 3600, HttpOnly: true, Secure: true, SameSite: http.SameSiteStrictMode, }) r.Use(sessions.Sessions("sessionid", store)) }
Session 认证中间件
gofunc SessionAuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { session := sessions.Default(c) userID := session.Get("user_id") if userID == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"}) c.Abort() return } c.Set("user_id", userID) c.Next() } }
Session 方案的缺点是分布式部署时需要共享 session 存储(Redis),否则请求打到不同实例会登录失败。所以微服务架构下,JWT 通常是更好的选择。
Basic Auth:简单但只适合内部工具
Gin 内置了 Basic Auth 中间件,三行代码就能用。但用户名密码是明文传输(Base64 不是加密),必须配合 HTTPS 使用,而且没有退出登录的概念,只适合内部管理面板或者快速原型。
goauthorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{ "admin": "admin123", "user": "user123", })) authorized.GET("/dashboard", func(c *gin.Context) { user := c.MustGet(gin.AuthUserKey).(string) c.JSON(http.StatusOK, gin.H{"message": "欢迎 " + user}) })
OAuth2:接入第三方登录
让用户用 Google、GitHub 账号登录,需要 OAuth2。Gin 本身不管 OAuth2 流程,靠 golang.org/x/oauth2 这个官方库来做。
安装依赖
bashgo get golang.org/x/oauth2 go get golang.org/x/oauth2/google
配置和回调处理
govar oauthConfig = &oauth2.Config{ ClientID: os.Getenv("GOOGLE_CLIENT_ID"), ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"), RedirectURL: "http://localhost:8080/auth/google/callback", Scopes: []string{"openid", "profile", "email"}, Endpoint: google.Endpoint, } // 发起登录 func GoogleLogin(c *gin.Context) { state := generateRandomState() c.SetCookie("oauth_state", state, 300, "/", "", true, true) c.Redirect(http.StatusTemporaryRedirect, oauthConfig.AuthCodeURL(state)) } // 回调处理 func GoogleCallback(c *gin.Context) { state := c.Query("state") cookieState, _ := c.Cookie("oauth_state") if state != cookieState { c.JSON(http.StatusBadRequest, gin.H{"error": "state 不匹配"}) return } code := c.Query("code") token, err := oauthConfig.Exchange(c.Request.Context(), code) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "获取 token 失败"}) return } // 用 token 拿用户信息,然后签发自己的 JWT client := oauthConfig.Client(c.Request.Context(), token) resp, _ := client.Get("https://www.googleapis.com/oauth2/v2/userinfo") // 解析 resp.Body,提取用户信息,生成自己的 JWT 或创建 Session // ... }
回调里拿到 Google 的用户信息后,通常的做法是:查数据库有没有这个用户,没有就创建,然后签发自己系统的 JWT 给前端。state 参数必须校验,防止 CSRF 攻击。
授权:认证之后的事
认证确认了用户是谁,授权决定他能干什么。下面讲两种最常用的授权模式。
基于角色的访问控制(RBAC)
给用户分配角色(admin、editor、viewer),中间件检查角色是否在允许列表里。
gofunc RoleMiddleware(allowedRoles ...string) gin.HandlerFunc { roleSet := make(map[string]bool, len(allowedRoles)) for _, r := range allowedRoles { roleSet[r] = true } return func(c *gin.Context) { role, exists := c.Get("role") if !exists { c.JSON(http.StatusForbidden, gin.H{"error": "缺少角色信息"}) c.Abort() return } if !roleSet[role.(string)] { c.JSON(http.StatusForbidden, gin.H{"error": "权限不足"}) c.Abort() return } c.Next() } }
路由配置:
goadminGroup := r.Group("/admin") adminGroup.Use(JWTAuthMiddleware(), RoleMiddleware("admin")) { adminGroup.GET("/users", listUsers) adminGroup.DELETE("/users/:id", deleteUser) } editorGroup := r.Group("/content") editorGroup.Use(JWTAuthMiddleware(), RoleMiddleware("admin", "editor")) { editorGroup.POST("/articles", createArticle) editorGroup.PUT("/articles/:id", updateArticle) }
RBAC 简单好维护,适合角色划分清晰的项目。但如果权限粒度细到“某个用户只能编辑自己创建的文章”,角色就不够用了。
基于权限的访问控制(PBAC)
把权限定义成具体操作(article:edit:own、article:edit:all),中间件检查用户是否拥有所需权限。
gofunc PermissionMiddleware(requiredPermissions ...string) gin.HandlerFunc { return func(c *gin.Context) { userPermissions, _ := c.Get("permissions") permList, ok := userPermissions.([]string) if !ok { c.JSON(http.StatusForbidden, gin.H{"error": "缺少权限信息"}) c.Abort() return } permSet := make(map[string]bool, len(permList)) for _, p := range permList { permSet[p] = true } for _, required := range requiredPermissions { if !permSet[required] { c.JSON(http.StatusForbidden, gin.H{"error": "权限不足,需要 " + required}) c.Abort() return } } c.Next() } }
权限数据从哪来?一般是数据库里存用户-权限关联表,登录时查出来放进 JWT claims 或者缓存到 Redis。如果权限不常变,放 JWT 里省一次查询;如果权限经常调整,走 Redis 实时查更可靠。
JWT 还是 Session:怎么选
| 对比维度 | JWT | Session |
|---|---|---|
| 服务端存储 | 不需要 | 需要(内存/Redis) |
| 水平扩展 | 天然支持 | 需要 Redis 共享 |
| 主动吊销 | 需要额外机制(黑名单) | 直接删 session |
| 适用场景 | API 服务、微服务 | 服务端渲染、传统 Web |
| 安全性 | token 泄露难发现 | session 可即时失效 |
简单说:前后端分离选 JWT,服务端渲染选 Session,内部工具用 Basic Auth,需要第三方登录走 OAuth2。项目里也可能混合使用,比如主站用 JWT,后台管理用 Session + RBAC。
几个实战经验
密钥管理:JWT 的签名密钥不要硬编码,从环境变量或配置中心读取,定期轮换。可以维护一个密钥版本列表,解析 token 时按 key ID 匹配密钥,实现平滑过渡。
Token 黑名单:用户改密码或被封禁后,已签发的 JWT 在过期前仍然有效。解决思路是在 Redis 里维护一个黑名单,中间件校验时额外查一次。虽然破坏了无状态性,但安全上是必要的。
HTTPS 是底线:不管用哪种认证方式,生产环境必须 HTTPS,否则 token/session id 在传输中可被截获。
日志和监控:认证失败的请求应该记录日志,4xx 突然增多可能是攻击信号。可以在中间件里加上 metrics 统计。
错误信息别太详细:返回“token 过期”和“token 签名无效”对调试有用,但对攻击者也有用。生产环境建议统一返回“认证失败”,详细信息写日志。