fix: P0/P1 security and quality fixes
P0-01: Add ESCAPE clause to LIKE queries in operation_log.go and device.go P0-02: Add atomic Increment to L1Cache and L2Cache interfaces P0-07: Add TOTP verification step after password login P1-01: Sanitize error messages in error.go middleware P1-03: Remove err.Error() from export error messages P1-04: Add error return to CountByResultSince in login_log.go P1-05: Add transactional DeleteCascade to RoleRepository P1-06: Add PasswordChangedAt tracking for JWT token invalidation P1-07: Wrap theme SetDefault in database transaction P1-08: Use config values for database pool parameters P1-09: Add rows.Err() checks in social_account_repo.go P1-10: Validate sortOrder with map in user.go ORDER BY P1-11: Add GORM tags to Announcement struct P1-15: Add pageSize upper limit (100) to device and log handlers
This commit is contained in:
@@ -53,6 +53,7 @@ type Claims struct {
|
||||
Type string `json:"type"` // access, refresh
|
||||
Remember bool `json:"remember,omitempty"` // 记住登录标记
|
||||
JTI string `json:"jti"` // JWT ID,用于黑名单
|
||||
PCE int64 `json:"pce,omitempty"` // Password Changed Epoch,密码变更时间戳,用于 token 失效机制
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@@ -318,8 +319,8 @@ func (j *JWT) GetAlgorithm() string {
|
||||
return j.algorithm
|
||||
}
|
||||
|
||||
// GenerateAccessToken 生成访问令牌(含JTI)
|
||||
func (j *JWT) GenerateAccessToken(userID int64, username string) (string, error) {
|
||||
// GenerateAccessToken 生成访问令牌(含JTI和密码变更时间戳)
|
||||
func (j *JWT) GenerateAccessToken(userID int64, username string, pce int64) (string, error) {
|
||||
if err := j.ensureReady(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -334,6 +335,7 @@ func (j *JWT) GenerateAccessToken(userID int64, username string) (string, error)
|
||||
Username: username,
|
||||
Type: "access",
|
||||
JTI: jti,
|
||||
PCE: pce,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(j.accessTokenExpire)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
@@ -345,8 +347,8 @@ func (j *JWT) GenerateAccessToken(userID int64, username string) (string, error)
|
||||
return token.SignedString(j.signingKey())
|
||||
}
|
||||
|
||||
// GenerateRefreshToken 生成刷新令牌(含JTI)
|
||||
func (j *JWT) GenerateRefreshToken(userID int64, username string) (string, error) {
|
||||
// GenerateRefreshToken 生成刷新令牌(含JTI和密码变更时间戳)
|
||||
func (j *JWT) GenerateRefreshToken(userID int64, username string, pce int64) (string, error) {
|
||||
if err := j.ensureReady(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -361,6 +363,7 @@ func (j *JWT) GenerateRefreshToken(userID int64, username string) (string, error
|
||||
Username: username,
|
||||
Type: "refresh",
|
||||
JTI: jti,
|
||||
PCE: pce,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(j.refreshTokenExpire)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
@@ -382,14 +385,14 @@ func (j *JWT) GetRefreshTokenExpire() time.Duration {
|
||||
return j.refreshTokenExpire
|
||||
}
|
||||
|
||||
// GenerateTokenPair 生成令牌对
|
||||
func (j *JWT) GenerateTokenPair(userID int64, username string) (accessToken, refreshToken string, err error) {
|
||||
accessToken, err = j.GenerateAccessToken(userID, username)
|
||||
// GenerateTokenPair 生成令牌对(含密码变更时间戳)
|
||||
func (j *JWT) GenerateTokenPair(userID int64, username string, pce int64) (accessToken, refreshToken string, err error) {
|
||||
accessToken, err = j.GenerateAccessToken(userID, username, pce)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
refreshToken, err = j.GenerateRefreshToken(userID, username)
|
||||
refreshToken, err = j.GenerateRefreshToken(userID, username, pce)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@@ -397,17 +400,17 @@ func (j *JWT) GenerateTokenPair(userID int64, username string) (accessToken, ref
|
||||
return accessToken, refreshToken, nil
|
||||
}
|
||||
|
||||
// GenerateTokenPairWithRemember 生成令牌对(支持记住登录)
|
||||
func (j *JWT) GenerateTokenPairWithRemember(userID int64, username string, remember bool) (accessToken, refreshToken string, err error) {
|
||||
accessToken, err = j.GenerateAccessToken(userID, username)
|
||||
// GenerateTokenPairWithRemember 生成令牌对(支持记住登录,含密码变更时间戳)
|
||||
func (j *JWT) GenerateTokenPairWithRemember(userID int64, username string, remember bool, pce int64) (accessToken, refreshToken string, err error) {
|
||||
accessToken, err = j.GenerateAccessToken(userID, username, pce)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if remember {
|
||||
refreshToken, err = j.GenerateLongLivedRefreshToken(userID, username)
|
||||
refreshToken, err = j.GenerateLongLivedRefreshToken(userID, username, pce)
|
||||
} else {
|
||||
refreshToken, err = j.GenerateRefreshToken(userID, username)
|
||||
refreshToken, err = j.GenerateRefreshToken(userID, username, pce)
|
||||
}
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
@@ -416,8 +419,8 @@ func (j *JWT) GenerateTokenPairWithRemember(userID int64, username string, remem
|
||||
return accessToken, refreshToken, nil
|
||||
}
|
||||
|
||||
// GenerateLongLivedRefreshToken 生成长期刷新令牌(记住登录时使用)
|
||||
func (j *JWT) GenerateLongLivedRefreshToken(userID int64, username string) (string, error) {
|
||||
// GenerateLongLivedRefreshToken 生成长期刷新令牌(记住登录时使用,含密码变更时间戳)
|
||||
func (j *JWT) GenerateLongLivedRefreshToken(userID int64, username string, pce int64) (string, error) {
|
||||
if err := j.ensureReady(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -440,6 +443,7 @@ func (j *JWT) GenerateLongLivedRefreshToken(userID int64, username string) (stri
|
||||
Type: "refresh",
|
||||
Remember: true, // 长期会话标记
|
||||
JTI: jti,
|
||||
PCE: pce,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(expireDuration)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
@@ -506,5 +510,5 @@ func (j *JWT) RefreshAccessToken(refreshTokenString string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return j.GenerateAccessToken(claims.UserID, claims.Username)
|
||||
return j.GenerateAccessToken(claims.UserID, claims.Username, claims.PCE)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ func TestNewJWT_DoesNotPanicOnInvalidLegacyConfig(t *testing.T) {
|
||||
t.Fatal("expected manager instance")
|
||||
}
|
||||
|
||||
if _, err := manager.GenerateAccessToken(1, "tester"); err == nil {
|
||||
if _, err := manager.GenerateAccessToken(1, "tester", 0); err == nil {
|
||||
t.Fatal("expected invalid legacy manager to return error")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestNewJWTWithOptions_RS256(t *testing.T) {
|
||||
t.Fatalf("create rs256 jwt manager failed: %v", err)
|
||||
}
|
||||
|
||||
accessToken, refreshToken, err := jwtManager.GenerateTokenPair(42, "rs256-user")
|
||||
accessToken, refreshToken, err := jwtManager.GenerateTokenPair(42, "rs256-user", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("generate token pair failed: %v", err)
|
||||
}
|
||||
@@ -136,7 +136,7 @@ func TestGenerateAccessToken_Success(t *testing.T) {
|
||||
t.Fatalf("create jwt manager failed: %v", err)
|
||||
}
|
||||
|
||||
token, err := jwtManager.GenerateAccessToken(123, "testuser")
|
||||
token, err := jwtManager.GenerateAccessToken(123, "testuser", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("generate access token failed: %v", err)
|
||||
}
|
||||
@@ -170,7 +170,7 @@ func TestGenerateRefreshToken_Success(t *testing.T) {
|
||||
t.Fatalf("create jwt manager failed: %v", err)
|
||||
}
|
||||
|
||||
token, err := jwtManager.GenerateRefreshToken(456, "refreshuser")
|
||||
token, err := jwtManager.GenerateRefreshToken(456, "refreshuser", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("generate refresh token failed: %v", err)
|
||||
}
|
||||
@@ -201,7 +201,7 @@ func TestGenerateTokenPair_Success(t *testing.T) {
|
||||
t.Fatalf("create jwt manager failed: %v", err)
|
||||
}
|
||||
|
||||
accessToken, refreshToken, err := jwtManager.GenerateTokenPair(789, "pairuser")
|
||||
accessToken, refreshToken, err := jwtManager.GenerateTokenPair(789, "pairuser", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("generate token pair failed: %v", err)
|
||||
}
|
||||
@@ -238,7 +238,7 @@ func TestGenerateTokenPairWithRemember_Success(t *testing.T) {
|
||||
t.Fatalf("create jwt manager failed: %v", err)
|
||||
}
|
||||
|
||||
accessToken, refreshToken, err := jwtManager.GenerateTokenPairWithRemember(999, "rememberuser", true)
|
||||
accessToken, refreshToken, err := jwtManager.GenerateTokenPairWithRemember(999, "rememberuser", true, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("generate token pair with remember failed: %v", err)
|
||||
}
|
||||
@@ -275,7 +275,7 @@ func TestValidateAccessToken_WrongType(t *testing.T) {
|
||||
}
|
||||
|
||||
// Use a refresh token as if it were an access token
|
||||
refreshToken, err := jwtManager.GenerateRefreshToken(123, "testuser")
|
||||
refreshToken, err := jwtManager.GenerateRefreshToken(123, "testuser", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("generate refresh token failed: %v", err)
|
||||
}
|
||||
@@ -298,7 +298,7 @@ func TestValidateRefreshToken_WrongType(t *testing.T) {
|
||||
}
|
||||
|
||||
// Use an access token as if it were a refresh token
|
||||
accessToken, err := jwtManager.GenerateAccessToken(123, "testuser")
|
||||
accessToken, err := jwtManager.GenerateAccessToken(123, "testuser", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("generate access token failed: %v", err)
|
||||
}
|
||||
@@ -389,7 +389,7 @@ func TestGenerateLongLivedRefreshToken_Success(t *testing.T) {
|
||||
t.Fatalf("create jwt manager failed: %v", err)
|
||||
}
|
||||
|
||||
token, err := jwtManager.GenerateLongLivedRefreshToken(123, "longliveuser")
|
||||
token, err := jwtManager.GenerateLongLivedRefreshToken(123, "longliveuser", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("generate long lived refresh token failed: %v", err)
|
||||
}
|
||||
@@ -446,7 +446,7 @@ func TestRefreshAccessToken_Success(t *testing.T) {
|
||||
}
|
||||
|
||||
// Generate a valid refresh token first
|
||||
refreshToken, err := jwtManager.GenerateRefreshToken(123, "testuser")
|
||||
refreshToken, err := jwtManager.GenerateRefreshToken(123, "testuser", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("generate refresh token failed: %v", err)
|
||||
}
|
||||
@@ -498,7 +498,7 @@ func TestRefreshAccessToken_AccessTokenProvided(t *testing.T) {
|
||||
}
|
||||
|
||||
// Generate an access token and try to use it as refresh
|
||||
accessToken, err := jwtManager.GenerateAccessToken(123, "testuser")
|
||||
accessToken, err := jwtManager.GenerateAccessToken(123, "testuser", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("generate access token failed: %v", err)
|
||||
}
|
||||
@@ -521,7 +521,7 @@ func TestGenerateTokenPairWithRemember_RememberFalse(t *testing.T) {
|
||||
t.Fatalf("create jwt manager failed: %v", err)
|
||||
}
|
||||
|
||||
accessToken, refreshToken, err := jwtManager.GenerateTokenPairWithRemember(123, "testuser", false)
|
||||
accessToken, refreshToken, err := jwtManager.GenerateTokenPairWithRemember(123, "testuser", false, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateTokenPairWithRemember failed: %v", err)
|
||||
}
|
||||
@@ -553,7 +553,7 @@ func TestGenerateTokenPairWithRemember_NoRememberExpireConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
// Should use RefreshTokenExpire when RememberLoginExpire is not set
|
||||
accessToken, refreshToken, err := jwtManager.GenerateTokenPairWithRemember(123, "testuser", true)
|
||||
accessToken, refreshToken, err := jwtManager.GenerateTokenPairWithRemember(123, "testuser", true, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateTokenPairWithRemember failed: %v", err)
|
||||
}
|
||||
@@ -583,7 +583,7 @@ func TestGenerateLongLivedRefreshToken_NoRememberExpire(t *testing.T) {
|
||||
t.Fatalf("create jwt manager failed: %v", err)
|
||||
}
|
||||
|
||||
token, err := jwtManager.GenerateLongLivedRefreshToken(123, "testuser")
|
||||
token, err := jwtManager.GenerateLongLivedRefreshToken(123, "testuser", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateLongLivedRefreshToken failed: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user