Browse Source
feat(security): JWT token_version 服务端撤销与登录安全加固
feat(security): JWT token_version 服务端撤销与登录安全加固
- 新增 token_version 服务端撤销机制:改密/禁用/角色变更/登出时自增,使已签发 token 立即失效 - refresh token 从 localStorage 迁移到 httpOnly Cookie,access token 有效期缩短至 15 分钟 - 新增登录失败锁定(LoginAttemptService)与密码强度校验(PasswordValidator) - SDK JWT 密钥支持环境变量注入,使用默认密钥时告警 修复: - assignRoles 角色未变化仍自增 token_version,导致用户新建用户时 token 被误失效而跳转登录 - 角色 ID 解析兼容前端字符串传输,避免雪花 ID 精度丢失导致 ClassCastException - updateFill 改用 setFieldValByName,修复 update_time 未自动填充的问题Spring-AI-1.1.2
30 changed files with 1067 additions and 205 deletions
-
30frontend/src/App.vue
-
16frontend/src/api/auth.ts
-
66frontend/src/api/request.ts
-
4frontend/src/layouts/MainLayout.vue
-
91frontend/src/layouts/Topbar.vue
-
18frontend/src/stores/auth.ts
-
6frontend/src/types/models.ts
-
4frontend/src/utils/sse.ts
-
21frontend/src/utils/token.ts
-
5frontend/src/views/LoginPage.vue
-
201src/main/java/com/wok/supportbot/auth/AuthController.java
-
21src/main/java/com/wok/supportbot/config/CorsConfig.java
-
51src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java
-
32src/main/java/com/wok/supportbot/controller/SysUserController.java
-
4src/main/java/com/wok/supportbot/entity/SysUser.java
-
5src/main/java/com/wok/supportbot/handler/MyMetaObjectHandler.java
-
43src/main/java/com/wok/supportbot/security/JwtAuthFilter.java
-
81src/main/java/com/wok/supportbot/security/JwtTokenProvider.java
-
95src/main/java/com/wok/supportbot/security/LoginAttemptService.java
-
48src/main/java/com/wok/supportbot/security/PasswordValidator.java
-
39src/main/java/com/wok/supportbot/security/SdkAuthFilter.java
-
18src/main/java/com/wok/supportbot/security/SdkJwtTokenProvider.java
-
17src/main/java/com/wok/supportbot/security/SecurityConfig.java
-
63src/main/java/com/wok/supportbot/service/SysUserService.java
-
15src/main/resources/application-dev.yml
-
16src/main/resources/application-prod.yml
-
28src/main/resources/application.yml
-
210src/main/resources/init-database.sql
-
8src/main/resources/static/sdk/test.html
-
16src/main/resources/support-bot.sql
@ -0,0 +1,95 @@ |
|||||
|
package com.wok.supportbot.security; |
||||
|
|
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.jdbc.core.JdbcTemplate; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.springframework.transaction.annotation.Transactional; |
||||
|
|
||||
|
import java.sql.Timestamp; |
||||
|
|
||||
|
/** |
||||
|
* 登录失败尝试记录服务 |
||||
|
* 用于防范暴力破解:同一账号连续失败达到阈值后锁定一段时间。 |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
@Service |
||||
|
public class LoginAttemptService { |
||||
|
|
||||
|
private final JdbcTemplate jdbcTemplate; |
||||
|
private final int maxAttempts; |
||||
|
private final int lockMinutes; |
||||
|
|
||||
|
public LoginAttemptService(JdbcTemplate jdbcTemplate, |
||||
|
@Value("${security.login.max-attempts:5}") int maxAttempts, |
||||
|
@Value("${security.login.lock-minutes:15}") int lockMinutes) { |
||||
|
this.jdbcTemplate = jdbcTemplate; |
||||
|
this.maxAttempts = maxAttempts; |
||||
|
this.lockMinutes = lockMinutes; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 返回账号锁定的剩余秒数;未锁定返回 0 |
||||
|
*/ |
||||
|
public int getLockedSeconds(String username) { |
||||
|
if (username == null || username.isBlank()) { |
||||
|
return 0; |
||||
|
} |
||||
|
Timestamp lockedUntil = jdbcTemplate.query( |
||||
|
"SELECT locked_until FROM login_attempt WHERE username = ?", |
||||
|
rs -> rs.next() ? rs.getTimestamp("locked_until") : null, |
||||
|
username.toLowerCase() |
||||
|
); |
||||
|
if (lockedUntil == null) { |
||||
|
return 0; |
||||
|
} |
||||
|
long remaining = lockedUntil.getTime() - System.currentTimeMillis(); |
||||
|
return remaining > 0 ? (int) (remaining / 1000) : 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 记录一次登录失败,达到阈值时锁定账号 |
||||
|
*/ |
||||
|
@Transactional(rollbackFor = Exception.class) |
||||
|
public void recordFailure(String username) { |
||||
|
if (username == null || username.isBlank()) { |
||||
|
return; |
||||
|
} |
||||
|
// 边界保护:login_attempt.username 为 VARCHAR(64),超长用户名截断避免 DataIntegrityViolationException |
||||
|
String name = username.toLowerCase(); |
||||
|
if (name.length() > 64) { |
||||
|
name = name.substring(0, 64); |
||||
|
} |
||||
|
jdbcTemplate.update(""" |
||||
|
INSERT INTO login_attempt (username, failed_count, last_failed_time, locked_until) |
||||
|
VALUES (?, 1, CURRENT_TIMESTAMP, NULL) |
||||
|
ON CONFLICT (username) DO UPDATE SET |
||||
|
failed_count = CASE |
||||
|
WHEN login_attempt.locked_until IS NOT NULL AND login_attempt.locked_until <= CURRENT_TIMESTAMP THEN 1 |
||||
|
ELSE LEAST(login_attempt.failed_count + 1, ?) |
||||
|
END, |
||||
|
last_failed_time = CURRENT_TIMESTAMP, |
||||
|
locked_until = CASE |
||||
|
WHEN login_attempt.locked_until > CURRENT_TIMESTAMP THEN login_attempt.locked_until |
||||
|
WHEN login_attempt.locked_until IS NOT NULL AND login_attempt.locked_until <= CURRENT_TIMESTAMP THEN NULL |
||||
|
WHEN login_attempt.failed_count + 1 >= ? THEN CURRENT_TIMESTAMP + (? * INTERVAL '1 minute') |
||||
|
ELSE NULL |
||||
|
END, |
||||
|
update_time = CURRENT_TIMESTAMP |
||||
|
""", name, Integer.MAX_VALUE, maxAttempts, lockMinutes); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 登录成功后清除失败记录 |
||||
|
*/ |
||||
|
@Transactional(rollbackFor = Exception.class) |
||||
|
public void recordSuccess(String username) { |
||||
|
if (username == null || username.isBlank()) { |
||||
|
return; |
||||
|
} |
||||
|
int rows = jdbcTemplate.update("DELETE FROM login_attempt WHERE username = ?", username.toLowerCase()); |
||||
|
if (rows > 0) { |
||||
|
log.info("登录成功,清除失败记录: {}", username); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
package com.wok.supportbot.security; |
||||
|
|
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
/** |
||||
|
* 密码强度校验器 |
||||
|
* 统一校验创建用户、修改密码、自助改密等场景的密码复杂度。 |
||||
|
*/ |
||||
|
@Component |
||||
|
public class PasswordValidator { |
||||
|
|
||||
|
private static final int MIN_LENGTH = 8; |
||||
|
private static final int MAX_LENGTH = 64; |
||||
|
|
||||
|
/** |
||||
|
* 校验密码强度,不通过则抛出 IllegalArgumentException |
||||
|
* |
||||
|
* 规则: |
||||
|
* - 长度 8-64 位 |
||||
|
* - 同时包含字母和数字 |
||||
|
*/ |
||||
|
public void validate(String password) { |
||||
|
if (password == null || password.length() < MIN_LENGTH || password.length() > MAX_LENGTH) { |
||||
|
throw new IllegalArgumentException("密码长度需在 8-64 位之间"); |
||||
|
} |
||||
|
if (!containsLetter(password) || !containsDigit(password)) { |
||||
|
throw new IllegalArgumentException("密码需同时包含字母和数字"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private boolean containsLetter(String s) { |
||||
|
for (char c : s.toCharArray()) { |
||||
|
if (Character.isLetter(c)) { |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private boolean containsDigit(String s) { |
||||
|
for (char c : s.toCharArray()) { |
||||
|
if (Character.isDigit(c)) { |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue