package authx import ( "context" "errors" "fmt" "net/http" "strconv" "strings" "time" "github.com/golang-jwt/jwt/v4" "github.com/zeromicro/go-zero/rest/httpx" ) type ctxKey string const ( CtxTenantID ctxKey = "tenant_id" CtxUserID ctxKey = "user_id" CtxRole ctxKey = "role" CtxAgentID ctxKey = "agent_id" CtxPerms ctxKey = "perms" CtxOrgUnitID ctxKey = "org_unit_id" ) // 平台级角色 → 权限映射(角色、权限一律中文命名) var RolePermissions = map[string][]string{ Role超级管理员: { Perm管理租户, // 只管公司一级:列表/新建等;不介入公司内模块、成员、同步 }, Role管理员: { Perm读取模块, Perm写入模块, Perm发布模块, Perm新增数据, Perm查询数据, Perm更新数据, Perm删除数据, Perm导出数据, Perm导入数据, Perm查看审计, Perm上传文件, Perm下载文件, Perm管理智能体, Perm邀请成员, Perm管理组织, Perm数据同步, }, Role编辑: { Perm读取模块, Perm写入模块, Perm新增数据, Perm查询数据, Perm更新数据, Perm导出数据, Perm导入数据, Perm上传文件, Perm下载文件, }, Role只读: { Perm读取模块, Perm查询数据, Perm导出数据, Perm下载文件, Perm查看审计, }, Role待加入: {}, } type Claims struct { TenantID int64 `json:"tenant_id"` UserID int64 `json:"user_id"` Role string `json:"role,omitempty"` OrgUnitID int64 `json:"org_unit_id,omitempty"` AgentID int64 `json:"agent_id,omitempty"` Perms []string `json:"perms,omitempty"` jwt.RegisteredClaims } type JWTConfig struct { AccessSecret string AccessExpire int64 } func (c JWTConfig) Expire() time.Duration { if c.AccessExpire <= 0 { return 24 * time.Hour } return time.Duration(c.AccessExpire) * time.Second } func IssueToken(cfg JWTConfig, tenantID, userID int64, role string, orgUnitID int64) (string, int64, error) { if cfg.AccessSecret == "" { return "", 0, errors.New("auth access secret empty") } if userID <= 0 { return "", 0, errors.New("user_id required") } if role == "" { role = Role管理员 } role = NormalizeRole(role) if _, ok := RolePermissions[role]; !ok { return "", 0, fmt.Errorf("unknown role: %s", role) } // 待加入 / 超级管理员可无租户;其他角色必须带 tenant_id if tenantID <= 0 && role != Role待加入 && role != Role超级管理员 { return "", 0, errors.New("tenant_id and user_id required") } if tenantID < 0 { tenantID = 0 } return signClaims(cfg, Claims{ TenantID: tenantID, UserID: userID, Role: role, OrgUnitID: orgUnitID, }) } // IssueAgentToken 签发智能体 JWT(role=agent,权限写在 perms 声明中)。 func IssueAgentToken(cfg JWTConfig, tenantID, agentID int64, perms []string) (string, int64, error) { if cfg.AccessSecret == "" { return "", 0, errors.New("auth access secret empty") } if tenantID <= 0 || agentID <= 0 { return "", 0, errors.New("tenant_id and agent_id required") } return signClaims(cfg, Claims{ TenantID: tenantID, UserID: agentID, Role: RoleAgent, AgentID: agentID, Perms: NormalizePerms(perms), }) } func signClaims(cfg JWTConfig, claims Claims) (string, int64, error) { exp := time.Now().Add(cfg.Expire()) claims.RegisteredClaims = jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(exp), IssuedAt: jwt.NewNumericDate(time.Now()), Issuer: "aijianzhan-platform", } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signed, err := token.SignedString([]byte(cfg.AccessSecret)) if err != nil { return "", 0, err } return signed, exp.Unix(), nil } func ParseToken(cfg JWTConfig, tokenStr string) (*Claims, error) { if cfg.AccessSecret == "" { return nil, errors.New("auth access secret empty") } parser := jwt.NewParser(jwt.WithValidMethods([]string{"HS256"})) token, err := parser.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (any, error) { return []byte(cfg.AccessSecret), nil }) if err != nil { return nil, err } claims, ok := token.Claims.(*Claims) if !ok || !token.Valid { return nil, errors.New("invalid token") } if claims.UserID <= 0 { return nil, errors.New("token missing user") } if claims.Role == "" { claims.Role = Role只读 } claims.Role = NormalizeRole(claims.Role) if claims.TenantID <= 0 && claims.Role != Role待加入 && claims.Role != Role超级管理员 { return nil, errors.New("token missing tenant/user") } if claims.TenantID < 0 { claims.TenantID = 0 } if claims.Role == Role智能体 && claims.AgentID <= 0 { claims.AgentID = claims.UserID } return claims, nil } func Middleware(jwtCfg JWTConfig, devAuth bool) func(http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if strings.HasPrefix(strings.ToLower(auth), "bearer ") { raw := strings.TrimSpace(auth[7:]) claims, err := ParseToken(jwtCfg, raw) if err != nil { WriteError(w, http.StatusUnauthorized, "invalid token: "+err.Error()) return } ctx := WithFullClaims(r.Context(), claims.TenantID, claims.UserID, claims.Role, claims.AgentID, claims.OrgUnitID, claims.Perms) next(w, r.WithContext(ctx)) return } if devAuth { tenantID, _ := strconv.ParseInt(r.Header.Get("X-Tenant-Id"), 10, 64) userID, _ := strconv.ParseInt(r.Header.Get("X-User-Id"), 10, 64) role := r.Header.Get("X-Role") if tenantID <= 0 { tenantID = 1 } if userID <= 0 { userID = 1 } if role == "" { role = Role管理员 } role = NormalizeRole(role) ctx := WithClaims(r.Context(), tenantID, userID, role) next(w, r.WithContext(ctx)) return } WriteError(w, http.StatusUnauthorized, "missing Authorization Bearer token") } } } // RequirePermission 校验平台角色或智能体 JWT 内嵌权限。 func RequirePermission(perm string) func(http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !HasPermissionCtx(r.Context(), perm) { role := Role(r.Context()) WriteError(w, http.StatusForbidden, "permission denied: "+perm+" (role="+role+")") return } next(w, r) } } } // RequireSyncPushAccess 放行「数据同步」权限,或已登录人类成员(用户自助 sync)。 // 智能体无「数据同步」仍 403;无 sync 权的人类由 handler 再按 Binding 收窄范围。 func RequireSyncPushAccess() func(http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if HasPermissionCtx(r.Context(), Perm数据同步) { next(w, r) return } if IsAgent(Role(r.Context())) { WriteError(w, http.StatusForbidden, "permission denied: 数据同步 (role=智能体)") return } if UserID(r.Context()) <= 0 { WriteError(w, http.StatusForbidden, "permission denied: need user login or 数据同步") return } next(w, r) } } } // SyncTenantWide 是否租户级同步权(管理员/智能体带「数据同步」);否则须按 Binding 自助范围。 func SyncTenantWide(ctx context.Context) bool { return HasPermissionCtx(ctx, Perm数据同步) } // RequireTenant 要求已加入租户;pending / 无 tenant 不可访问业务数据。 // 超级管理员可在「进入某公司」后携带 tenant_id 访问公司内接口(可看可改,前端会多重确认)。 func RequireTenant() func(http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if Role(r.Context()) == Role智能体 { if TenantID(r.Context()) <= 0 { WriteError(w, http.StatusForbidden, "agent missing tenant") return } next(w, r) return } if Role(r.Context()) == Role超级管理员 { if TenantID(r.Context()) <= 0 { WriteError(w, http.StatusForbidden, "请先在平台管理中进入某公司后再操作公司内部功能") return } next(w, r) return } if TenantID(r.Context()) <= 0 || Role(r.Context()) == Role待加入 { WriteError(w, http.StatusForbidden, "join a tenant first (pending membership)") return } next(w, r) } } } // RequirePlatformAdmin 仅平台超级管理员。 func RequirePlatformAdmin() func(http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !IsPlatformAdmin(Role(r.Context())) { WriteError(w, http.StatusForbidden, "需要超级管理员") return } next(w, r) } } } // RequireTenantBound 保留兼容:业务接口要求已绑定租户。 func RequireTenantBound() func(http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if IsPlatformAdmin(Role(r.Context())) { if TenantID(r.Context()) <= 0 { WriteError(w, http.StatusForbidden, "请先进入某公司") return } next(w, r) return } if TenantID(r.Context()) <= 0 { WriteError(w, http.StatusForbidden, "请先加入公司") return } next(w, r) } } } func HasPermission(role, perm string) bool { perms, ok := RolePermissions[NormalizeRole(role)] if !ok { return false } want := NormalizePerm(perm) for _, p := range perms { if NormalizePerm(p) == want { return true } } return false } func HasPermissionCtx(ctx context.Context, perm string) bool { role := NormalizeRole(Role(ctx)) want := NormalizePerm(perm) if role == Role超级管理员 { // 进入公司后:超管可使用全部公司权限(平台专属「管理租户」仍保留) if want == Perm管理租户 { return true } for _, p := range CompanyPermCatalog() { if NormalizePerm(p) == want { return TenantID(ctx) > 0 } } return HasPermission(role, want) } if role == Role智能体 { ok := false for _, p := range Perms(ctx) { if NormalizePerm(p) == want { ok = true break } } if !ok { return false } } else if !HasPermission(role, want) { return false } tid := TenantID(ctx) if tid > 0 && EntitlementChecker != nil { if !EntitlementChecker(ctx, tid, want) { return false } } return true } func withClaims(ctx context.Context, tenantID, userID int64, role string) context.Context { return WithClaims(ctx, tenantID, userID, role) } // WithClaims 写入租户/用户/角色(公开展示等场景用)。 func WithClaims(ctx context.Context, tenantID, userID int64, role string) context.Context { return WithFullClaims(ctx, tenantID, userID, role, 0, 0, nil) } func WithFullClaims(ctx context.Context, tenantID, userID int64, role string, agentID, orgUnitID int64, perms []string) context.Context { ctx = context.WithValue(ctx, CtxTenantID, tenantID) ctx = context.WithValue(ctx, CtxUserID, userID) ctx = context.WithValue(ctx, CtxRole, role) ctx = context.WithValue(ctx, CtxAgentID, agentID) ctx = context.WithValue(ctx, CtxOrgUnitID, orgUnitID) if perms != nil { ctx = context.WithValue(ctx, CtxPerms, append([]string{}, perms...)) } return ctx } func TenantID(ctx context.Context) int64 { v, _ := ctx.Value(CtxTenantID).(int64) return v } func UserID(ctx context.Context) int64 { v, _ := ctx.Value(CtxUserID).(int64) return v } func AgentID(ctx context.Context) int64 { v, _ := ctx.Value(CtxAgentID).(int64) return v } func OrgUnitID(ctx context.Context) int64 { v, _ := ctx.Value(CtxOrgUnitID).(int64) return v } func Role(ctx context.Context) string { v, _ := ctx.Value(CtxRole).(string) if v == "" { return Role只读 } return NormalizeRole(v) } func Perms(ctx context.Context) []string { v, _ := ctx.Value(CtxPerms).([]string) return v } func WriteError(w http.ResponseWriter, code int, msg string) { httpx.WriteJson(w, code, map[string]any{ "code": code, "message": msg, }) } // WriteRetryableError 供 agent 识别可稍后重试(不换 UUID)。 func WriteRetryableError(w http.ResponseWriter, code int, msg string) { httpx.WriteJson(w, code, map[string]any{ "code": code, "message": msg, "retryable": true, }) }