package middleware import ( "strings" "github.com/goravel/framework/contracts/http" "goravel/app/http/trans" "goravel/app/models" "goravel/app/services" "goravel/app/utils/errorlog" "goravel/app/utils/logger" ) // Permission 权限验证中间件 func Permission() http.Middleware { return func(ctx http.Context) { // 从context中获取admin信息(由JWT中间件设置) adminValue := ctx.Value("admin") if adminValue == nil { _ = ctx.Response().Json(http.StatusUnauthorized, http.Json{ "code": 401, "message": trans.Get(ctx, "not_logged_in"), }).Abort() return } admin, ok := adminValue.(models.Admin) if !ok { _ = ctx.Response().Json(http.StatusUnauthorized, http.Json{ "code": 401, "message": trans.Get(ctx, "not_logged_in"), }).Abort() return } // 加载管理员的角色、权限等关联数据 adminService := services.NewAdminServiceImpl() if err := adminService.LoadRelationsWithPermissions(&admin); err != nil { logger.ErrorfHTTP(ctx, "permission middleware load relations failed: %v", err) errorlog.RecordHTTP(ctx, "permission", "Failed to load admin relations with permissions", map[string]any{ "error": err.Error(), "admin_id": admin.ID, "path": ctx.Request().Path(), }, "Load admin relations failed: %v", err) _ = ctx.Response().Json(http.StatusInternalServerError, http.Json{ "code": 500, "message": trans.Get(ctx, "load_permissions_failed"), }).Abort() return } // 检查是否是超级管理员 // 拥有 super-admin 角色的管理员(包括超级管理员和开发者管理员)都跳过权限“拦截”,但仍然参与权限匹配,用于生成操作标题 isSuperAdmin := false for _, role := range admin.Roles { if role.Slug == "super-admin" && role.Status == 1 { isSuperAdmin = true break } } // 获取当前请求的方法和路径 method := ctx.Request().Method() path := ctx.Request().Path() // 收集所有角色的权限(已通过预加载获取) var allPermissions []models.Permission for _, role := range admin.Roles { if role.Status == 1 { allPermissions = append(allPermissions, role.Permissions...) } } // 检查是否有权限,并记录匹配的权限标识 hasPermission := false var matchedPermissionSlug string var menuDisabled bool for _, perm := range allPermissions { if perm.Status == 1 { // 检查方法匹配 if perm.Method == "" || perm.Method == method { // 检查路径匹配(支持通配符) if perm.Path == "" || perm.Path == path || matchPath(perm.Path, path) { // 检查关联菜单的状态(如果权限关联了菜单) // 如果权限没有关联菜单(MenuID = 0),则允许访问 // 如果权限关联了菜单,需要检查菜单状态是否为启用(status = 1) if perm.MenuID == 0 { // 权限没有关联菜单,允许访问 hasPermission = true matchedPermissionSlug = perm.Slug break } else if perm.Menu.ID > 0 { // 权限关联了菜单,检查菜单状态 if perm.Menu.Status == 1 { // 菜单状态为启用,允许访问 hasPermission = true matchedPermissionSlug = perm.Slug break } else { // 菜单状态为关闭,记录但继续查找其他权限 menuDisabled = true } } // 如果菜单没有加载(perm.Menu.ID == 0),为了安全起见,不允许访问 // 这种情况应该很少发生,因为我们已经预加载了菜单 } } } } // 非超级管理员且无匹配权限时拦截;超级管理员即使无匹配权限也放行 if !hasPermission && !isSuperAdmin { // 如果是因为菜单状态为关闭而禁止访问,返回更具体的错误信息 if menuDisabled { _ = ctx.Response().Json(http.StatusForbidden, http.Json{ "code": 403, "message": trans.Get(ctx, "menu_disabled"), }).Abort() } else { _ = ctx.Response().Json(http.StatusForbidden, http.Json{ "code": 403, "message": trans.Get(ctx, "no_permission"), }).Abort() } return } // 将匹配的权限标识存储到 context 中,供操作日志使用 if matchedPermissionSlug != "" { ctx.WithValue("permission_slug", matchedPermissionSlug) } ctx.Request().Next() } } // matchPath 路径匹配,支持通配符 // 支持的模式: // 1. 精确匹配:/api/admin/roles 匹配 /api/admin/roles // 2. 末尾通配符:/api/admin/roles/* 匹配 /api/admin/roles/1 // 3. 中间通配符:/api/admin/attachments/*/display-name 匹配 /api/admin/attachments/1/display-name func matchPath(pattern, path string) bool { if pattern == path { return true } // 如果模式不包含通配符,直接返回 false if !contains(pattern, '*') { return false } // 将模式按 * 分割成多个部分 parts := splitPattern(pattern) if len(parts) == 0 { return false } // 如果模式以 * 开头,需要特殊处理 if pattern[0] == '*' { // 检查路径是否以模式的剩余部分结尾 if len(parts) > 1 { suffix := parts[1] return len(path) >= len(suffix) && path[len(path)-len(suffix):] == suffix } return true } // 如果模式以 * 结尾 if pattern[len(pattern)-1] == '*' { prefix := pattern[:len(pattern)-1] if len(path) >= len(prefix) { pathPrefix := path[:len(prefix)] if pathPrefix == prefix { // 如果前缀以 / 结尾,路径必须比前缀长(即后面还有内容) if len(prefix) > 0 && prefix[len(prefix)-1] == '/' { return len(path) > len(prefix) } // 如果前缀不以 / 结尾,路径可以等于或长于前缀 return true } } return false } // 处理中间有通配符的情况,如 /api/admin/attachments/*/display-name // 将模式按 * 分割 patternParts := splitPattern(pattern) if len(patternParts) < 2 { return false } // 过滤掉 "*" 标记,只保留实际的部分 var actualParts []string for _, part := range patternParts { if part != "*" { actualParts = append(actualParts, part) } } if len(actualParts) < 2 { return false } // 检查路径是否以第一部分开头 firstPart := actualParts[0] if len(path) < len(firstPart) || path[:len(firstPart)] != firstPart { return false } // 检查路径是否以最后一部分结尾 lastPart := actualParts[len(actualParts)-1] if len(path) < len(lastPart) || path[len(path)-len(lastPart):] != lastPart { return false } // 检查中间部分是否存在(通配符匹配任意内容) // 路径应该是:firstPart + 任意内容 + lastPart remainingPath := path[len(firstPart) : len(path)-len(lastPart)] // 确保中间部分不为空(至少有一个字符,通常是数字ID) return len(remainingPath) > 0 } // contains 检查字符串是否包含指定字符 func contains(s string, c byte) bool { for i := range s { if s[i] == c { return true } } return false } // splitPattern 按 * 分割模式字符串 func splitPattern(pattern string) []string { var parts []string var current strings.Builder for i := range pattern { if pattern[i] == '*' { if current.Len() > 0 { parts = append(parts, current.String()) current.Reset() } parts = append(parts, "*") } else { current.WriteByte(pattern[i]) } } if current.Len() > 0 { parts = append(parts, current.String()) } return parts }