278 lines
7.5 KiB
Go
278 lines
7.5 KiB
Go
|
|
package service
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"carrotskin/internal/model"
|
|||
|
|
"carrotskin/internal/repository"
|
|||
|
|
"context"
|
|||
|
|
"errors"
|
|||
|
|
"fmt"
|
|||
|
|
"github.com/google/uuid"
|
|||
|
|
"github.com/jackc/pgx/v5"
|
|||
|
|
"go.uber.org/zap"
|
|||
|
|
"strconv"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"gorm.io/gorm"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// 常量定义
|
|||
|
|
const (
|
|||
|
|
ExtendedTimeout = 10 * time.Second
|
|||
|
|
TokensMaxCount = 10 // 用户最多保留的token数量
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// NewToken 创建新令牌
|
|||
|
|
func NewToken(db *gorm.DB, logger *zap.Logger, userId int64, UUID string, clientToken string) (*model.Profile, []*model.Profile, string, string, error) {
|
|||
|
|
var (
|
|||
|
|
selectedProfileID *model.Profile
|
|||
|
|
availableProfiles []*model.Profile
|
|||
|
|
)
|
|||
|
|
// 设置超时上下文
|
|||
|
|
_, cancel := context.WithTimeout(context.Background(), DefaultTimeout)
|
|||
|
|
defer cancel()
|
|||
|
|
|
|||
|
|
// 验证用户存在
|
|||
|
|
_, err := repository.FindProfileByUUID(UUID)
|
|||
|
|
if err != nil {
|
|||
|
|
return selectedProfileID, availableProfiles, "", "", fmt.Errorf("获取用户信息失败: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成令牌
|
|||
|
|
if clientToken == "" {
|
|||
|
|
clientToken = uuid.New().String()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
accessToken := uuid.New().String()
|
|||
|
|
token := model.Token{
|
|||
|
|
AccessToken: accessToken,
|
|||
|
|
ClientToken: clientToken,
|
|||
|
|
UserID: userId,
|
|||
|
|
Usable: true,
|
|||
|
|
IssueDate: time.Now(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取用户配置文件
|
|||
|
|
profiles, err := repository.FindProfilesByUserID(userId)
|
|||
|
|
if err != nil {
|
|||
|
|
return selectedProfileID, availableProfiles, "", "", fmt.Errorf("获取用户配置文件失败: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果用户只有一个配置文件,自动选择
|
|||
|
|
if len(profiles) == 1 {
|
|||
|
|
selectedProfileID = profiles[0]
|
|||
|
|
token.ProfileId = selectedProfileID.UUID
|
|||
|
|
}
|
|||
|
|
availableProfiles = profiles
|
|||
|
|
|
|||
|
|
// 插入令牌到tokens集合
|
|||
|
|
_, insertCancel := context.WithTimeout(context.Background(), DefaultTimeout)
|
|||
|
|
defer insertCancel()
|
|||
|
|
|
|||
|
|
err = repository.CreateToken(&token)
|
|||
|
|
if err != nil {
|
|||
|
|
return selectedProfileID, availableProfiles, "", "", fmt.Errorf("创建Token失败: %w", err)
|
|||
|
|
}
|
|||
|
|
// 清理多余的令牌
|
|||
|
|
go CheckAndCleanupExcessTokens(db, logger, userId)
|
|||
|
|
|
|||
|
|
return selectedProfileID, availableProfiles, accessToken, clientToken, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// CheckAndCleanupExcessTokens 检查并清理用户多余的令牌,只保留最新的10个
|
|||
|
|
func CheckAndCleanupExcessTokens(db *gorm.DB, logger *zap.Logger, userId int64) {
|
|||
|
|
if userId == 0 {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
// 获取用户所有令牌,按发行日期降序排序
|
|||
|
|
tokens, err := repository.GetTokensByUserId(userId)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Error("[ERROR] 获取用户Token失败: ", zap.Error(err), zap.String("userId", strconv.FormatInt(userId, 10)))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果令牌数量不超过上限,无需清理
|
|||
|
|
if len(tokens) <= TokensMaxCount {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取需要删除的令牌ID列表
|
|||
|
|
tokensToDelete := make([]string, 0, len(tokens)-TokensMaxCount)
|
|||
|
|
for i := TokensMaxCount; i < len(tokens); i++ {
|
|||
|
|
tokensToDelete = append(tokensToDelete, tokens[i].AccessToken)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 执行批量删除,传入上下文和待删除的令牌列表(作为切片参数)
|
|||
|
|
DeletedCount, err := repository.BatchDeleteTokens(tokensToDelete)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Error("[ERROR] 清理用户多余Token失败: ", zap.Error(err), zap.String("userId", strconv.FormatInt(userId, 10)))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if DeletedCount > 0 {
|
|||
|
|
logger.Info("[INFO] 成功清理用户多余Token", zap.Any("userId:", userId), zap.Any("count:", DeletedCount))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ValidToken 验证令牌有效性
|
|||
|
|
func ValidToken(db *gorm.DB, accessToken string, clientToken string) bool {
|
|||
|
|
if accessToken == "" {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用投影只获取需要的字段
|
|||
|
|
var token *model.Token
|
|||
|
|
token, err := repository.FindTokenByID(accessToken)
|
|||
|
|
|
|||
|
|
if err != nil {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if !token.Usable {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果客户端令牌为空,只验证访问令牌
|
|||
|
|
if clientToken == "" {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 否则验证客户端令牌是否匹配
|
|||
|
|
return token.ClientToken == clientToken
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func GetUUIDByAccessToken(db *gorm.DB, accessToken string) (string, error) {
|
|||
|
|
return repository.GetUUIDByAccessToken(accessToken)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func GetUserIDByAccessToken(db *gorm.DB, accessToken string) (int64, error) {
|
|||
|
|
return repository.GetUserIDByAccessToken(accessToken)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// RefreshToken 刷新令牌
|
|||
|
|
func RefreshToken(db *gorm.DB, logger *zap.Logger, accessToken, clientToken string, selectedProfileID string) (string, string, error) {
|
|||
|
|
if accessToken == "" {
|
|||
|
|
return "", "", errors.New("accessToken不能为空")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查找旧令牌
|
|||
|
|
oldToken, err := repository.GetTokenByAccessToken(accessToken)
|
|||
|
|
if err != nil {
|
|||
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|||
|
|
return "", "", errors.New("accessToken无效")
|
|||
|
|
}
|
|||
|
|
logger.Error("[ERROR] 查询Token失败: ", zap.Error(err), zap.Any("accessToken:", accessToken))
|
|||
|
|
return "", "", fmt.Errorf("查询令牌失败: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证profile
|
|||
|
|
if selectedProfileID != "" {
|
|||
|
|
valid, validErr := ValidateProfileByUserID(db, oldToken.UserID, selectedProfileID)
|
|||
|
|
if validErr != nil {
|
|||
|
|
logger.Error(
|
|||
|
|
"验证Profile失败",
|
|||
|
|
zap.Error(err),
|
|||
|
|
zap.Any("userId", oldToken.UserID),
|
|||
|
|
zap.String("profileId", selectedProfileID),
|
|||
|
|
)
|
|||
|
|
return "", "", fmt.Errorf("验证角色失败: %w", err)
|
|||
|
|
}
|
|||
|
|
if !valid {
|
|||
|
|
return "", "", errors.New("角色与用户不匹配")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查 clientToken 是否有效
|
|||
|
|
if clientToken != "" && clientToken != oldToken.ClientToken {
|
|||
|
|
return "", "", errors.New("clientToken无效")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查 selectedProfileID 的逻辑
|
|||
|
|
if selectedProfileID != "" {
|
|||
|
|
if oldToken.ProfileId != "" && oldToken.ProfileId != selectedProfileID {
|
|||
|
|
return "", "", errors.New("原令牌已绑定角色,无法选择新角色")
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
selectedProfileID = oldToken.ProfileId // 如果未指定,则保持原角色
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成新令牌
|
|||
|
|
newAccessToken := uuid.New().String()
|
|||
|
|
newToken := model.Token{
|
|||
|
|
AccessToken: newAccessToken,
|
|||
|
|
ClientToken: oldToken.ClientToken, // 新令牌的 clientToken 与原令牌相同
|
|||
|
|
UserID: oldToken.UserID,
|
|||
|
|
Usable: true,
|
|||
|
|
ProfileId: selectedProfileID, // 绑定到指定角色或保持原角色
|
|||
|
|
IssueDate: time.Now(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用双重写入模式替代事务,先插入新令牌,再删除旧令牌
|
|||
|
|
|
|||
|
|
err = repository.CreateToken(&newToken)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Error(
|
|||
|
|
"创建新Token失败",
|
|||
|
|
zap.Error(err),
|
|||
|
|
zap.String("accessToken", accessToken),
|
|||
|
|
)
|
|||
|
|
return "", "", fmt.Errorf("创建新Token失败: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
err = repository.DeleteTokenByAccessToken(accessToken)
|
|||
|
|
if err != nil {
|
|||
|
|
// 删除旧令牌失败,记录日志但不阻止操作,因为新令牌已成功创建
|
|||
|
|
logger.Warn(
|
|||
|
|
"删除旧Token失败,但新Token已创建",
|
|||
|
|
zap.Error(err),
|
|||
|
|
zap.String("oldToken", oldToken.AccessToken),
|
|||
|
|
zap.String("newToken", newAccessToken),
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.Info(
|
|||
|
|
"成功刷新Token",
|
|||
|
|
zap.Any("userId", oldToken.UserID),
|
|||
|
|
zap.String("accessToken", newAccessToken),
|
|||
|
|
)
|
|||
|
|
return newAccessToken, oldToken.ClientToken, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// InvalidToken 使令牌失效
|
|||
|
|
func InvalidToken(db *gorm.DB, logger *zap.Logger, accessToken string) {
|
|||
|
|
if accessToken == "" {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
err := repository.DeleteTokenByAccessToken(accessToken)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Error(
|
|||
|
|
"删除Token失败",
|
|||
|
|
zap.Error(err),
|
|||
|
|
zap.String("accessToken", accessToken),
|
|||
|
|
)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
logger.Info("[INFO] 成功删除", zap.Any("Token:", accessToken))
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// InvalidUserTokens 使用户所有令牌失效
|
|||
|
|
func InvalidUserTokens(db *gorm.DB, logger *zap.Logger, userId int64) {
|
|||
|
|
if userId == 0 {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
err := repository.DeleteTokenByUserId(userId)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Error(
|
|||
|
|
"[ERROR]删除用户Token失败",
|
|||
|
|
zap.Error(err),
|
|||
|
|
zap.Any("userId", userId),
|
|||
|
|
)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.Info("[INFO] 成功删除用户Token", zap.Any("userId:", userId))
|
|||
|
|
|
|||
|
|
}
|