8 Commits
f0325d8f2d
...
3038cd0387
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
3038cd0387 |
fix(security): CORS 拆分为 SDK 开放与管理白名单双轨
修复
|
2 months ago |
|
|
9fe3ee6828 |
fix(sdk): 修复历史会话不显示图片附件
- MessageConverter 落库时持久化多模态 Media 到 metadata._media - ConversationService 历史消息接口提取 _media 为顶层 images 字段 - SDK 前端历史映射补齐 images,复用 renderUserBubble 附件渲染 |
2 months ago |
|
|
081758128c |
fix(app): 修复切换会话后用户提问被查询重写结果覆盖
RAG 查询重写(REWRITE 等策略)的润色产物被当作用户消息落库到 chat_message 会话记忆表,导致 ChatSDK 切换角色/对话后前端拉取 历史显示的是润色版而非原始提问。 ChatPipeline 在 RAG 分支将 finalMessage 由 rag.rewrittenQuery() 改为 ctx.message(),使查询重写仅服务于检索,用户消息(传给模型、 落库、前端展示)统一为原始提问。 |
2 months ago |
|
|
377aad48db |
fix(app): 修复切换推理模型后对话流式超时
chatStreamOpenAi 流开始时立即发出首片 chunk(role=assistant),确保 SSE 响应头及时 flush,避免推理模型思考阶段无输出导致前端等待首字节触发 60s 超时;AI_READ_TIMEOUT 由 120s 放宽到 300s,给推理模型思考+生成留足总时长 |
2 months ago |
|
|
0aa11a5ec6 |
fix(app): 修复推荐问题显示哨兵标记异常
SuggestionResponseParser 剥离旧版 ___SUGGESTIONS___ 哨兵标记,避免按行降级解析成垃圾条目;同步更新 suggestion_prompt 种子默认值,移除与新架构"只输出 JSON 数组"要求矛盾的哨兵文案 |
2 months ago |
|
|
d5d392aca4 |
feat(sdk): 支持图片上传与多模态对话
SDK 前端新增图片附件上传与多模态对话能力,输入区新增附件按钮与图片预览,新增 allowImageUpload 配置项;/attachment/upload 鉴权改由 SdkAuthFilter 承担(移除方法级 @PreAuthorize),JwtAuthFilter/SecurityConfig 同步放行 |
2 months ago |
|
|
5856237746 |
fix(frontend): 移除分类管理列表中的文档数量显示
|
2 months ago |
|
|
9338fcbd5f |
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 未自动填充的问题 |
2 months ago |
48 changed files with 1450 additions and 288 deletions
-
15CLAUDE.md
-
70client/src/api.ts
-
132client/src/chat.ts
-
1client/src/config.ts
-
28client/src/dom.ts
-
2client/src/i18n.ts
-
1client/src/index.ts
-
20client/src/types.ts
-
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
-
2frontend/src/views/CategoryManager.vue
-
5frontend/src/views/LoginPage.vue
-
13src/main/java/com/wok/supportbot/app/AssistantApp.java
-
6src/main/java/com/wok/supportbot/app/ChatPipeline.java
-
2src/main/java/com/wok/supportbot/app/ChatRequest.java
-
9src/main/java/com/wok/supportbot/app/SuggestionResponseParser.java
-
201src/main/java/com/wok/supportbot/auth/AuthController.java
-
4src/main/java/com/wok/supportbot/config/ChatModelFactory.java
-
31src/main/java/com/wok/supportbot/config/CorsConfig.java
-
65src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java
-
6src/main/java/com/wok/supportbot/controller/AttachmentController.java
-
32src/main/java/com/wok/supportbot/controller/SysUserController.java
-
70src/main/java/com/wok/supportbot/converter/MessageConverter.java
-
4src/main/java/com/wok/supportbot/entity/SysUser.java
-
5src/main/java/com/wok/supportbot/handler/MyMetaObjectHandler.java
-
4src/main/java/com/wok/supportbot/rag/RagContext.java
-
47src/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
-
43src/main/java/com/wok/supportbot/security/SdkAuthFilter.java
-
18src/main/java/com/wok/supportbot/security/SdkJwtTokenProvider.java
-
47src/main/java/com/wok/supportbot/security/SecurityConfig.java
-
9src/main/java/com/wok/supportbot/service/ConversationService.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
-
222src/main/resources/init-database.sql
-
6src/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