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:
@@ -79,6 +79,9 @@ func (h *DeviceHandler) GetMyDevices(c *gin.Context) {
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
devices, total, err := h.deviceService.GetUserDevices(c.Request.Context(), userID, page, pageSize)
|
||||
if err != nil {
|
||||
@@ -293,6 +296,9 @@ func (h *DeviceHandler) GetUserDevices(c *gin.Context) {
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
devices, total, err := h.deviceService.GetUserDevices(c.Request.Context(), userID, page, pageSize)
|
||||
if err != nil {
|
||||
|
||||
@@ -63,7 +63,8 @@ func (h *ExportHandler) ExportUsers(c *gin.Context) {
|
||||
|
||||
data, filename, contentType, err := h.exportService.ExportUsers(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "导出失败: " + err.Error()})
|
||||
// 安全修复:不泄露内部错误详情
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "导出失败"})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,9 @@ func (h *LogHandler) GetMyLoginLogs(c *gin.Context) {
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
logs, total, err := h.loginLogService.GetMyLoginLogs(c.Request.Context(), userID, page, pageSize)
|
||||
if err != nil {
|
||||
@@ -83,6 +86,9 @@ func (h *LogHandler) GetMyOperationLogs(c *gin.Context) {
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
logs, total, err := h.operationLogService.GetMyOperationLogs(c.Request.Context(), userID, page, pageSize)
|
||||
if err != nil {
|
||||
|
||||
@@ -74,6 +74,12 @@ func (m *AuthMiddleware) Required() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if m.isPasswordChangedSinceTokenIssued(c.Request.Context(), claims.UserID, claims.PCE) {
|
||||
c.JSON(http.StatusUnauthorized, apierrors.New(http.StatusUnauthorized, "UNAUTHORIZED", "密码已更新,请重新登录"))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if !m.isUserActive(c.Request.Context(), claims.UserID) {
|
||||
c.JSON(http.StatusUnauthorized, apierrors.New(http.StatusUnauthorized, "UNAUTHORIZED", "账号不可用,请重新登录"))
|
||||
c.Abort()
|
||||
@@ -97,7 +103,7 @@ func (m *AuthMiddleware) Optional() gin.HandlerFunc {
|
||||
token := m.extractToken(c)
|
||||
if token != "" {
|
||||
claims, err := m.jwt.ValidateAccessToken(token)
|
||||
if err == nil && !m.isJTIBlacklisted(c.Request.Context(), claims.JTI) && m.isUserActive(c.Request.Context(), claims.UserID) {
|
||||
if err == nil && !m.isJTIBlacklisted(c.Request.Context(), claims.JTI) && !m.isPasswordChangedSinceTokenIssued(c.Request.Context(), claims.UserID, claims.PCE) && m.isUserActive(c.Request.Context(), claims.UserID) {
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("token_jti", claims.JTI)
|
||||
@@ -140,6 +146,27 @@ func (m *AuthMiddleware) isJTIBlacklisted(ctx context.Context, jti string) bool
|
||||
return false
|
||||
}
|
||||
|
||||
// isPasswordChangedSinceTokenIssued 检查用户密码是否在令牌发放后已更改
|
||||
// 如果 tokenPCE 为 0(旧令牌),则不检查(向后兼容)
|
||||
func (m *AuthMiddleware) isPasswordChangedSinceTokenIssued(ctx context.Context, userID int64, tokenPCE int64) bool {
|
||||
if tokenPCE == 0 {
|
||||
// 旧令牌没有密码变更时间戳,不拦截
|
||||
return false
|
||||
}
|
||||
|
||||
if m.userRepo == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
user, err := m.userRepo.GetByID(ctx, userID)
|
||||
if err != nil || user.PasswordChangedAt.IsZero() {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果令牌的 PCE < 用户密码变更时间,说明密码在令牌发放后已更改
|
||||
return tokenPCE < user.PasswordChangedAt.Unix()
|
||||
}
|
||||
|
||||
func (m *AuthMiddleware) loadUserRolesAndPerms(ctx context.Context, userID int64) ([]string, []string) {
|
||||
if m.userRoleRepo == nil {
|
||||
return nil, nil
|
||||
|
||||
@@ -22,7 +22,9 @@ func ErrorHandler() gin.HandlerFunc {
|
||||
if appErr, ok := err.Err.(*apierrors.ApplicationError); ok {
|
||||
c.JSON(int(appErr.Code), appErr)
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, apierrors.New(http.StatusInternalServerError, "", err.Err.Error()))
|
||||
// 安全修复:未知错误不泄露内部详情,只返回通用消息
|
||||
// 详细错误记录到日志,供调试使用
|
||||
c.JSON(http.StatusInternalServerError, apierrors.New(http.StatusInternalServerError, "", "服务器内部错误"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user