package middleware import ( "context" "encoding/json" "github.com/gogf/gf/v2/net/ghttp" "service.xpcool.com/internal/consts" "service.xpcool.com/internal/library/jwt" "service.xpcool.com/internal/library/response" "strings" "time" ) type contextKey string const ( UserIDKey contextKey = "userId" AdminIDKey contextKey = "adminId" PermissionKey contextKey = "permission" ) func bearer(r *ghttp.Request) string { // 统一从 Authorization: Bearer 提取令牌。 return strings.TrimSpace(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer")) } func auditParam(raw string) string { // 操作日志需要参数用于审计,但绝不记录密码、令牌和验证码等敏感字段。 lower := strings.ToLower(raw) if strings.Contains(lower, "password") || strings.Contains(lower, "token") || strings.Contains(lower, "code") { return "[redacted]" } return raw } // extractErrorMsg 当响应状态码 >= 400 时,从响应体 JSON 中提取 msg 作为失败原因摘要。 // 非失败场景返回空字符串,避免无意义写入。 func extractErrorMsg(r *ghttp.Request) string { if r.Response.Status < 400 { return "" } body := r.Response.BufferString() if len(body) == 0 { return "" } var out struct { Msg string `json:"msg"` } if err := json.Unmarshal([]byte(body), &out); err == nil && out.Msg != "" { return out.Msg } return string(body) } func UserAuth(s *jwt.Service) ghttp.HandlerFunc { // 用户端仅接受 scope=user 的 access token。 return func(r *ghttp.Request) { c, err := s.Parse(bearer(r), "access", "user") if err != nil { response.JSONWithStatus(r, 401, consts.CodeUnauthorized, "login required", nil) return } r.SetCtxVar(UserIDKey, c.Subject) r.Middleware.Next() } } func AdminAuthOnly(s *jwt.Service) ghttp.HandlerFunc { // 仅校验 admin access token(不要求 X-Permission),用于登录后即可访问的 // 个人资料/权限码/菜单路由等接口,如 /auth/info、/auth/codes、/menu/routes。 return func(r *ghttp.Request) { c, err := s.Parse(bearer(r), "access", "admin") if err != nil { response.JSONWithStatus(r, 401, consts.CodeUnauthorized, "admin login required", nil) return } r.SetCtxVar(AdminIDKey, c.Subject) r.Middleware.Next() } } func AdminAuth(s *jwt.Service, permissionLookup func(context.Context, string, string) (string, error), permissionCheck func(context.Context, uint64, string) (bool, error), audit func(context.Context, uint64, string, string, string, string, string, int, int, string, string)) ghttp.HandlerFunc { // 管理端接口鉴权:先解析 admin token,再按「请求方法+路径」反查所需权限码 // (admin_menu type=2 行的 path 映射),最后校验该管理员是否拥有该权限码。 // 未配置映射的接口一律拒绝,防止用任意已拥有权限码越权访问。 return func(r *ghttp.Request) { start := time.Now() c, err := s.Parse(bearer(r), "access", "admin") if err != nil { response.JSONWithStatus(r, 401, consts.CodeUnauthorized, "admin login required", nil) return } permission, err := permissionLookup(r.Context(), r.Method, r.URL.Path) if err != nil || permission == "" { response.JSON(r, consts.CodeForbidden, "permission mapping not configured", nil) return } ok, err := permissionCheck(r.Context(), c.Subject, permission) if err != nil || !ok { response.JSON(r, consts.CodeAdminPermissionDenied, "permission denied", nil) return } defer func() { // 失败时从响应体解析 msg 作为错误摘要;UA 直接取请求头。 audit(r.Context(), c.Subject, permission, r.Method, r.URL.Path, r.GetClientIp(), auditParam(r.GetBodyString()), int(time.Since(start).Milliseconds()), r.Response.Status, r.Header.Get("User-Agent"), extractErrorMsg(r)) }() r.SetCtxVar(AdminIDKey, c.Subject) r.SetCtxVar(PermissionKey, permission) r.Middleware.Next() } }