feat: 实现用户认证系统

 新增功能:
1. 用户认证数据库模型
   - AuthToken (认证令牌)
   - LoginAttempt (登录尝试记录)
   - PasswordReset (密码重置)
   - Session (用户会话)

2. 认证服务 (AuthService)
   - 用户登录/注册
   - 令牌刷新
   - 密码重置
   - 会话管理

3. JWT管理器
   - 访问令牌生成/验证
   - 刷新令牌管理
   - 密码重置令牌
   - API令牌支持

🔒 安全特性:
- bcrypt密码加密
- JWT令牌验证
- 登录尝试记录
- 会话管理
- 令牌撤销机制

📝 技术实现:
- 使用GORM进行数据库操作
- JWT v5进行令牌管理
- 完整的错误处理
- 详细的日志记录

作者:小弟 (大哥的AI助手)
分支:feature/user-authentication
This commit is contained in:
Ubuntu
2026-02-27 17:11:15 +08:00
parent fc7138786b
commit 89623306c3
4 changed files with 1043 additions and 0 deletions

View File

@@ -0,0 +1,249 @@
package jwt
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
// Claims JWT声明
type Claims struct {
UserID uint `json:"user_id"`
TenantID uint `json:"tenant_id"`
Role string `json:"role"`
jwt.RegisteredClaims
}
// JWTManager JWT管理器
type JWTManager struct {
secretKey []byte
}
// NewJWTManager 创建JWT管理器
func NewJWTManager(secretKey string) *JWTManager {
return &JWTManager{
secretKey: []byte(secretKey),
}
}
// GenerateAccessToken 生成访问令牌
func (m *JWTManager) GenerateAccessToken(userID, tenantID uint, role string) (string, time.Time, error) {
expiresAt := time.Now().Add(24 * time.Hour) // 24小时有效
claims := &Claims{
UserID: userID,
TenantID: tenantID,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "smart-customer-service",
Subject: "access_token",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(m.secretKey)
if err != nil {
return "", time.Time{}, err
}
return tokenString, expiresAt, nil
}
// GenerateRefreshToken 生成刷新令牌
func (m *JWTManager) GenerateRefreshToken(userID, tenantID uint) (string, time.Time, error) {
expiresAt := time.Now().Add(7 * 24 * time.Hour) // 7天有效
claims := &Claims{
UserID: userID,
TenantID: tenantID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "smart-customer-service",
Subject: "refresh_token",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(m.secretKey)
if err != nil {
return "", time.Time{}, err
}
return tokenString, expiresAt, nil
}
// ValidateToken 验证令牌
func (m *JWTManager) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
// 验证签名方法
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("无效的签名方法")
}
return m.secretKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("无效的令牌")
}
// ParseToken 解析令牌(不验证过期)
func (m *JWTManager) ParseToken(tokenString string) (*Claims, error) {
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
token, _, err := parser.ParseUnverified(tokenString, &Claims{})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok {
return claims, nil
}
return nil, errors.New("无法解析令牌声明")
}
// GetTokenExpiration 获取令牌过期时间
func (m *JWTManager) GetTokenExpiration(tokenString string) (time.Time, error) {
claims, err := m.ParseToken(tokenString)
if err != nil {
return time.Time{}, err
}
return claims.ExpiresAt.Time, nil
}
// IsTokenExpired 检查令牌是否过期
func (m *JWTManager) IsTokenExpired(tokenString string) (bool, error) {
expiresAt, err := m.GetTokenExpiration(tokenString)
if err != nil {
return true, err
}
return time.Now().After(expiresAt), nil
}
// GeneratePasswordResetToken 生成密码重置令牌
func (m *JWTManager) GeneratePasswordResetToken(userID, tenantID uint) (string, time.Time, error) {
expiresAt := time.Now().Add(1 * time.Hour) // 1小时有效
claims := &Claims{
UserID: userID,
TenantID: tenantID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "smart-customer-service",
Subject: "password_reset",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(m.secretKey)
if err != nil {
return "", time.Time{}, err
}
return tokenString, expiresAt, nil
}
// GenerateEmailVerificationToken 生成邮箱验证令牌
func (m *JWTManager) GenerateEmailVerificationToken(userID, tenantID uint) (string, time.Time, error) {
expiresAt := time.Now().Add(24 * time.Hour) // 24小时有效
claims := &Claims{
UserID: userID,
TenantID: tenantID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "smart-customer-service",
Subject: "email_verification",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(m.secretKey)
if err != nil {
return "", time.Time{}, err
}
return tokenString, expiresAt, nil
}
// GenerateAPIToken 生成API令牌
func (m *JWTManager) GenerateAPIToken(tenantID uint, permissions []string) (string, time.Time, error) {
expiresAt := time.Now().Add(30 * 24 * time.Hour) // 30天有效
claims := &Claims{
TenantID: tenantID,
Role: "api",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "smart-customer-service",
Subject: "api_token",
},
}
// 添加自定义声明
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 添加权限声明
token.Claims.(jwt.MapClaims)["permissions"] = permissions
tokenString, err := token.SignedString(m.secretKey)
if err != nil {
return "", time.Time{}, err
}
return tokenString, expiresAt, nil
}
// GetTokenClaims 获取令牌声明(安全版本)
func (m *JWTManager) GetTokenClaims(tokenString string) (map[string]interface{}, error) {
claims, err := m.ValidateToken(tokenString)
if err != nil {
return nil, err
}
return map[string]interface{}{
"user_id": claims.UserID,
"tenant_id": claims.TenantID,
"role": claims.Role,
"exp": claims.ExpiresAt.Time.Unix(),
"iat": claims.IssuedAt.Time.Unix(),
"iss": claims.Issuer,
"sub": claims.Subject,
}, nil
}
// RenewToken 续期令牌
func (m *JWTManager) RenewToken(tokenString string) (string, time.Time, error) {
claims, err := m.ValidateToken(tokenString)
if err != nil {
return "", time.Time{}, err
}
// 根据令牌类型续期
switch claims.Subject {
case "access_token":
return m.GenerateAccessToken(claims.UserID, claims.TenantID, claims.Role)
case "refresh_token":
return m.GenerateRefreshToken(claims.UserID, claims.TenantID)
case "api_token":
// API令牌不自动续期
return "", time.Time{}, errors.New("API令牌不支持自动续期")
default:
return "", time.Time{}, errors.New("未知的令牌类型")
}
}