后端: - 种子数据:超级管理员(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)
87 lines
3.1 KiB
Go
87 lines
3.1 KiB
Go
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()
|
||
}
|
||
}
|