first commit

This commit is contained in:
2026-03-31 17:28:07 +08:00
commit 36929736c1
27 changed files with 4630 additions and 0 deletions
+75
View File
@@ -0,0 +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)
}
}
+222
View File
@@ -0,0 +1,222 @@
package middleware
import (
"bytes"
"fmt"
"io"
"server/models"
"server/services"
"strconv"
"strings"
"time"
"github.com/beego/beego/v2/server/web/context"
)
// OperationLogMiddleware 操作日志中间件 - 记录所有接口的调用记录
func OperationLogMiddleware(ctx *context.Context) {
// 跳过静态资源和内部路由
url := ctx.Input.URL()
if shouldSkipLogging(url) {
return
}
method := ctx.Input.Method()
// 获取用户信息和租户信息(由 JWT 中间件设置在 Input.Data 中)
userId := 0
tenantId := 0
username := ""
userType := "" // 用户类型:user(平台用户) 或 employee(租户员工)
if v := ctx.Input.GetData("userId"); v != nil {
if id, ok := v.(int); ok {
userId = id
}
}
if v := ctx.Input.GetData("tenantId"); v != nil {
if id, ok := v.(int); ok {
tenantId = id
}
}
if v := ctx.Input.GetData("username"); v != nil {
if s, ok := v.(string); ok {
username = s
}
}
if v := ctx.Input.GetData("userType"); v != nil {
if s, ok := v.(string); ok {
userType = s
}
}
// 用户信息补全
if username == "" {
username = "anonymous"
}
// 读取请求体(对于有请求体的方法)
var requestBody string
if method == "POST" || method == "PUT" || method == "PATCH" {
body, err := io.ReadAll(ctx.Request.Body)
if err == nil && len(body) > 0 {
requestBody = string(body)
// 重置请求体,使其可以被后续处理
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body))
}
}
startTime := time.Now()
ipAddress := ctx.Input.IP()
userAgent := ctx.Input.Header("User-Agent")
queryString := ctx.Request.URL.RawQuery
// 使用延迟函数来记录操作
defer func() {
duration := time.Since(startTime)
// 解析操作相关信息
operation := parseOperationType(method, url)
module := parseModule(url)
resourceType := parseResourceType(url)
resourceId := parseResourceId(url)
// 为所有接口都记录日志
log := &models.OperationLog{
TenantId: tenantId,
UserId: userId,
Username: username,
Module: module,
ResourceType: resourceType,
Operation: operation,
IpAddress: ipAddress,
UserAgent: userAgent,
RequestMethod: method,
RequestUrl: url,
Status: 1, // 默认成功
Duration: int(duration.Milliseconds()),
CreateTime: time.Now(),
}
// 设置资源ID
if resourceId > 0 {
log.ResourceId = &resourceId
}
// 记录请求信息到Description
var description strings.Builder
if requestBody != "" {
description.WriteString("Request: " + truncateString(requestBody, 500))
}
if queryString != "" {
if description.Len() > 0 {
description.WriteString(" | ")
}
description.WriteString("Query: " + queryString)
}
log.Description = description.String()
// 如果有请求体,作为NewValue保存
if requestBody != "" {
log.NewValue = requestBody
}
// 添加用户类型信息到Description
if userType != "" {
if log.Description != "" {
log.Description += " | "
}
log.Description += "UserType: " + userType
}
// 调用服务层保存日志
if err := services.AddOperationLog(log); err != nil {
fmt.Printf("Failed to save operation log: %v\n", err)
}
}()
}
// parseOperationType 根据HTTP方法解析操作类型
func parseOperationType(method, url string) string {
switch method {
case "POST":
// 检查URL是否包含特定的操作关键字
if strings.Contains(url, "login") {
return "LOGIN"
}
if strings.Contains(url, "logout") {
return "LOGOUT"
}
if strings.Contains(url, "add") || strings.Contains(url, "create") {
return "CREATE"
}
return "CREATE"
case "PUT", "PATCH":
return "UPDATE"
case "DELETE":
return "DELETE"
default:
return "READ"
}
}
// parseResourceType 根据URL解析资源类型
func parseResourceType(url string) string {
parts := strings.Split(strings.TrimPrefix(url, "/api/"), "/")
if len(parts) > 0 {
// 移除复数形式的s
resourceType := strings.TrimSuffix(parts[0], "s")
return resourceType
}
return "unknown"
}
// parseResourceId 从URL中提取资源ID
func parseResourceId(url string) int {
parts := strings.Split(strings.TrimPrefix(url, "/api/"), "/")
if len(parts) >= 2 {
// 尝试解析第二个部分为ID
if id, err := strconv.Atoi(parts[1]); err == nil {
return id
}
}
return 0
}
// parseModule 根据URL解析模块名称
func parseModule(url string) string {
// 返回与 sys_operation_log.module 字段匹配的短code(例如 dict、user 等)
parts := strings.Split(strings.TrimPrefix(url, "/api/"), "/")
if len(parts) > 0 {
return strings.ToLower(parts[0])
}
return "unknown"
}
// shouldSkipLogging 判断是否需要跳过日志记录
func shouldSkipLogging(url string) bool {
// 跳过静态资源、健康检查等
skipPatterns := []string{
"/static/",
"/uploads/",
"/favicon.ico",
"/health",
"/ping",
}
for _, pattern := range skipPatterns {
if strings.HasPrefix(url, pattern) {
return true
}
}
return false
}
// truncateString 截断字符串到指定长度
func truncateString(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}
+188
View File
@@ -0,0 +1,188 @@
package middleware
import (
"server/models"
"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 := models.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 ""
}