Files
EchoChat/backend/go-service/pkg/middleware/auth.go
bujinyuan d975adaf2a feat(auth): 完整认证系统 — 注册/登录/JWT/路由(Task 4 + Task 5)
Task 4 — 认证服务层:
- pkg/utils/password.go: bcrypt 密码加密与校验
- pkg/utils/jwt.go: Access/Refresh Token 生成与解析(jwt/v5@v5.2.1,兼容 Go 1.23)
- app/dto/auth_dto.go: 认证相关请求/响应 DTO(含参数校验 binding tag)
- app/auth/service/auth_service.go: 核心业务逻辑(注册、登录、管理员登录、Token 刷新、个人信息、改密码)
- pkg/middleware/auth.go: JWT 认证中间件 + 角色权限检查中间件

Task 5 — Controller 与路由注册:
- app/auth/controller/auth_controller.go: 前台认证接口(7 个 API)
- app/auth/controller/admin_auth_controller.go: 后台管理认证接口
- app/auth/router.go: auth 模块路由定义(模块自包含)
- router/router.go: 主路由汇总入口(仅做调度,不含具体路由定义)
- cmd/server/main.go: 路由注册迁移到 router.Setup()

已验证全部接口:注册 ✓ 登录 ✓ 获取 profile ✓ 错误密码 ✓ 未认证拒绝 ✓ 非管理员拒绝 ✓

Made-with: Cursor
2026-02-28 16:42:28 +08:00

130 lines
3.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package middleware
import (
"strings"
"github.com/echochat/backend/config"
"github.com/echochat/backend/pkg/logs"
"github.com/echochat/backend/pkg/utils"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
const (
ContextKeyUserID = "user_id" // Context 中存储当前用户 ID 的 Key
ContextKeyUsername = "username" // Context 中存储当前用户名的 Key
ContextKeyRoles = "roles" // Context 中存储当前用户角色列表的 Key
)
// JWTAuth JWT 认证中间件
// 从 Authorization Header 中提取 Bearer Token验证后将用户信息注入 Gin Context
func JWTAuth(jwtCfg *config.JWTConfig) gin.HandlerFunc {
return func(c *gin.Context) {
funcName := "middleware.JWTAuth"
ctx := c.Request.Context()
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
utils.ResponseUnauthorized(c, "缺少认证信息")
c.Abort()
return
}
// 提取 Bearer Token
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
utils.ResponseUnauthorized(c, "认证格式错误,应为 Bearer {token}")
c.Abort()
return
}
tokenStr := parts[1]
claims, err := utils.ParseToken(jwtCfg, tokenStr)
if err != nil {
logs.Warn(ctx, funcName, "Token 验证失败",
zap.Error(err),
zap.String("ip", c.ClientIP()),
)
utils.ResponseUnauthorized(c, "认证已过期或无效,请重新登录")
c.Abort()
return
}
// 验证 Token 类型(只允许 access token 访问接口)
if claims.Subject != "access" {
utils.ResponseUnauthorized(c, "无效的 Token 类型")
c.Abort()
return
}
// 将用户信息注入 Gin Context供后续 Controller/Service 使用
c.Set(ContextKeyUserID, claims.UserID)
c.Set(ContextKeyUsername, claims.Username)
c.Set(ContextKeyRoles, claims.Roles)
c.Next()
}
}
// RequireRole 角色权限检查中间件
// 检查当前用户是否拥有指定角色之一OR 逻辑),需在 JWTAuth 之后使用
func RequireRole(roles ...string) gin.HandlerFunc {
return func(c *gin.Context) {
funcName := "middleware.RequireRole"
ctx := c.Request.Context()
userRoles, exists := c.Get(ContextKeyRoles)
if !exists {
utils.ResponseForbidden(c, "无权限访问")
c.Abort()
return
}
roleList, ok := userRoles.([]string)
if !ok {
utils.ResponseForbidden(c, "无权限访问")
c.Abort()
return
}
for _, required := range roles {
for _, userRole := range roleList {
if userRole == required {
c.Next()
return
}
}
}
userID, _ := c.Get(ContextKeyUserID)
logs.Warn(ctx, funcName, "角色权限不足",
zap.Any("user_id", userID),
zap.Strings("required", roles),
zap.Strings("actual", roleList),
)
utils.ResponseForbidden(c, "权限不足,需要角色: "+strings.Join(roles, " 或 "))
c.Abort()
}
}
// GetCurrentUserID 从 Gin Context 获取当前登录用户 ID
// 供 Controller 层调用的便捷方法
func GetCurrentUserID(c *gin.Context) (int64, bool) {
val, exists := c.Get(ContextKeyUserID)
if !exists {
return 0, false
}
userID, ok := val.(int64)
return userID, ok
}
// GetCurrentUsername 从 Gin Context 获取当前登录用户名
func GetCurrentUsername(c *gin.Context) (string, bool) {
val, exists := c.Get(ContextKeyUsername)
if !exists {
return "", false
}
username, ok := val.(string)
return username, ok
}