Files
Edu/services/api-gateway/internal/middleware/auth.go
SpecialX 5759b09c9f feat(api-gateway): 添加公开路径白名单
register/login/refresh 无需 JWT 即可通过 Gateway,修复之前无 token 无法注册登录的死锁问题。
2026-07-09 00:30:35 +08:00

142 lines
3.7 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 (
"net/http"
"strings"
"github.com/edu-cloud/api-gateway/internal/config"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
// publicPaths 是无需鉴权的公开路径(精确匹配,基于去掉 /api/v1 前缀后的路径)
var publicPaths = map[string]bool{
"/iam/register": true,
"/iam/login": true,
"/iam/refresh": true,
}
// isPublicPath 判断请求路径是否属于公开路径(无需鉴权)
// 匹配规则:去掉 /api/v1 前缀后,与 publicPaths 精确匹配
func isPublicPath(path string) bool {
stripped := strings.TrimPrefix(path, "/api/v1")
return publicPaths[stripped]
}
// AuthMiddleware 验证 JWT 并注入用户信息到请求头
// P1 用 HS256P2 改 RS256IAM 签发)
func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
// 健康检查跳过鉴权
if c.Request.URL.Path == "/healthz" {
c.Next()
return
}
// 公开路径白名单register/login/refresh 无需鉴权)
if isPublicPath(c.Request.URL.Path) {
c.Next()
return
}
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"success": false,
"error": gin.H{
"code": "UNAUTHORIZED",
"message": "missing authorization header",
},
})
return
}
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
if tokenStr == authHeader {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"success": false,
"error": gin.H{
"code": "UNAUTHORIZED",
"message": "invalid authorization scheme, expected Bearer",
},
})
return
}
// 开发模式旁路DEV_MODE=true 时接受 "dev-token",注入开发用户
// 仅用于本地联调,生产环境必须关闭 DEV_MODE
if cfg.DevMode && tokenStr == "dev-token" {
c.Request.Header.Set("x-user-id", "dev-user")
c.Request.Header.Set("x-user-roles", "teacher,admin")
c.Next()
return
}
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return []byte(cfg.JWTSecret), nil
})
if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"success": false,
"error": gin.H{
"code": "INVALID_TOKEN",
"message": "token validation failed",
},
})
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"success": false,
"error": gin.H{
"code": "INVALID_CLAIMS",
"message": "invalid token claims",
},
})
return
}
// 注入用户信息到下游请求头
if sub, ok := claims["sub"].(string); ok {
c.Request.Header.Set("x-user-id", sub)
}
if roles, ok := claims["roles"].([]interface{}); ok {
roleStrs := make([]string, 0, len(roles))
for _, r := range roles {
if s, ok := r.(string); ok {
roleStrs = append(roleStrs, s)
}
}
c.Request.Header.Set("x-user-roles", strings.Join(roleStrs, ","))
}
c.Next()
}
}
// RequestIDMiddleware 注入请求 ID 用于全链路追踪
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = generateUUID()
}
c.Set("request_id", requestID)
c.Writer.Header().Set("X-Request-ID", requestID)
c.Next()
}
}
// generateUUID 生成带 req- 前缀的唯一请求 ID
// 使用 uuid.New() 基于 RFC 4122 v4 随机 UUID避免 time.Now() 产生的冲突与可预测性
func generateUUID() string {
return "req-" + uuid.New().String()
}