kindergarten_java/docs/dev-logs/2026-03-17.md
En dfbf89e8fe feat: 实现多地点登录支持
主要变更:
- 修改 JwtTokenRedisService.validateToken() 方法,移除 token 一致性检查
- 在 JwtAuthenticationFilter 中新增 isAccountActive() 方法,每次请求验证账户状态
- 所有状态判断改为忽略大小写 (equalsIgnoreCase)
- 保留黑名单机制用于主动踢人、登出等场景

功能特性:
- 同一账号可以在多个设备/浏览器同时登录
- 各个登录状态的 token 都有效,不会互踢下线
- 支持所有角色:admin, school, teacher, parent
- JWT token 过期时间(默认 24 小时)保证安全性

修改文件:
- JwtTokenRedisService.java
- JwtAuthenticationFilter.java
- AuthServiceImpl.java

文档更新:
- docs/CHANGELOG.md
- docs/dev-logs/2026-03-17.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 12:13:21 +08:00

146 lines
4.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 开发日志 - 2026-03-17
## 多地点登录支持实现
### 修改内容
#### 1. JwtTokenRedisService.java
**文件路径**: `reading-platform-java/src/main/java/com/reading/platform/common/security/JwtTokenRedisService.java`
**修改内容**:
- 修改 `validateToken()` 方法,移除了 token 一致性检查
- 现在只检查 token 是否在黑名单中
- 允许同一账号有多个有效的 token支持多地点同时登录
**修改前逻辑**:
```java
// 检查是否与存储的 token 一致
String storedToken = getStoredToken(username);
if (storedToken == null) {
return true;
}
boolean isValid = token.equals(storedToken);
return isValid; // 如果不一致则返回 false导致互踢下线
```
**修改后逻辑**:
```java
// 仅检查是否在黑名单中
if (isBlacklisted(token)) {
return false;
}
// 不再检查是否与存储的 token 一致,允许同一账号有多个有效 token
return true;
```
#### 2. JwtAuthenticationFilter.java
**文件路径**: `reading-platform-java/src/main/java/com/reading/platform/common/security/JwtAuthenticationFilter.java`
**修改内容**:
- 新增依赖注入:`AdminUserMapper`, `TenantMapper`, `TeacherMapper`, `ParentMapper`
- 在 token 验证通过后,增加账户状态检查
- 新增 `isAccountActive()` 方法,根据用户角色查询对应表验证账户状态
**新增代码**:
```java
// 检查账户状态是否为 active
if (!isAccountActive(payload)) {
log.debug("Account is not active for user: {}", payload.getUsername());
sendError(response, HttpStatus.UNAUTHORIZED, "账户已被禁用,请联系管理员");
return;
}
```
**新增方法**:
```java
private boolean isAccountActive(JwtPayload payload) {
String role = payload.getRole();
Long userId = payload.getUserId();
return switch (role) {
case "admin" -> {
AdminUser adminUser = adminUserMapper.selectById(userId);
yield adminUser != null && "active".equalsIgnoreCase(adminUser.getStatus());
}
case "school" -> {
Tenant tenant = tenantMapper.selectById(userId);
yield tenant != null && "active".equalsIgnoreCase(tenant.getStatus());
}
case "teacher" -> {
Teacher teacher = teacherMapper.selectById(userId);
yield teacher != null && "active".equalsIgnoreCase(teacher.getStatus());
}
case "parent" -> {
Parent parent = parentMapper.selectById(userId);
yield parent != null && "active".equalsIgnoreCase(parent.getStatus());
}
default -> false;
};
}
```
#### 3. AuthServiceImpl.java
**文件路径**: `reading-platform-java/src/main/java/com/reading/platform/service/impl/AuthServiceImpl.java`
**修改内容**:
- 更新 `logout()` 方法的注释,说明当前实现
#### 4. 状态判断忽略大小写修改
**修改内容**:
- 将所有 `"active".equals(status)` 修改为 `"active".equalsIgnoreCase(status)`
- 确保大小写不敏感的状态判断,如 "Active"、"ACTIVE"、"active" 都被识别为激活状态
**修改文件**:
- `JwtAuthenticationFilter.java` - `isAccountActive()` 方法中的 4 处判断
- `AuthServiceImpl.java` - `login()` 方法中的 8 处判断admin、teacher、parent、tenant 各 2 处)
### 功能说明
#### 多地点登录
- 同一账号可以在多个设备/浏览器同时登录
- 各个登录状态的 token 都有效,不会互踢下线
- JWT token 本身的过期时间(默认 24 小时)保证安全性
#### 账户状态验证
- 每次请求都会验证账户状态是否为 "active"
- 如果管理员在后台禁用某个账号,该账号的所有已登录会话将立即失效
- 支持所有角色admin、school、teacher、parent
#### 黑名单机制
- 黑名单机制仍然有效
- 可以用于主动使特定 token 失效(如踢人下线场景)
### 验证步骤
1. **多地点登录测试**:
- 使用同一账号在两个不同的浏览器登录
- 在两个浏览器都发起 API 请求
- 预期:两个登录都保持有效
2. **账户状态禁用测试**:
- 使用账号 A 在浏览器 1 登录
- 在超管后台将账号 A 的状态修改为"非激活"
- 在浏览器 1 再次发起请求,应返回"账户已被禁用"
### 安全性考虑
1. JWT token 有过期时间(默认 24 小时),过期后自动失效
2. 黑名单机制仍然有效,可以主动使特定 token 失效
3. 每次请求都会验证账户状态,确保禁用账号无法访问
### 影响范围
- 所有角色的登录认证流程
- Token 验证流程
- 账户状态检查
### 兼容性
- 此修改不影响现有功能
- 登出、黑名单等功能仍然正常工作
- 前端无需修改