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:
2026-04-18 15:33:12 +08:00
parent 9d7abb8a46
commit 8095307d82
23 changed files with 186 additions and 86 deletions

View File

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

View File

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

View File

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