- main.go: 禁用 RedirectTrailingSlash,为 classes/iam/teacher 双注册无尾斜杠与通配符路由 - auth.go: DEV_MODE=true 时接受 Bearer dev-token 注入开发用户 - config.go: 新增 DevMode 配置项与 getEnvBool 工具 - page.tsx: 开发模式请求携带 Authorization: Bearer dev-token - .env.example: 添加 DEV_MODE=false 默认值与生产警告
122 lines
3.0 KiB
Go
122 lines
3.0 KiB
Go
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"
|
||
)
|
||
|
||
// AuthMiddleware 验证 JWT 并注入用户信息到请求头
|
||
// P1 用 HS256,P2 改 RS256(IAM 签发)
|
||
func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
// 健康检查跳过鉴权
|
||
if c.Request.URL.Path == "/healthz" {
|
||
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()
|
||
}
|