|
|
// Copyright (C) 2024 veypi <i@veypi.com>
|
|
|
// 2025-03-04 16:08:06
|
|
|
// Distributed under terms of the MIT license.
|
|
|
|
|
|
package auth
|
|
|
|
|
|
import (
|
|
|
"encoding/json"
|
|
|
"fmt"
|
|
|
"strings"
|
|
|
"time"
|
|
|
|
|
|
"github.com/veypi/vbase/cfg"
|
|
|
"github.com/veypi/vbase/libs/cache"
|
|
|
"github.com/veypi/vbase/libs/jwt"
|
|
|
"github.com/veypi/vbase/models"
|
|
|
"github.com/veypi/vigo"
|
|
|
)
|
|
|
|
|
|
// orgMemberCache 组织成员身份缓存结构
|
|
|
type orgMemberCache struct {
|
|
|
IsMember bool `json:"is_member"`
|
|
|
RoleCodes []string `json:"role_codes"`
|
|
|
}
|
|
|
|
|
|
// AuthMiddleware 统一认证中间件
|
|
|
// 1. JWT认证: 解析token,验证有效性,设置用户信息
|
|
|
// 2. 组织上下文: 如果请求包含org_id,验证用户成员身份,设置组织信息
|
|
|
// 使用Redis缓存组织成员身份和角色信息,减少数据库查询
|
|
|
func AuthMiddleware() func(*vigo.X) error {
|
|
|
return func(x *vigo.X) error {
|
|
|
// === 1. JWT 认证部分 ===
|
|
|
tokenString := extractToken(x)
|
|
|
if tokenString == "" {
|
|
|
return vigo.ErrUnauthorized.WithString("missing token")
|
|
|
}
|
|
|
|
|
|
// 解析token
|
|
|
claims, err := jwt.ParseToken(tokenString)
|
|
|
if err != nil {
|
|
|
if err == jwt.ErrExpiredToken {
|
|
|
return vigo.ErrTokenExpired
|
|
|
}
|
|
|
return vigo.ErrTokenInvalid
|
|
|
}
|
|
|
|
|
|
// 检查token是否在黑名单中
|
|
|
if cache.IsEnabled() {
|
|
|
blacklisted, _ := cache.IsTokenBlacklisted(claims.ID)
|
|
|
if blacklisted {
|
|
|
return vigo.ErrUnauthorized.WithString("token has been revoked")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 将用户信息存入上下文
|
|
|
x.Set("user_id", claims.UserID)
|
|
|
x.Set("user_name", claims.Username)
|
|
|
x.Set("user_orgs", claims.Orgs)
|
|
|
x.Set("token_claims", claims)
|
|
|
|
|
|
// === 2. 组织上下文部分 ===
|
|
|
orgID := x.Request.Header.Get("X-Org-ID")
|
|
|
if orgID == "" {
|
|
|
orgID = x.Request.URL.Query().Get("org_id")
|
|
|
}
|
|
|
|
|
|
if orgID == "" {
|
|
|
// 没有指定组织,仅完成用户认证
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// 尝试从缓存获取组织成员信息
|
|
|
var roleCodes []string
|
|
|
var isMember bool
|
|
|
|
|
|
if cache.IsEnabled() {
|
|
|
cacheKey := fmt.Sprintf("auth:org_member:%s:%s", claims.UserID, orgID)
|
|
|
cachedData, err := cache.Get(cacheKey)
|
|
|
if err == nil && cachedData != "" {
|
|
|
var cached orgMemberCache
|
|
|
if err := json.Unmarshal([]byte(cachedData), &cached); err == nil {
|
|
|
isMember = cached.IsMember
|
|
|
roleCodes = cached.RoleCodes
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 缓存未命中,查询数据库
|
|
|
if roleCodes == nil {
|
|
|
// 验证用户是否为组织成员
|
|
|
var member models.OrgMember
|
|
|
err := cfg.DB().Where("org_id = ? AND user_id = ? AND status = ?",
|
|
|
orgID, claims.UserID, models.MemberStatusActive).First(&member).Error
|
|
|
isMember = err == nil
|
|
|
|
|
|
if isMember {
|
|
|
// 查询用户的角色
|
|
|
cfg.DB().Model(&models.UserRole{}).
|
|
|
Joins("JOIN roles ON user_roles.role_id = roles.id").
|
|
|
Where("user_roles.user_id = ? AND user_roles.org_id = ?", claims.UserID, orgID).
|
|
|
Pluck("roles.code", &roleCodes)
|
|
|
}
|
|
|
|
|
|
// 写入缓存
|
|
|
if cache.IsEnabled() {
|
|
|
cacheData := orgMemberCache{
|
|
|
IsMember: isMember,
|
|
|
RoleCodes: roleCodes,
|
|
|
}
|
|
|
if data, err := json.Marshal(cacheData); err == nil {
|
|
|
cacheKey := fmt.Sprintf("auth:org_member:%s:%s", claims.UserID, orgID)
|
|
|
// 缓存5分钟
|
|
|
cache.Set(cacheKey, string(data), 5*time.Minute)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if !isMember {
|
|
|
return vigo.ErrForbidden.WithString("you are not a member of this organization")
|
|
|
}
|
|
|
|
|
|
x.Set("org_id", orgID)
|
|
|
x.Set("org_roles", roleCodes)
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func extractToken(x *vigo.X) string {
|
|
|
auth := x.Request.Header.Get("Authorization")
|
|
|
if auth != "" {
|
|
|
if len(auth) > 7 && strings.HasPrefix(auth, "Bearer ") {
|
|
|
return auth[7:]
|
|
|
}
|
|
|
}
|
|
|
return x.Request.URL.Query().Get("access_token")
|
|
|
}
|