163 lines
5.2 KiB
Go
163 lines
5.2 KiB
Go
package services
|
||
|
||
import (
|
||
"context"
|
||
"crypto/rand"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"time"
|
||
|
||
"github.com/goravel/framework/facades"
|
||
|
||
apperrors "goravel/app/errors"
|
||
"goravel/app/models"
|
||
"goravel/app/utils/errorlog"
|
||
"goravel/app/utils/traceid"
|
||
)
|
||
|
||
type TokenService interface {
|
||
// CreateToken 创建token并存入数据库
|
||
CreateToken(tokenableType string, tokenableID uint, name string, expiresAt *time.Time, browser, ip, os, sessionID string) (string, *models.PersonalAccessToken, error)
|
||
// FindToken 根据token值查找token记录
|
||
FindToken(token string) (*models.PersonalAccessToken, error)
|
||
// DeleteToken 删除token
|
||
DeleteToken(token string) error
|
||
// DeleteTokensByUser 删除用户的所有token
|
||
DeleteTokensByUser(tokenableType string, tokenableID uint) error
|
||
// GetTokensByUser 获取用户的所有token
|
||
GetTokensByUser(tokenableType string, tokenableID uint) ([]models.PersonalAccessToken, error)
|
||
// UpdateLastUsedAt 更新最后使用时间
|
||
UpdateLastUsedAt(token string) error
|
||
}
|
||
|
||
type TokenServiceImpl struct {
|
||
}
|
||
|
||
func NewTokenServiceImpl() *TokenServiceImpl {
|
||
return &TokenServiceImpl{}
|
||
}
|
||
|
||
// CreateToken 创建token并存入数据库
|
||
func (s *TokenServiceImpl) CreateToken(tokenableType string, tokenableID uint, name string, expiresAt *time.Time, browser, ip, os, sessionID string) (string, *models.PersonalAccessToken, error) {
|
||
// 生成随机token(类似Laravel Sanctum)
|
||
plainToken := s.generateRandomToken()
|
||
tokenHash := s.hashToken(plainToken)
|
||
|
||
// 如果没有提供sessionID,使用tokenHash的前16位作为sessionID
|
||
if sessionID == "" {
|
||
if len(tokenHash) >= 16 {
|
||
sessionID = tokenHash[:16]
|
||
} else {
|
||
sessionID = tokenHash
|
||
}
|
||
}
|
||
|
||
// 创建token记录,立即设置last_used_at为当前时间
|
||
now := time.Now()
|
||
accessToken := &models.PersonalAccessToken{
|
||
TokenableType: tokenableType,
|
||
TokenableID: tokenableID,
|
||
Name: name,
|
||
Token: tokenHash,
|
||
ExpiresAt: expiresAt,
|
||
LastUsedAt: &now, // 登录时立即设置最后使用时间
|
||
Browser: browser,
|
||
IP: ip,
|
||
OS: os,
|
||
SessionID: sessionID,
|
||
}
|
||
|
||
if err := facades.Orm().Query().Create(accessToken); err != nil {
|
||
return "", nil, err
|
||
}
|
||
|
||
return plainToken, accessToken, nil
|
||
}
|
||
|
||
// FindToken 根据token值查找token记录
|
||
func (s *TokenServiceImpl) FindToken(token string) (*models.PersonalAccessToken, error) {
|
||
if token == "" {
|
||
return nil, apperrors.ErrInvalidArgument.WithMessage("token is empty")
|
||
}
|
||
|
||
tokenHash := s.hashToken(token)
|
||
var accessToken models.PersonalAccessToken
|
||
if err := facades.Orm().Query().Where("token", tokenHash).FirstOrFail(&accessToken); err != nil {
|
||
// 记录调试信息
|
||
// facades.Log().Debugf("TokenService: FindToken failed, hash: %s, error: %v", tokenHash[:min(20, len(tokenHash))], err)
|
||
return nil, err
|
||
}
|
||
|
||
// 检查是否过期
|
||
if accessToken.ExpiresAt != nil && accessToken.ExpiresAt.Before(time.Now()) {
|
||
if _, err := facades.Orm().Query().Delete(&accessToken); err != nil {
|
||
// 使用 traceid.EnsureContext 确保有 trace_id,即使使用 context.Background()
|
||
// 这样可以保证日志的可追踪性
|
||
ctx, _ := traceid.EnsureContext(context.Background())
|
||
errorlog.Record(ctx, "token", "Failed to delete expired token", map[string]any{
|
||
"token_id": accessToken.ID,
|
||
"tokenable_id": accessToken.TokenableID,
|
||
"expires_at": accessToken.ExpiresAt,
|
||
"error": err.Error(),
|
||
}, "Failed to delete expired token (ID: %d): %v", accessToken.ID, err)
|
||
}
|
||
|
||
return nil, apperrors.ErrInvalidArgument.WithMessage("token expired")
|
||
}
|
||
|
||
return &accessToken, nil
|
||
}
|
||
|
||
// DeleteToken 删除token
|
||
func (s *TokenServiceImpl) DeleteToken(token string) error {
|
||
tokenHash := s.hashToken(token)
|
||
_, err := facades.Orm().Query().Where("token", tokenHash).Delete(&models.PersonalAccessToken{})
|
||
return err
|
||
}
|
||
|
||
// DeleteTokensByUser 删除用户的所有token
|
||
func (s *TokenServiceImpl) DeleteTokensByUser(tokenableType string, tokenableID uint) error {
|
||
_, err := facades.Orm().Query().
|
||
Where("tokenable_type", tokenableType).
|
||
Where("tokenable_id", tokenableID).
|
||
Delete(&models.PersonalAccessToken{})
|
||
return err
|
||
}
|
||
|
||
// GetTokensByUser 获取用户的所有token
|
||
func (s *TokenServiceImpl) GetTokensByUser(tokenableType string, tokenableID uint) ([]models.PersonalAccessToken, error) {
|
||
var tokens []models.PersonalAccessToken
|
||
err := facades.Orm().Query().
|
||
Where("tokenable_type", tokenableType).
|
||
Where("tokenable_id", tokenableID).
|
||
Order("created_at desc").
|
||
Find(&tokens)
|
||
return tokens, err
|
||
}
|
||
|
||
// UpdateLastUsedAt 更新最后使用时间
|
||
func (s *TokenServiceImpl) UpdateLastUsedAt(token string) error {
|
||
tokenHash := s.hashToken(token)
|
||
now := time.Now()
|
||
_, err := facades.Orm().Query().
|
||
Model(&models.PersonalAccessToken{}).
|
||
Where("token", tokenHash).
|
||
Update("last_used_at", now)
|
||
return err
|
||
}
|
||
|
||
// generateRandomToken 生成随机token(40个字符)
|
||
func (s *TokenServiceImpl) generateRandomToken() string {
|
||
// 生成40个随机字节
|
||
b := make([]byte, 40)
|
||
_, _ = rand.Read(b)
|
||
// 转换为十六进制字符串(80个字符),然后取前40个字符
|
||
return hex.EncodeToString(b)[:40]
|
||
}
|
||
|
||
// hashToken 对token进行SHA256哈希
|
||
func (s *TokenServiceImpl) hashToken(token string) string {
|
||
hash := sha256.Sum256([]byte(token))
|
||
return hex.EncodeToString(hash[:])
|
||
}
|