- 未授权响应改为 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 不兜底
108 lines
3.8 KiB
Go
108 lines
3.8 KiB
Go
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()
|
||
}
|
||
}
|