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:
249
backend/pkg/jwt/jwt_manager.go
Normal file
249
backend/pkg/jwt/jwt_manager.go
Normal 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("未知的令牌类型")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user