Files
backend/internal/service/token_service.go
lan 4b4980820f
Some checks failed
SonarQube Analysis / sonarqube (push) Has been cancelled
Test / test (push) Has been cancelled
Test / lint (push) Has been cancelled
Test / build (push) Has been cancelled
chore: 初始化仓库,排除二进制文件和覆盖率文件
2025-11-28 23:30:49 +08:00

278 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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))
}