service.xpcool.com/internal/middleware/auth.go
夏犀麟 3d24dbd8cb feat(auth): 认证链路联调修复——401 语义化 + 登出撤销会话 + 零配置兜底
- 未授权响应改为 HTTP 401(原 200+code10002 vben 拦截器不识别,
  token 过期永远不触发静默刷新/重登)
- 新增 response.JSONWithStatus;三处 401(UserAuth/AdminAuthOnly/AdminAuth)切换
- LogoutReq 支持 refreshToken:登出时撤销刷新会话(幂等),阻止设备续期;
  Refresh 时顺带清理已过期会话行
- IAdminAuth 新增 Revoke;dao/internal/admin_user.go 补 bark_device_id/
  pushplus_token 列映射(与 016 SQL/0569c70 对齐,恢复 int/gtime 类型映射)
- injectEnv 开发模式兜底: 等占位符未注入 env 时回填本地默认 DSN,
  任何启动方式(GoLand/命令行/go run)零配置可跑;prod 不兜底
2026-08-27 23:00:53 +08:00

108 lines
3.8 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 (
"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 <token> 提取令牌。
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()
}
}