package middleware import ( "context" "database/sql" "errors" "net/http" "slices" "strconv" "strings" "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/os/gtime" "github.com/gogf/gf/v2/util/gmeta" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/contexts" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model/entity" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/packed/response" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/service" ) type sMiddleware struct{} const apiDataScopeRoleId int64 = 17 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 || contexts.IsWebSocket(r.GetCtx()) { return } if getContentType(r) == consts.HTTPContentTypeExcel { r.Response.Status = http.StatusOK return } 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()) return } response.Json(r, code.Code(), "", res) } func (s *sMiddleware) ApiCtx(r *ghttp.Request) { ctx := r.GetCtx() r.SetCtx(r.GetNeverDoneCtx()) if r.URL != nil && s.IsExceptLogin(ctx, r.URL.Path) { r.Middleware.Next() return } data, err := service.QueryToken().GetCurrentCustomer(ctx) if err != nil { g.Log().Errorf(ctx, "sMiddleware.ApiCtx current customer error: %v", err) r.Middleware.Next() return } if data != nil { contextModel := new(model.Context) contextModel.User = &model.Identity{ BaseClaims: model.BaseClaims{ ID: 0, TenantId: data.BaseClaimsApi.TenantId, DeptId: data.BaseClaimsApi.DeptId, Roles: []model.Role{ {RoleId: apiDataScopeRoleId, DataScope: consts.SysRoleDataScopeDeptAndBelow}, }, LoginAt: data.BaseClaimsApi.LoginAt, }, } contextModel.AgentUser = &data.BaseClaimsApi contexts.Init(r, contextModel) } r.Middleware.Next() } func (s *sMiddleware) ApiAuth(r *ghttp.Request) { ctx := r.Context() path := r.URL.Path if s.IsExceptLogin(ctx, path) { r.Middleware.Next() return } deptKey := contexts.GetDeptKey(ctx) if deptKey == "" { g.Log().Error(ctx, "sMiddleware.ApiAuth deptKey is empty", "path", path) response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message()) return } appId := r.Get("appId").String() if appId == "" { g.Log().Error(ctx, "sMiddleware.ApiAuth appId is empty", "path", path) response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message()) return } if deptKey != appId { g.Log().Error(ctx, "sMiddleware.ApiAuth deptKey not match appId", "path", path) response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message()) return } if contexts.GetAgentDeptId(ctx) == 0 { g.Log().Error(ctx, "sMiddleware.ApiAuth deptId is 0", "path", path) response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message()) return } allowed, err := s.hasOpenApiPermission(ctx, r.Method, path) if err != nil { g.Log().Errorf(ctx, "sMiddleware.ApiAuth permission check failed, method: %s, path: %s, err: %v", r.Method, path, err) response.JsonExit(r, consts.CodePermissionCheckFailed.Code(), consts.CodePermissionCheckFailed.Message()) return } if !allowed { response.JsonExit(r, consts.CodeNoPermission.Code(), consts.CodeNoPermission.Message()) return } r.Middleware.Next() } func openApiMethodValues(method string) []string { method = strings.ToUpper(strings.TrimSpace(method)) values := []string{method} switch method { case "GET": values = append(values, "4") case "POST": values = append(values, "1") case "PUT": values = append(values, "2") case "DELETE": values = append(values, "3") } return values } // hasOpenApiPermission 判定当前 API 账号对指定路由是否已授权。 // 返回值 error 仅代表权限数据查询本身的系统故障,与「无权限」(false, nil) 严格区分: // 调用方对 error 返回系统异常码,对 false 返回无权限码,避免把数据库故障误报成授权问题。 // 未登记资源、资源停用、账号不可用、未绑定权限组均按无权限处理。 func (s *sMiddleware) hasOpenApiPermission(ctx context.Context, method, path string) (bool, error) { agentUser := contexts.GetAgentUser(ctx) if agentUser == nil { return false, nil } path = strings.TrimSpace(path) if path != "/" { path = strings.TrimRight(path, "/") } // 每条受保护路由(含 /common 兼容前缀的别名路由)都必须显式登记资源并完成授权, // 资源查询失败不得降级为放行。 var apiResource entity.TApiResource err := dao.TApiResource.Ctx(ctx). WhereIn(dao.TApiResource.Columns().Method, openApiMethodValues(method)). Where(dao.TApiResource.Columns().Path, path). Scan(&apiResource) if err != nil && !errors.Is(err, sql.ErrNoRows) { return false, gerror.Wrapf(err, "query t_api_resource failed, method: %s, path: %s", method, path) } if err != nil || apiResource.Id == 0 { g.Log().Warningf(ctx, "sMiddleware.hasOpenApiPermission resource not configured, method: %s, path: %s", method, path) return false, nil } if apiResource.Status == 1 { return false, nil } var tApi entity.TApi apiMod := dao.TApi.Ctx(ctx) if agentUser.ApiId > 0 { apiMod = apiMod.Where(dao.TApi.Columns().Id, agentUser.ApiId) } else if agentUser.AppKey != "" { apiMod = apiMod.Where(dao.TApi.Columns().AppKey, agentUser.AppKey) } else { g.Log().Warningf(ctx, "sMiddleware.hasOpenApiPermission api identity is empty, resourceApiId: %d, path: %s", apiResource.Id, path) return false, nil } if err := apiMod.Scan(&tApi); err != nil { if errors.Is(err, sql.ErrNoRows) { g.Log().Warningf(ctx, "sMiddleware.hasOpenApiPermission t_api not found, resourceApiId: %d, path: %s", apiResource.Id, path) return false, nil } return false, gerror.Wrapf(err, "query t_api failed, resourceApiId: %d, path: %s", apiResource.Id, path) } if tApi.Id == 0 || tApi.Status == 1 { g.Log().Warningf(ctx, "sMiddleware.hasOpenApiPermission t_api unavailable, tApiId: %d, status: %d, resourceApiId: %d, path: %s", tApi.Id, tApi.Status, apiResource.Id, path) return false, nil } if tApi.GroupId <= 0 { g.Log().Warningf(ctx, "sMiddleware.hasOpenApiPermission groupId is empty, tApiId: %d, appKey: %s, resourceApiId: %d, path: %s", tApi.Id, tApi.AppKey, apiResource.Id, path) return false, nil } count, err := dao.TApiGroupResource.Ctx(ctx). Where(dao.TApiGroupResource.Columns().GroupId, tApi.GroupId). Where(dao.TApiGroupResource.Columns().ApiId, apiResource.Id). Count() if err != nil { return false, gerror.Wrapf(err, "query t_api_group_resource failed, groupId: %d, resourceApiId: %d, path: %s", tApi.GroupId, apiResource.Id, path) } return count > 0, nil } func (m *sMiddleware) OldResponseHandler(r *ghttp.Request) { r.Middleware.Next() if r.Response.BufferLength() > 0 || contexts.IsWebSocket(r.GetCtx()) { return } if getContentType(r) == consts.HTTPContentTypeExcel { r.Response.Status = http.StatusOK return } 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.OldJson(r, code.Code(), err.Error()) return } response.OldJson(r, code.Code(), "", res) } func (s *sMiddleware) OldCtx(r *ghttp.Request) { ctx := r.GetCtx() r.SetCtx(r.GetNeverDoneCtx()) verifyVar := r.Get("verify") var verify model.VerifyItem if err := verifyVar.Scan(&verify); err != nil { g.Log().Errorf(ctx, "sMiddleware.OldCtx verify scan error: %v", err) response.JsonExit(r, gcode.CodeNotAuthorized.Code(), "request param invalid") return } var tApi entity.TApi err := dao.TApi.Ctx(ctx).Where(g.Map{ dao.TApi.Columns().AppKey: verify.AppId, }).Scan(&tApi) if err != nil { r.Middleware.Next() return } if tApi.Id != 0 { contextModel := new(model.Context) contextModel.User = &model.Identity{ BaseClaims: model.BaseClaims{ ID: 0, TenantId: tApi.TenantId, DeptId: tApi.DeptId, Roles: []model.Role{ {RoleId: apiDataScopeRoleId, DataScope: consts.SysRoleDataScopeDeptAndBelow}, }, LoginAt: gtime.Now().TimestampMilli(), }, } contextModel.AgentUser = &model.BaseClaimsApi{ ApiId: tApi.Id, AppKey: tApi.AppKey, GroupId: tApi.GroupId, DeptId: tApi.DeptId, TenantId: tApi.TenantId, LoginAt: gtime.Now().TimestampMilli(), } contexts.Init(r, contextModel) } r.Middleware.Next() } func (s *sMiddleware) OldAuth(r *ghttp.Request) { ctx := r.Context() path := r.URL.Path if s.IsExceptLogin(ctx, path) { r.Middleware.Next() return } verifyVar := r.Get("verify") var verify model.VerifyItem if err := verifyVar.Scan(&verify); err != nil { g.Log().Errorf(ctx, "sMiddleware.OldAuth verify scan error: %v", err) response.JsonExit(r, gcode.CodeNotAuthorized.Code(), "request param invalid") return } if verify.TimeStamp == "" { g.Log().Error(ctx, "sMiddleware.OldAuth timestamp is empty", "path", path) response.JsonExit(r, gcode.CodeNotAuthorized.Code(), "request param invalid") return } givenTime, err := strconv.ParseInt(verify.TimeStamp, 10, 64) if err != nil { g.Log().Errorf(ctx, "sMiddleware.OldAuth timestamp parse error: %v", err) response.JsonExit(r, gcode.CodeNotAuthorized.Code(), "request param invalid") return } if givenTime > gtime.Now().TimestampMilli()+3*60*1000 { g.Log().Error(ctx, "sMiddleware.OldAuth timestamp expired", "path", path) response.JsonExit(r, gcode.CodeNotAuthorized.Code(), "request timeout") return } if verify.AppId == "" { g.Log().Error(ctx, "sMiddleware.OldAuth appId is empty", "path", path) response.JsonExit(r, gcode.CodeNotAuthorized.Code(), "request param invalid") return } var tApi entity.TApi err = dao.TApi.Ctx(ctx).Where(g.Map{ dao.TApi.Columns().AppKey: verify.AppId, }).Scan(&tApi) if err != nil || tApi.Id == 0 || verify.Password != tApi.AppSecret { g.Log().Errorf(ctx, "sMiddleware.OldAuth auth failed: %v", err) response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeUserNotFound.Message()) return } r.Middleware.Next() } func (s *sMiddleware) IsExceptLogin(ctx context.Context, path string) bool { pathList := g.Cfg().MustGet(ctx, "router.exceptLogin").Strings() return slices.Contains(pathList, path) }