service.xpcool.com/internal/middleware/auth.go
夏犀麟 8fdb25e6bd feat(admin): 后台登录/菜单/按钮级权限/RBAC管理与服务器日志监控
后端:
- 种子数据:超级管理员(admin/admin123)、super_admin 角色、22 条菜单(含按钮权限码)、角色绑定
- admin_menu 扩展 icon/component/hidden 字段(003_schema_ext.sql),手动补齐 entity/do/table
- auth 扩展:/auth/info、/auth/codes(仅登录),路由拆分公开/Profile/受保护三组
- 菜单路由:/menu/routes 返回 vben backend 动态路由树(按角色过滤、排序)
- RBAC 管理:/admins、/roles、/menus CRUD(含角色绑定、重置密码、菜单授权)
- 日志监控:/log/files、/log/tail(尾部读取+关键词过滤+路径穿越防护)
- 安全加固:接口鉴权改为「方法+路径→权限码」自动映射,杜绝任意权限码越权
- 修复:gf v2.10.2  不自动替换→cmd 注入;MySQL driver 需显式引入 contrib
- 全链路冒烟测试通过(含越权拒绝 30003)
2026-08-24 22:35:54 +08:00

87 lines
3.1 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"
"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
}
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.JSON(r, 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.JSON(r, 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)) 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.JSON(r, 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() {
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.SetCtxVar(AdminIDKey, c.Subject)
r.SetCtxVar(PermissionKey, permission)
r.Middleware.Next()
}
}