397 lines
11 KiB
Go
397 lines
11 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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,
|
||
})
|
||
}
|