379 lines
11 KiB
Go
379 lines
11 KiB
Go
// package middleware
|
|
// @Link https://github.com/xiujiecn/xiu-admin
|
|
// @Copyright Copyright (c) 2025 LiXiujie
|
|
// @Author Lxj <li@example.com>
|
|
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
|
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
|
|
|
|
"github.com/gogf/gf/v2/errors/gcode"
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
"github.com/gogf/gf/v2/net/ghttp"
|
|
"github.com/gogf/gf/v2/util/gmeta"
|
|
|
|
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
|
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/appidentity"
|
|
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/bcache"
|
|
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
|
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/mcache"
|
|
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
|
"git.5iot.com/iotlcm-public/iotlcm-app/internal/packed/response"
|
|
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
|
"git.5iot.com/iotlcm-public/iotlcm-app/utility/queue"
|
|
)
|
|
|
|
type sMiddleware struct {
|
|
}
|
|
|
|
func New() *sMiddleware {
|
|
return &sMiddleware{}
|
|
}
|
|
|
|
func init() {
|
|
service.RegisterMiddleware(New())
|
|
}
|
|
|
|
func getContentType(r *ghttp.Request) (contentType string) {
|
|
contentType = r.Response.Header().Get("Content-Type")
|
|
if contentType != "" {
|
|
return
|
|
}
|
|
|
|
mime := gmeta.Get(r.GetHandlerResponse(), "mime").String()
|
|
if mime == "" {
|
|
contentType = consts.HTTPContentTypeJson
|
|
} else {
|
|
contentType = mime
|
|
}
|
|
return
|
|
}
|
|
|
|
// 响应处理中间件
|
|
func (m *sMiddleware) ResponseHandler(r *ghttp.Request) {
|
|
r.Middleware.Next()
|
|
if r.Response.BufferLength() > 0 {
|
|
return
|
|
}
|
|
if contexts.IsWebSocket(r.GetCtx()) {
|
|
return
|
|
}
|
|
contentType := getContentType(r)
|
|
if contentType == consts.HTTPContentTypeExcel {
|
|
r.Response.Status = http.StatusOK
|
|
return
|
|
}
|
|
// g.Log().Infof(r.GetCtx(), "sMiddleware.ResponseHandler,contentType %v", contentType)
|
|
err := r.GetError()
|
|
res := r.GetHandlerResponse()
|
|
var code gcode.Code = gcode.CodeOK
|
|
|
|
if err != nil {
|
|
code = gerror.Code(err)
|
|
if code == gcode.CodeNil {
|
|
code = gcode.CodeInternalError
|
|
}
|
|
response.JsonExit(r, code.Code(), err.Error())
|
|
} else {
|
|
response.Json(r, code.Code(), "", res)
|
|
}
|
|
}
|
|
|
|
// Ctx 自定义上下文对象
|
|
func (s *sMiddleware) Ctx(r *ghttp.Request) {
|
|
ctx := r.GetCtx()
|
|
r.SetCtx(r.GetNeverDoneCtx())
|
|
|
|
// 初始化登录用户信息
|
|
data, err := service.SysAuth().GetCurrentUser(ctx)
|
|
if err != nil {
|
|
r.Middleware.Next()
|
|
return
|
|
}
|
|
if data != nil {
|
|
contextModel := new(model.Context)
|
|
contextModel.User = &model.Identity{
|
|
BaseClaims: data.BaseClaims,
|
|
}
|
|
contexts.Init(r, contextModel)
|
|
}
|
|
r.Middleware.Next()
|
|
}
|
|
|
|
// CORS 跨域处理
|
|
func (s *sMiddleware) CORS(r *ghttp.Request) {
|
|
r.SetCtx(r.GetNeverDoneCtx())
|
|
if contexts.IsWebSocket(r.GetCtx()) {
|
|
r.Middleware.Next()
|
|
return
|
|
}
|
|
corsOptions := r.Response.DefaultCORSOptions()
|
|
corsConfig := g.Cfg().MustGet(context.Background(), "server.allowedDomains").Strings()
|
|
if len(corsConfig) == 0 {
|
|
r.Response.CORSDefault()
|
|
} else {
|
|
corsOptions.AllowDomain = corsConfig
|
|
r.Response.CORS(corsOptions)
|
|
}
|
|
r.Middleware.Next()
|
|
}
|
|
|
|
// Auth 认证处理
|
|
func (s *sMiddleware) Auth(r *ghttp.Request) {
|
|
var (
|
|
ctx = r.Context()
|
|
method = r.Method
|
|
path = ""
|
|
)
|
|
if r.URL != nil {
|
|
path = r.URL.Path
|
|
}
|
|
// 不需要验证登录的路由地址
|
|
if s.IsExceptLogin(ctx, path) {
|
|
r.Middleware.Next()
|
|
return
|
|
}
|
|
|
|
userId := contexts.GetUserId(ctx)
|
|
tenantId := contexts.GetTenantId(ctx)
|
|
g.Log().Debugf(ctx, "sMiddleware.Auth userId: %d tenantId: %s method: %s path: %s, ================>>", userId, tenantId, method, path)
|
|
if userId == 0 {
|
|
g.Log().Error(ctx, "sMiddleware.Auth userId is 0", "path", path, "clientIp", r.GetClientIp(), "User-Agent", utility.GetUserAgent(ctx))
|
|
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
|
// r.Response.WriteStatus(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
// 如果是websocket,则不进行权限验证
|
|
// g.Log().Infof(ctx, "sMiddleware.Auth is websocket: %v", contexts.IsWebSocket(ctx))
|
|
if contexts.IsWebSocket(ctx) {
|
|
r.Middleware.Next()
|
|
return
|
|
}
|
|
// 是否校验强行退出用户
|
|
forceLogout, err := mcache.GetSystemConfig(ctx, consts.ConfigOnlineForceLogout, consts.ConfigOnlineForceLogoutFalse)
|
|
if err != nil {
|
|
g.Log().Errorf(ctx, "sMiddleware.Auth GetSystemConfig error: %v", err)
|
|
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
|
return
|
|
}
|
|
if forceLogout == consts.ConfigOnlineForceLogoutTrue {
|
|
uuid := contexts.GetUserUuid(ctx)
|
|
token, err := bcache.GetSysAuthTokenReject(ctx, userId, uuid)
|
|
if err != nil {
|
|
g.Log().Errorf(ctx, "sMiddleware.Auth GetSysAuthTokenReject error: %v", err)
|
|
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
|
return
|
|
}
|
|
if token != "" {
|
|
g.Log().Infof(ctx, "sMiddleware.Auth token is reject, userId: %d, uuid: %s", userId, uuid)
|
|
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
|
return
|
|
}
|
|
}
|
|
// 超级管理员
|
|
if contexts.IsSuperAdmin(r.GetCtx()) {
|
|
r.Middleware.Next()
|
|
return
|
|
}
|
|
// 权限验证
|
|
serveHandler := r.GetServeHandler()
|
|
userAccessCodeList, menuRoleDataAccessCodeList, err := mcache.GetUserAccessCodeList(ctx, userId)
|
|
if err != nil {
|
|
g.Log().Errorf(ctx, "sMiddleware.Auth GetUserAccessCodeList error: %v", err)
|
|
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
|
return
|
|
}
|
|
if serveHandler != nil {
|
|
g.Log().Infof(ctx, "sMiddleware.Auth serveHandler GetMetaTag(x-check-permission): %+v", serveHandler.GetMetaTag("x-check-permission"))
|
|
accessCode := serveHandler.GetMetaTag("x-check-permission")
|
|
if accessCode != "" {
|
|
accessCodeList := strings.Split(accessCode, ",")
|
|
hasPermission := false
|
|
for _, code := range accessCodeList {
|
|
code = strings.TrimSpace(code)
|
|
if code == consts.SysCheckPermissionTerminalUser {
|
|
if !isTerminalAppUser(ctx, userId) {
|
|
g.Log().Errorf(ctx, "sMiddleware.Auth terminal permission denied, userId: %d", userId)
|
|
continue
|
|
}
|
|
hasPermission = true
|
|
break
|
|
}
|
|
if strings.HasPrefix(code, consts.SysCheckPermissionPrefix) {
|
|
accessCodeItemList := splitAccessCodeExpression(code)
|
|
if len(accessCodeItemList) == 0 {
|
|
continue
|
|
}
|
|
if missingItem, ok := missingAccessCodeExpressionItem(userAccessCodeList, accessCodeItemList); ok {
|
|
g.Log().Errorf(ctx, "sMiddleware.Auth userAccessCodeList not contains item: %v, userAccessCodeList: %v", missingItem, userAccessCodeList)
|
|
continue
|
|
}
|
|
applyMatchedPermissionDataScope(ctx, menuRoleDataAccessCodeList, accessCodeItemList)
|
|
hasPermission = true
|
|
break
|
|
}
|
|
}
|
|
if !hasPermission {
|
|
response.JsonExit(r, consts.CodeNoPermission.Code(), consts.CodeNoPermission.Message())
|
|
return
|
|
}
|
|
}
|
|
}
|
|
r.Middleware.Next()
|
|
}
|
|
|
|
// matchAccessCode 检查权限码是否匹配,支持通配符 *
|
|
func matchAccessCode(pattern string, code string) bool {
|
|
pattern = normalizeAccessCode(pattern)
|
|
code = normalizeAccessCode(code)
|
|
if pattern == code {
|
|
return true
|
|
}
|
|
if strings.HasSuffix(pattern, "*") {
|
|
prefix := pattern[:len(pattern)-1]
|
|
return strings.HasPrefix(code, prefix)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func normalizeAccessCode(code string) string {
|
|
code = strings.TrimSpace(code)
|
|
for strings.HasPrefix(code, consts.SysCheckPermissionMenuPrefix+consts.SysCheckPermissionMenuPrefix) {
|
|
code = strings.TrimPrefix(code, consts.SysCheckPermissionMenuPrefix)
|
|
}
|
|
return code
|
|
}
|
|
|
|
func isTerminalAppUser(ctx context.Context, userId int64) bool {
|
|
if userId <= 0 {
|
|
return false
|
|
}
|
|
ok, err := appidentity.IsTerminalUserId(ctx, userId)
|
|
if err != nil {
|
|
g.Log().Errorf(ctx, "sMiddleware.Auth query app terminal identity failed, userId: %d, error: %v", userId, err)
|
|
return false
|
|
}
|
|
return ok
|
|
}
|
|
|
|
// hasAccessCode 检查用户权限码列表中是否存在匹配指定权限码的项。
|
|
func hasAccessCode(userAccessCodeList []string, requiredCode string) bool {
|
|
for _, userCode := range userAccessCodeList {
|
|
if matchAccessCode(userCode, requiredCode) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func splitAccessCodeExpression(code string) []string {
|
|
parts := strings.Split(code, "&")
|
|
result := make([]string, 0, len(parts))
|
|
for _, item := range parts {
|
|
item = strings.TrimSpace(item)
|
|
if item != "" {
|
|
result = append(result, item)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func hasAccessCodeExpression(userAccessCodeList []string, accessCodeItemList []string) bool {
|
|
if len(accessCodeItemList) == 0 {
|
|
return false
|
|
}
|
|
for _, item := range accessCodeItemList {
|
|
if !hasAccessCode(userAccessCodeList, item) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func missingAccessCodeExpressionItem(userAccessCodeList []string, accessCodeItemList []string) (string, bool) {
|
|
for _, item := range accessCodeItemList {
|
|
if !hasAccessCode(userAccessCodeList, item) {
|
|
return item, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func applyMatchedPermissionDataScope(ctx context.Context, menuRoleDataAccessCodeList []string, accessCodeItemList []string) {
|
|
roleId, roleDataScope, ok := matchedPermissionDataScope(menuRoleDataAccessCodeList, accessCodeItemList)
|
|
if !ok {
|
|
return
|
|
}
|
|
contexts.SetDataValue(ctx, "currentRoleId", roleId)
|
|
contexts.SetDataValue(ctx, "currentRoleDataScope", roleDataScope)
|
|
g.Log().Debugf(ctx, "sMiddleware.Auth currentRoleId: %s, currentRoleDataScope: %s", roleId, roleDataScope)
|
|
}
|
|
|
|
func matchedPermissionDataScope(menuRoleDataAccessCodeList []string, accessCodeItemList []string) (roleId string, roleDataScope string, ok bool) {
|
|
for _, item := range accessCodeItemList {
|
|
for _, roleDataAccessCode := range menuRoleDataAccessCodeList {
|
|
roleDataAccessCodeItemList := strings.Split(roleDataAccessCode, "|")
|
|
if len(roleDataAccessCodeItemList) != 3 {
|
|
continue
|
|
}
|
|
if !matchAccessCode(roleDataAccessCodeItemList[0], item) {
|
|
continue
|
|
}
|
|
return roleDataAccessCodeItemList[1], roleDataAccessCodeItemList[2], true
|
|
}
|
|
}
|
|
return "", "", false
|
|
}
|
|
|
|
// IsExceptLogin 判断是否需要验证登录
|
|
func (s *sMiddleware) IsExceptLogin(ctx context.Context, path string) bool {
|
|
pathList := g.Cfg().MustGet(ctx, "router.exceptLogin").Strings()
|
|
return slices.Contains(pathList, path)
|
|
}
|
|
|
|
// OperationLog 操作日志
|
|
func (s *sMiddleware) OperationLog(r *ghttp.Request) {
|
|
r.Middleware.Next()
|
|
ctx := r.Context()
|
|
enabledDBOperationLog := g.Cfg().MustGet(ctx, "server.enabledDBOperationLog").Bool()
|
|
if !enabledDBOperationLog {
|
|
return
|
|
}
|
|
data, err := service.SysOperLog().AnalysisLog(ctx)
|
|
if err != nil {
|
|
g.Log().Errorf(context.TODO(), "sMiddleware.OperationLog error:%v", err)
|
|
return
|
|
}
|
|
// 写入队列
|
|
// logData, _ := json.Marshal(data)
|
|
queue.Push(r.GetCtx(), consts.QueueSysOptLog, data)
|
|
}
|
|
|
|
// ResponseHandlerRaw 返回处理中间件
|
|
func (s *sMiddleware) ResponseHandlerRaw(r *ghttp.Request) {
|
|
r.Middleware.Next()
|
|
// 如果已经有返回内容,那么该中间件什么也不做
|
|
if r.Response.BufferLength() > 0 {
|
|
g.Log().Infof(r.Context(), "ResponseHandler r.Response.BufferLength() %d buf:%s", r.Response.BufferLength(), r.Response.BufferString())
|
|
return
|
|
}
|
|
|
|
var (
|
|
err = r.GetError()
|
|
res = r.GetHandlerResponse()
|
|
)
|
|
if err != nil {
|
|
r.Response.WriteJson(res)
|
|
r.ExitAll()
|
|
} else {
|
|
r.Response.WriteJson(res)
|
|
}
|
|
}
|