commits
This commit is contained in:
+75
-75
@@ -1,75 +1,75 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
)
|
||||
|
||||
// JWTAuthMiddleware JWT认证中间件
|
||||
func JWTAuthMiddleware() web.FilterFunc {
|
||||
return func(ctx *context.Context) {
|
||||
// 跳过登录相关的路由
|
||||
if strings.HasPrefix(ctx.Request.RequestURI, "/api/login") ||
|
||||
strings.HasPrefix(ctx.Request.RequestURI, "/api/reset-password") {
|
||||
return
|
||||
}
|
||||
|
||||
// 从请求头中获取Authorization
|
||||
authHeader := ctx.Request.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
ctx.Output.SetStatus(401)
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "未提供认证信息",
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
|
||||
// 按空格分割
|
||||
authParts := strings.SplitN(authHeader, " ", 2)
|
||||
if !(len(authParts) == 2 && authParts[0] == "Bearer") {
|
||||
ctx.Output.SetStatus(401)
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "认证信息格式错误",
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析token
|
||||
claims, err := jwtutil.ParseToken(authParts[1])
|
||||
if err != nil {
|
||||
// 处理各种错误情况
|
||||
ctx.Output.SetStatus(401)
|
||||
switch err.Error() {
|
||||
case "token is expired":
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "token已过期",
|
||||
}, false, false)
|
||||
default:
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "无效的token",
|
||||
}, false, false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存储在上下文
|
||||
ctx.Input.SetData("userId", claims.UserID)
|
||||
ctx.Input.SetData("username", claims.Username)
|
||||
ctx.Input.SetData("tenantId", claims.TenantId)
|
||||
|
||||
// 从token中获取用户类型(如果token中没有,则默认为"user")
|
||||
userType := claims.UserType
|
||||
if userType == "" {
|
||||
userType = "user"
|
||||
}
|
||||
ctx.Input.SetData("userType", userType)
|
||||
}
|
||||
}
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
)
|
||||
|
||||
// JWTAuthMiddleware JWT认证中间件
|
||||
func JWTAuthMiddleware() web.FilterFunc {
|
||||
return func(ctx *context.Context) {
|
||||
// 跳过登录相关的路由
|
||||
if strings.HasPrefix(ctx.Request.RequestURI, "/api/login") ||
|
||||
strings.HasPrefix(ctx.Request.RequestURI, "/api/reset-password") {
|
||||
return
|
||||
}
|
||||
|
||||
// 从请求头中获取Authorization
|
||||
authHeader := ctx.Request.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
ctx.Output.SetStatus(401)
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "未提供认证信息",
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
|
||||
// 按空格分割
|
||||
authParts := strings.SplitN(authHeader, " ", 2)
|
||||
if !(len(authParts) == 2 && authParts[0] == "Bearer") {
|
||||
ctx.Output.SetStatus(401)
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "认证信息格式错误",
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析token
|
||||
claims, err := jwtutil.ParseToken(authParts[1])
|
||||
if err != nil {
|
||||
// 处理各种错误情况
|
||||
ctx.Output.SetStatus(401)
|
||||
switch err.Error() {
|
||||
case "token is expired":
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "token已过期",
|
||||
}, false, false)
|
||||
default:
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "无效的token",
|
||||
}, false, false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存储在上下文
|
||||
ctx.Input.SetData("userId", claims.UserID)
|
||||
ctx.Input.SetData("username", claims.Username)
|
||||
ctx.Input.SetData("tenantId", claims.TenantId)
|
||||
|
||||
// 从token中获取用户类型(如果token中没有,则默认为"user")
|
||||
userType := claims.UserType
|
||||
if userType == "" {
|
||||
userType = "user"
|
||||
}
|
||||
ctx.Input.SetData("userType", userType)
|
||||
}
|
||||
}
|
||||
|
||||
+251
-251
@@ -1,251 +1,251 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
)
|
||||
|
||||
const (
|
||||
oplogStartKey = "__oplog_start"
|
||||
oplogReqBodyKey = "__oplog_req_body"
|
||||
)
|
||||
|
||||
// BeginOperationLog 在 BeforeRouter 采集请求信息
|
||||
func BeginOperationLog(ctx *context.Context) {
|
||||
url := ctx.Input.URL()
|
||||
method := ctx.Input.Method()
|
||||
if shouldSkipLogging(method, url) {
|
||||
return
|
||||
}
|
||||
ctx.Input.SetData(oplogStartKey, time.Now())
|
||||
|
||||
// 请求体由 main.go 的 CopyBody 保留在 Input.RequestBody
|
||||
if rb := ctx.Input.RequestBody; len(rb) > 0 {
|
||||
s := string(rb)
|
||||
ctx.Input.SetData(oplogReqBodyKey, truncateString(maskSensitive(s), 5000))
|
||||
}
|
||||
}
|
||||
|
||||
// FinishOperationLog 在 FinishRouter 统一落库到 yz_system_operation_log
|
||||
func FinishOperationLog(ctx *context.Context) {
|
||||
url := ctx.Input.URL()
|
||||
method := ctx.Input.Method()
|
||||
if shouldSkipLogging(method, url) {
|
||||
return
|
||||
}
|
||||
|
||||
start, _ := ctx.Input.GetData(oplogStartKey).(time.Time)
|
||||
if start.IsZero() {
|
||||
start = time.Now()
|
||||
}
|
||||
execSec := float64(time.Since(start).Milliseconds()) / 1000.0
|
||||
|
||||
uid := parseUint64FromCtx(ctx.Input.GetData("userId"))
|
||||
tidVal := parseUint64FromCtx(ctx.Input.GetData("tenantId"))
|
||||
var tid *uint64
|
||||
if tidVal > 0 {
|
||||
tid = &tidVal
|
||||
}
|
||||
|
||||
module := parseModule(url)
|
||||
action := parseAction(method, url)
|
||||
ip := ctx.Input.IP()
|
||||
userAgent := truncateString(ctx.Input.Header("User-Agent"), 500)
|
||||
status := int8(1)
|
||||
if code := ctx.ResponseWriter.Status; code >= 400 {
|
||||
status = 0
|
||||
}
|
||||
|
||||
var reqData *string
|
||||
if v, ok := ctx.Input.GetData(oplogReqBodyKey).(string); ok && strings.TrimSpace(v) != "" {
|
||||
reqData = &v
|
||||
} else if q := strings.TrimSpace(ctx.Request.URL.RawQuery); q != "" {
|
||||
q = truncateString(maskSensitive(q), 5000)
|
||||
reqData = &q
|
||||
}
|
||||
|
||||
var respData *string
|
||||
if code := ctx.ResponseWriter.Status; code >= 400 {
|
||||
msg := "HTTP " + strconv.Itoa(code)
|
||||
respData = &msg
|
||||
}
|
||||
|
||||
var errMsg *string
|
||||
if status == 0 {
|
||||
msg := "请求失败"
|
||||
if respData != nil {
|
||||
msg = *respData
|
||||
}
|
||||
errMsg = &msg
|
||||
}
|
||||
|
||||
logRow := &models.SystemOperationLog{
|
||||
Tid: tid,
|
||||
UserID: uid,
|
||||
Module: module,
|
||||
Action: action,
|
||||
Method: method,
|
||||
URL: truncateString(url, 255),
|
||||
IP: truncateString(ip, 50),
|
||||
UserAgent: userAgent,
|
||||
RequestData: reqData,
|
||||
ResponseData: respData,
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
ExecutionTime: execSec,
|
||||
}
|
||||
_, _ = models.Orm.Insert(logRow)
|
||||
}
|
||||
|
||||
func parseAction(method, url string) string {
|
||||
u := strings.ToLower(url)
|
||||
if strings.Contains(u, "login") {
|
||||
return "登录"
|
||||
}
|
||||
if strings.Contains(u, "logout") {
|
||||
return "退出"
|
||||
}
|
||||
if strings.Contains(u, "upload") {
|
||||
return "上传"
|
||||
}
|
||||
switch method {
|
||||
case "POST":
|
||||
if strings.Contains(u, "delete") {
|
||||
return "删除"
|
||||
}
|
||||
if strings.Contains(u, "update") || strings.Contains(u, "edit") || strings.Contains(u, "rename") {
|
||||
return "编辑"
|
||||
}
|
||||
if strings.Contains(u, "create") || strings.Contains(u, "add") {
|
||||
return "新增"
|
||||
}
|
||||
return "提交"
|
||||
case "PUT", "PATCH":
|
||||
return "编辑"
|
||||
case "DELETE":
|
||||
return "删除"
|
||||
default:
|
||||
return "查询"
|
||||
}
|
||||
}
|
||||
|
||||
func parseModule(url string) string {
|
||||
path := strings.Trim(strings.ToLower(url), "/")
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) >= 2 {
|
||||
return truncateString(parts[1], 50)
|
||||
}
|
||||
if len(parts) == 1 && parts[0] != "" {
|
||||
return truncateString(parts[0], 50)
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func shouldSkipLogging(method, url string) bool {
|
||||
skipPatterns := []string{
|
||||
"/static/",
|
||||
"/uploads/",
|
||||
"/favicon.ico",
|
||||
"/health",
|
||||
"/ping",
|
||||
}
|
||||
for _, pattern := range skipPatterns {
|
||||
if strings.HasPrefix(url, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 高频噪声接口:默认跳过(可按需再扩充)
|
||||
if method == "GET" {
|
||||
noisyExact := map[string]bool{
|
||||
"/platform/currentUser": true,
|
||||
"/platform/allmenu": true,
|
||||
"/platform/getOpenVerify": true, // 若未来改名/迁移可再调整
|
||||
}
|
||||
if noisyExact[url] {
|
||||
return true
|
||||
}
|
||||
// 菜单详情/列表类:频率高且多为前端路由加载
|
||||
if strings.HasPrefix(url, "/platform/menu/") {
|
||||
return true
|
||||
}
|
||||
// 登录页极验配置轮询/获取(不影响关键业务)
|
||||
if strings.HasPrefix(url, "/platform/login/getGeetest") || strings.HasPrefix(url, "/platform/login/getOpenVerify") {
|
||||
return true
|
||||
}
|
||||
// 客户端高频版本检查
|
||||
if strings.HasPrefix(url, "/api/softwareupgrade/check") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseUint64FromCtx(v interface{}) uint64 {
|
||||
switch x := v.(type) {
|
||||
case int:
|
||||
if x > 0 {
|
||||
return uint64(x)
|
||||
}
|
||||
case int64:
|
||||
if x > 0 {
|
||||
return uint64(x)
|
||||
}
|
||||
case uint64:
|
||||
return x
|
||||
case float64:
|
||||
if x > 0 {
|
||||
return uint64(x)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func truncateString(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "..."
|
||||
}
|
||||
|
||||
func maskSensitive(s string) string {
|
||||
// 尝试 JSON 脱敏(失败则返回原文)
|
||||
var obj interface{}
|
||||
if err := json.Unmarshal([]byte(s), &obj); err != nil {
|
||||
return s
|
||||
}
|
||||
maskInObj(&obj)
|
||||
bs, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
func maskInObj(v *interface{}) {
|
||||
switch t := (*v).(type) {
|
||||
case map[string]interface{}:
|
||||
for k, val := range t {
|
||||
lk := strings.ToLower(k)
|
||||
if lk == "password" || lk == "pwd" || lk == "token" || lk == "api_key" || lk == "api_secret" || lk == "authorization" {
|
||||
t[k] = "***"
|
||||
continue
|
||||
}
|
||||
tmp := val
|
||||
maskInObj(&tmp)
|
||||
t[k] = tmp
|
||||
}
|
||||
case []interface{}:
|
||||
for i := range t {
|
||||
tmp := t[i]
|
||||
maskInObj(&tmp)
|
||||
t[i] = tmp
|
||||
}
|
||||
}
|
||||
}
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
)
|
||||
|
||||
const (
|
||||
oplogStartKey = "__oplog_start"
|
||||
oplogReqBodyKey = "__oplog_req_body"
|
||||
)
|
||||
|
||||
// BeginOperationLog 在 BeforeRouter 采集请求信息
|
||||
func BeginOperationLog(ctx *context.Context) {
|
||||
url := ctx.Input.URL()
|
||||
method := ctx.Input.Method()
|
||||
if shouldSkipLogging(method, url) {
|
||||
return
|
||||
}
|
||||
ctx.Input.SetData(oplogStartKey, time.Now())
|
||||
|
||||
// 请求体由 main.go 的 CopyBody 保留在 Input.RequestBody
|
||||
if rb := ctx.Input.RequestBody; len(rb) > 0 {
|
||||
s := string(rb)
|
||||
ctx.Input.SetData(oplogReqBodyKey, truncateString(maskSensitive(s), 5000))
|
||||
}
|
||||
}
|
||||
|
||||
// FinishOperationLog 在 FinishRouter 统一落库到 yz_system_operation_log
|
||||
func FinishOperationLog(ctx *context.Context) {
|
||||
url := ctx.Input.URL()
|
||||
method := ctx.Input.Method()
|
||||
if shouldSkipLogging(method, url) {
|
||||
return
|
||||
}
|
||||
|
||||
start, _ := ctx.Input.GetData(oplogStartKey).(time.Time)
|
||||
if start.IsZero() {
|
||||
start = time.Now()
|
||||
}
|
||||
execSec := float64(time.Since(start).Milliseconds()) / 1000.0
|
||||
|
||||
uid := parseUint64FromCtx(ctx.Input.GetData("userId"))
|
||||
tidVal := parseUint64FromCtx(ctx.Input.GetData("tenantId"))
|
||||
var tid *uint64
|
||||
if tidVal > 0 {
|
||||
tid = &tidVal
|
||||
}
|
||||
|
||||
module := parseModule(url)
|
||||
action := parseAction(method, url)
|
||||
ip := ctx.Input.IP()
|
||||
userAgent := truncateString(ctx.Input.Header("User-Agent"), 500)
|
||||
status := int8(1)
|
||||
if code := ctx.ResponseWriter.Status; code >= 400 {
|
||||
status = 0
|
||||
}
|
||||
|
||||
var reqData *string
|
||||
if v, ok := ctx.Input.GetData(oplogReqBodyKey).(string); ok && strings.TrimSpace(v) != "" {
|
||||
reqData = &v
|
||||
} else if q := strings.TrimSpace(ctx.Request.URL.RawQuery); q != "" {
|
||||
q = truncateString(maskSensitive(q), 5000)
|
||||
reqData = &q
|
||||
}
|
||||
|
||||
var respData *string
|
||||
if code := ctx.ResponseWriter.Status; code >= 400 {
|
||||
msg := "HTTP " + strconv.Itoa(code)
|
||||
respData = &msg
|
||||
}
|
||||
|
||||
var errMsg *string
|
||||
if status == 0 {
|
||||
msg := "请求失败"
|
||||
if respData != nil {
|
||||
msg = *respData
|
||||
}
|
||||
errMsg = &msg
|
||||
}
|
||||
|
||||
logRow := &models.SystemOperationLog{
|
||||
Tid: tid,
|
||||
UserID: uid,
|
||||
Module: module,
|
||||
Action: action,
|
||||
Method: method,
|
||||
URL: truncateString(url, 255),
|
||||
IP: truncateString(ip, 50),
|
||||
UserAgent: userAgent,
|
||||
RequestData: reqData,
|
||||
ResponseData: respData,
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
ExecutionTime: execSec,
|
||||
}
|
||||
_, _ = models.Orm.Insert(logRow)
|
||||
}
|
||||
|
||||
func parseAction(method, url string) string {
|
||||
u := strings.ToLower(url)
|
||||
if strings.Contains(u, "login") {
|
||||
return "登录"
|
||||
}
|
||||
if strings.Contains(u, "logout") {
|
||||
return "退出"
|
||||
}
|
||||
if strings.Contains(u, "upload") {
|
||||
return "上传"
|
||||
}
|
||||
switch method {
|
||||
case "POST":
|
||||
if strings.Contains(u, "delete") {
|
||||
return "删除"
|
||||
}
|
||||
if strings.Contains(u, "update") || strings.Contains(u, "edit") || strings.Contains(u, "rename") {
|
||||
return "编辑"
|
||||
}
|
||||
if strings.Contains(u, "create") || strings.Contains(u, "add") {
|
||||
return "新增"
|
||||
}
|
||||
return "提交"
|
||||
case "PUT", "PATCH":
|
||||
return "编辑"
|
||||
case "DELETE":
|
||||
return "删除"
|
||||
default:
|
||||
return "查询"
|
||||
}
|
||||
}
|
||||
|
||||
func parseModule(url string) string {
|
||||
path := strings.Trim(strings.ToLower(url), "/")
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) >= 2 {
|
||||
return truncateString(parts[1], 50)
|
||||
}
|
||||
if len(parts) == 1 && parts[0] != "" {
|
||||
return truncateString(parts[0], 50)
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func shouldSkipLogging(method, url string) bool {
|
||||
skipPatterns := []string{
|
||||
"/static/",
|
||||
"/uploads/",
|
||||
"/favicon.ico",
|
||||
"/health",
|
||||
"/ping",
|
||||
}
|
||||
for _, pattern := range skipPatterns {
|
||||
if strings.HasPrefix(url, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 高频噪声接口:默认跳过(可按需再扩充)
|
||||
if method == "GET" {
|
||||
noisyExact := map[string]bool{
|
||||
"/platform/currentUser": true,
|
||||
"/platform/allmenu": true,
|
||||
"/platform/getOpenVerify": true, // 若未来改名/迁移可再调整
|
||||
}
|
||||
if noisyExact[url] {
|
||||
return true
|
||||
}
|
||||
// 菜单详情/列表类:频率高且多为前端路由加载
|
||||
if strings.HasPrefix(url, "/platform/menu/") {
|
||||
return true
|
||||
}
|
||||
// 登录页极验配置轮询/获取(不影响关键业务)
|
||||
if strings.HasPrefix(url, "/platform/login/getGeetest") || strings.HasPrefix(url, "/platform/login/getOpenVerify") {
|
||||
return true
|
||||
}
|
||||
// 客户端高频版本检查
|
||||
if strings.HasPrefix(url, "/api/softwareupgrade/check") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseUint64FromCtx(v interface{}) uint64 {
|
||||
switch x := v.(type) {
|
||||
case int:
|
||||
if x > 0 {
|
||||
return uint64(x)
|
||||
}
|
||||
case int64:
|
||||
if x > 0 {
|
||||
return uint64(x)
|
||||
}
|
||||
case uint64:
|
||||
return x
|
||||
case float64:
|
||||
if x > 0 {
|
||||
return uint64(x)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func truncateString(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "..."
|
||||
}
|
||||
|
||||
func maskSensitive(s string) string {
|
||||
// 尝试 JSON 脱敏(失败则返回原文)
|
||||
var obj interface{}
|
||||
if err := json.Unmarshal([]byte(s), &obj); err != nil {
|
||||
return s
|
||||
}
|
||||
maskInObj(&obj)
|
||||
bs, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
func maskInObj(v *interface{}) {
|
||||
switch t := (*v).(type) {
|
||||
case map[string]interface{}:
|
||||
for k, val := range t {
|
||||
lk := strings.ToLower(k)
|
||||
if lk == "password" || lk == "pwd" || lk == "token" || lk == "api_key" || lk == "api_secret" || lk == "authorization" {
|
||||
t[k] = "***"
|
||||
continue
|
||||
}
|
||||
tmp := val
|
||||
maskInObj(&tmp)
|
||||
t[k] = tmp
|
||||
}
|
||||
case []interface{}:
|
||||
for i := range t {
|
||||
tmp := t[i]
|
||||
maskInObj(&tmp)
|
||||
t[i] = tmp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+188
-188
@@ -1,188 +1,188 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"server/services"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
)
|
||||
|
||||
// PermissionMiddleware 权限验证中间件
|
||||
// 根据路由的权限标识检查用户是否有访问权限
|
||||
func PermissionMiddleware() func(ctx *context.Context) {
|
||||
return func(ctx *context.Context) {
|
||||
// 获取当前请求的路径
|
||||
path := ctx.Input.URL()
|
||||
|
||||
// 不需要权限验证的路径列表
|
||||
publicPaths := []string{
|
||||
"/api/login",
|
||||
"/api/logout",
|
||||
"/api/reset-password",
|
||||
"/api/program-categories/public",
|
||||
"/api/program-infos/public",
|
||||
"/api/files/public",
|
||||
}
|
||||
|
||||
// 检查是否为公开路径
|
||||
for _, p := range publicPaths {
|
||||
if path == p {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否为公开预览接口
|
||||
if strings.HasPrefix(path, "/api/files/public-preview/") {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户ID
|
||||
userIdData := ctx.Input.GetData("userId")
|
||||
if userIdData == nil {
|
||||
// 如果没有用户ID,说明未登录,这个应该在JWT中间件中处理
|
||||
// 这里直接返回,因为JWT中间件已经拦截了
|
||||
return
|
||||
}
|
||||
|
||||
userId, ok := userIdData.(int)
|
||||
if !ok {
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "用户ID格式错误",
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前路由对应的权限标识
|
||||
permission := getPermissionByPath(path, ctx.Input.Method())
|
||||
|
||||
// 如果没有权限标识,说明该接口不需要权限控制
|
||||
if permission == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户是否拥有该权限
|
||||
hasPermission, err := services.CheckUserPermission(userId, permission)
|
||||
if err != nil {
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "权限验证失败",
|
||||
"error": err.Error(),
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
|
||||
if !hasPermission {
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "您没有权限访问此接口",
|
||||
"code": 403,
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getPermissionByPath 根据路径和方法获取权限标识
|
||||
// 这是一个简化版本,实际应该从数据库中动态获取路由-权限映射关系
|
||||
func getPermissionByPath(path, method string) string {
|
||||
// 权限映射表(路径模式 -> 权限标识)
|
||||
// 这里只列举了部分示例,实际应该从数据库中加载
|
||||
permissionMap := map[string]string{
|
||||
// 用户管理
|
||||
"GET:/api/allUsers": "user:list",
|
||||
"GET:/api/user/:id": "user:detail",
|
||||
"POST:/api/addUser": "user:add",
|
||||
"POST:/api/editUser/:id": "user:edit",
|
||||
"DELETE:/api/deleteUser/:id": "user:delete",
|
||||
"POST:/api/changePassword/:id":"user:changePassword",
|
||||
|
||||
// 角色管理
|
||||
"GET:/api/roles": "role:list",
|
||||
"POST:/api/roles": "role:create",
|
||||
"GET:/api/roles/:id": "role:detail",
|
||||
"POST:/api/roles/:id": "role:update",
|
||||
"DELETE:/api/roles/:id": "role:delete",
|
||||
|
||||
// 菜单管理
|
||||
"GET:/api/allmenu": "menu:list",
|
||||
"POST:/api/menu": "menu:create",
|
||||
"PUT:/api/menu/:id": "menu:update",
|
||||
"DELETE:/api/menu/:id": "menu:delete",
|
||||
|
||||
// 文件管理
|
||||
"GET:/api/files": "file:list",
|
||||
"POST:/api/files": "file:upload",
|
||||
"GET:/api/files/my": "file:my",
|
||||
"GET:/api/files/download/:id": "file:download",
|
||||
"GET:/api/files/preview/:id": "file:preview",
|
||||
"GET:/api/files/:id": "file:detail",
|
||||
"PUT:/api/files/:id": "file:update",
|
||||
"DELETE:/api/files/:id": "file:delete",
|
||||
"GET:/api/files/search": "file:search",
|
||||
"GET:/api/files/statistics": "file:statistics",
|
||||
|
||||
// 租户管理
|
||||
"GET:/api/tenant/list": "tenant:list",
|
||||
"POST:/api/tenant": "tenant:create",
|
||||
"PUT:/api/tenant/:id": "tenant:update",
|
||||
"DELETE:/api/tenant/:id": "tenant:delete",
|
||||
"POST:/api/tenant/:id/audit": "tenant:audit",
|
||||
"GET:/api/tenant/:id": "tenant:detail",
|
||||
|
||||
// 知识库
|
||||
"GET:/api/knowledge/list": "knowledge:list",
|
||||
"GET:/api/knowledge/detail": "knowledge:detail",
|
||||
"POST:/api/knowledge/create": "knowledge:create",
|
||||
"POST:/api/knowledge/update": "knowledge:update",
|
||||
"POST:/api/knowledge/delete": "knowledge:delete",
|
||||
}
|
||||
|
||||
// 匹配路径(简化版本,不支持动态参数匹配)
|
||||
key := method + ":" + path
|
||||
if perm, ok := permissionMap[key]; ok {
|
||||
return perm
|
||||
}
|
||||
|
||||
// 尝试匹配动态路由(简单的ID参数替换)
|
||||
// 例如:/api/user/123 -> /api/user/:id
|
||||
pathParts := strings.Split(path, "/")
|
||||
for pattern, perm := range permissionMap {
|
||||
parts := strings.Split(pattern, ":")
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
methodPart := parts[0]
|
||||
pathPattern := parts[1]
|
||||
|
||||
if methodPart != method {
|
||||
continue
|
||||
}
|
||||
|
||||
patternParts := strings.Split(pathPattern, "/")
|
||||
if len(patternParts) != len(pathParts) {
|
||||
continue
|
||||
}
|
||||
|
||||
match := true
|
||||
for i, part := range patternParts {
|
||||
if strings.HasPrefix(part, ":") {
|
||||
// 动态参数,跳过
|
||||
continue
|
||||
}
|
||||
if part != pathParts[i] {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if match {
|
||||
return perm
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到匹配的权限标识,返回空字符串(表示不需要权限控制)
|
||||
return ""
|
||||
}
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"server/services"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
)
|
||||
|
||||
// PermissionMiddleware 权限验证中间件
|
||||
// 根据路由的权限标识检查用户是否有访问权限
|
||||
func PermissionMiddleware() func(ctx *context.Context) {
|
||||
return func(ctx *context.Context) {
|
||||
// 获取当前请求的路径
|
||||
path := ctx.Input.URL()
|
||||
|
||||
// 不需要权限验证的路径列表
|
||||
publicPaths := []string{
|
||||
"/api/login",
|
||||
"/api/logout",
|
||||
"/api/reset-password",
|
||||
"/api/program-categories/public",
|
||||
"/api/program-infos/public",
|
||||
"/api/files/public",
|
||||
}
|
||||
|
||||
// 检查是否为公开路径
|
||||
for _, p := range publicPaths {
|
||||
if path == p {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否为公开预览接口
|
||||
if strings.HasPrefix(path, "/api/files/public-preview/") {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户ID
|
||||
userIdData := ctx.Input.GetData("userId")
|
||||
if userIdData == nil {
|
||||
// 如果没有用户ID,说明未登录,这个应该在JWT中间件中处理
|
||||
// 这里直接返回,因为JWT中间件已经拦截了
|
||||
return
|
||||
}
|
||||
|
||||
userId, ok := userIdData.(int)
|
||||
if !ok {
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "用户ID格式错误",
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前路由对应的权限标识
|
||||
permission := getPermissionByPath(path, ctx.Input.Method())
|
||||
|
||||
// 如果没有权限标识,说明该接口不需要权限控制
|
||||
if permission == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户是否拥有该权限
|
||||
hasPermission, err := services.CheckUserPermission(userId, permission)
|
||||
if err != nil {
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "权限验证失败",
|
||||
"error": err.Error(),
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
|
||||
if !hasPermission {
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "您没有权限访问此接口",
|
||||
"code": 403,
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getPermissionByPath 根据路径和方法获取权限标识
|
||||
// 这是一个简化版本,实际应该从数据库中动态获取路由-权限映射关系
|
||||
func getPermissionByPath(path, method string) string {
|
||||
// 权限映射表(路径模式 -> 权限标识)
|
||||
// 这里只列举了部分示例,实际应该从数据库中加载
|
||||
permissionMap := map[string]string{
|
||||
// 用户管理
|
||||
"GET:/api/allUsers": "user:list",
|
||||
"GET:/api/user/:id": "user:detail",
|
||||
"POST:/api/addUser": "user:add",
|
||||
"POST:/api/editUser/:id": "user:edit",
|
||||
"DELETE:/api/deleteUser/:id": "user:delete",
|
||||
"POST:/api/changePassword/:id":"user:changePassword",
|
||||
|
||||
// 角色管理
|
||||
"GET:/api/roles": "role:list",
|
||||
"POST:/api/roles": "role:create",
|
||||
"GET:/api/roles/:id": "role:detail",
|
||||
"POST:/api/roles/:id": "role:update",
|
||||
"DELETE:/api/roles/:id": "role:delete",
|
||||
|
||||
// 菜单管理
|
||||
"GET:/api/allmenu": "menu:list",
|
||||
"POST:/api/menu": "menu:create",
|
||||
"PUT:/api/menu/:id": "menu:update",
|
||||
"DELETE:/api/menu/:id": "menu:delete",
|
||||
|
||||
// 文件管理
|
||||
"GET:/api/files": "file:list",
|
||||
"POST:/api/files": "file:upload",
|
||||
"GET:/api/files/my": "file:my",
|
||||
"GET:/api/files/download/:id": "file:download",
|
||||
"GET:/api/files/preview/:id": "file:preview",
|
||||
"GET:/api/files/:id": "file:detail",
|
||||
"PUT:/api/files/:id": "file:update",
|
||||
"DELETE:/api/files/:id": "file:delete",
|
||||
"GET:/api/files/search": "file:search",
|
||||
"GET:/api/files/statistics": "file:statistics",
|
||||
|
||||
// 租户管理
|
||||
"GET:/api/tenant/list": "tenant:list",
|
||||
"POST:/api/tenant": "tenant:create",
|
||||
"PUT:/api/tenant/:id": "tenant:update",
|
||||
"DELETE:/api/tenant/:id": "tenant:delete",
|
||||
"POST:/api/tenant/:id/audit": "tenant:audit",
|
||||
"GET:/api/tenant/:id": "tenant:detail",
|
||||
|
||||
// 知识库
|
||||
"GET:/api/knowledge/list": "knowledge:list",
|
||||
"GET:/api/knowledge/detail": "knowledge:detail",
|
||||
"POST:/api/knowledge/create": "knowledge:create",
|
||||
"POST:/api/knowledge/update": "knowledge:update",
|
||||
"POST:/api/knowledge/delete": "knowledge:delete",
|
||||
}
|
||||
|
||||
// 匹配路径(简化版本,不支持动态参数匹配)
|
||||
key := method + ":" + path
|
||||
if perm, ok := permissionMap[key]; ok {
|
||||
return perm
|
||||
}
|
||||
|
||||
// 尝试匹配动态路由(简单的ID参数替换)
|
||||
// 例如:/api/user/123 -> /api/user/:id
|
||||
pathParts := strings.Split(path, "/")
|
||||
for pattern, perm := range permissionMap {
|
||||
parts := strings.Split(pattern, ":")
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
methodPart := parts[0]
|
||||
pathPattern := parts[1]
|
||||
|
||||
if methodPart != method {
|
||||
continue
|
||||
}
|
||||
|
||||
patternParts := strings.Split(pathPattern, "/")
|
||||
if len(patternParts) != len(pathParts) {
|
||||
continue
|
||||
}
|
||||
|
||||
match := true
|
||||
for i, part := range patternParts {
|
||||
if strings.HasPrefix(part, ":") {
|
||||
// 动态参数,跳过
|
||||
continue
|
||||
}
|
||||
if part != pathParts[i] {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if match {
|
||||
return perm
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到匹配的权限标识,返回空字符串(表示不需要权限控制)
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user