fix: 前后台 Token Redis 存储隔离,修复同账号互相覆盖问题
核心变更:
- Redis key 加入 clientType 前缀:echo:auth:token:{frontend|admin}:{user_id}
- JWT Claims 新增 client_type 字段区分前台/管理端
- 新增 constants/client_type.go 定义 ClientTypeFrontend / ClientTypeAdmin
- 全链路传递 clientType:Controller → Service → TokenStore → Redis
- 中间件从 JWT Claims 提取 clientType 做 Redis 校验
- 登出只删除当前端 Token,不影响另一端
前端变更:
- 管理端 login API 改为调用 POST /api/v1/admin/auth/login(之前错误调用了前台接口)
文档更新:
- 系统设计文档、实施计划、API文档、开发规范、项目进度全部同步
Made-with: Cursor
This commit is contained in:
@@ -139,7 +139,7 @@
|
||||
|
||||
**权限:** 需认证
|
||||
|
||||
**说明:** 采用有状态 JWT 方案,Token 存储在 Redis 中(`echo:auth:token:{user_id}` 和 `echo:auth:refresh:{user_id}`)。登出时服务端会从 Redis 中删除该用户的 Access Token 和 Refresh Token,使其立即失效。客户端也应同步清除本地存储的 Token。
|
||||
**说明:** 采用有状态 JWT 方案,Token 按 clientType 隔离存储在 Redis 中(`echo:auth:token:frontend:{user_id}` 和 `echo:auth:refresh:frontend:{user_id}`)。登出时服务端只删除前台端的 Token,不影响管理端。客户端也应同步清除本地存储的 Token。
|
||||
|
||||
**成功响应:**
|
||||
```json
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **适用范围**:EchoChat 项目全端(Go 后端 + admin 管理端 + frontend 用户端)
|
||||
> **创建日期**:2026-03-02
|
||||
> **最后更新**:2026-03-02
|
||||
> **最后更新**:2026-03-02(新增前后台 Token 隔离规范)
|
||||
|
||||
---
|
||||
|
||||
@@ -202,3 +202,66 @@ if err != nil {
|
||||
| 用户不存在 | 404 | 用户不存在 | 显示 message |
|
||||
| 服务器内部错误 | 500 | 获取用户列表失败 | 显示 message |
|
||||
| 网络异常 | - | (无响应体) | 显示前端 fallback 文案 |
|
||||
|
||||
---
|
||||
|
||||
## 六、前后台 Token 隔离规范
|
||||
|
||||
### 6.1 核心原则
|
||||
|
||||
前台用户端(frontend)和后台管理端(admin)的 Token **必须在 Redis 中完全隔离**,同一用户可以同时在两端保持登录状态,互不影响。
|
||||
|
||||
### 6.2 Redis Key 格式
|
||||
|
||||
```
|
||||
echo:auth:token:{client_type}:{user_id} → Access Token
|
||||
echo:auth:refresh:{client_type}:{user_id} → Refresh Token
|
||||
```
|
||||
|
||||
| client_type | 说明 | 示例 Key |
|
||||
|------------|------|----------|
|
||||
| `frontend` | 前台用户端 | `echo:auth:token:frontend:1` |
|
||||
| `admin` | 后台管理端 | `echo:auth:token:admin:1` |
|
||||
|
||||
### 6.3 JWT Claims 中的 client_type
|
||||
|
||||
JWT Token 的 Claims 中包含 `client_type` 字段,用于:
|
||||
- 中间件校验时定位正确的 Redis key
|
||||
- 登出时只删除当前端的 Token
|
||||
- Refresh Token 刷新时保持 client_type 不变
|
||||
|
||||
```json
|
||||
{
|
||||
"user_id": 1,
|
||||
"username": "admin_test",
|
||||
"roles": ["user", "admin"],
|
||||
"client_type": "admin",
|
||||
"sub": "access",
|
||||
"exp": 1709400000,
|
||||
"iss": "echochat"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 API 路由区分
|
||||
|
||||
| 端 | 登录 API | 说明 |
|
||||
|----|---------|------|
|
||||
| 前台 | `POST /api/v1/auth/login` | clientType=frontend |
|
||||
| 管理端 | `POST /api/v1/admin/auth/login` | clientType=admin,额外检查管理员角色 |
|
||||
|
||||
### 6.5 登出行为
|
||||
|
||||
- 前台登出:只删除 `echo:auth:token:frontend:{user_id}`,不影响管理端
|
||||
- 管理端登出:只删除 `echo:auth:token:admin:{user_id}`,不影响前台
|
||||
- 管理端禁用用户:应删除该用户两端的所有 Token(由管理模块负责)
|
||||
|
||||
### 6.6 检查清单
|
||||
|
||||
新增认证相关功能时,必须确认:
|
||||
|
||||
- [ ] Login 方法传递了正确的 clientType
|
||||
- [ ] 前端调用了正确的登录 API 端点
|
||||
- [ ] Redis key 包含 clientType 前缀
|
||||
- [ ] JWT Claims 中包含 client_type 字段
|
||||
- [ ] 登出时只删除对应 clientType 的 Token
|
||||
- [ ] Token 刷新时保持原 clientType 不变
|
||||
|
||||
@@ -573,9 +573,10 @@ COMMENT ON COLUMN admin_operation_logs.created_at IS '操作时间';
|
||||
### 4.2 Redis 数据结构
|
||||
|
||||
```
|
||||
# 用户认证
|
||||
echo:auth:token:{user_id} → JWT Token (STRING, TTL 7天)
|
||||
echo:auth:refresh:{user_id} → Refresh Token (STRING, TTL 30天)
|
||||
# 用户认证(按 client_type 隔离前台和管理端)
|
||||
echo:auth:token:{client_type}:{user_id} → JWT Access Token (STRING, TTL 由配置决定)
|
||||
echo:auth:refresh:{client_type}:{user_id} → Refresh Token (STRING, TTL 由配置决定)
|
||||
# client_type: frontend(前台用户端)/ admin(后台管理端)
|
||||
|
||||
# 用户在线状态
|
||||
echo:user:online → 在线用户集合 (SET)
|
||||
|
||||
@@ -507,11 +507,11 @@ type UserInfo struct {
|
||||
**Step 4: 创建 Token Redis 存储管理**
|
||||
|
||||
创建 `app/auth/service/token_store.go`:
|
||||
- 遵循设计方案中的 Redis Key 规范:`echo:auth:token:{user_id}` / `echo:auth:refresh:{user_id}`
|
||||
- `SaveTokens(ctx, userID, accessToken, refreshToken)` — 存入 Redis(覆盖旧 Token,实现单设备登录)
|
||||
- `ValidateAccessToken(ctx, userID, token)` — 校验 Access Token 与 Redis 一致
|
||||
- `ValidateRefreshToken(ctx, userID, token)` — 校验 Refresh Token 与 Redis 一致
|
||||
- `RemoveTokens(ctx, userID)` — 登出时删除 Redis 中的 Token
|
||||
- Redis Key 按 clientType 隔离:`echo:auth:token:{client_type}:{user_id}` / `echo:auth:refresh:{client_type}:{user_id}`
|
||||
- `SaveTokens(ctx, userID, clientType, accessToken, refreshToken)` — 存入 Redis(按 clientType 隔离)
|
||||
- `ValidateAccessToken(ctx, userID, clientType, token)` — 校验 Access Token 与 Redis 一致
|
||||
- `ValidateRefreshToken(ctx, userID, clientType, token)` — 校验 Refresh Token 与 Redis 一致
|
||||
- `RemoveTokens(ctx, userID, clientType)` — 登出时删除指定 clientType 的 Token
|
||||
|
||||
**Step 5: 创建认证服务**
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
### 后端(Go)
|
||||
1. **框架组合**:Gin + GORM + Wire + Zap + Viper
|
||||
2. **JWT 策略**:有状态 JWT,Token 存储在 Redis(`echo:auth:token:{user_id}`)
|
||||
2. **JWT 策略**:有状态 JWT,Token 按 clientType 隔离存储在 Redis(`echo:auth:token:{frontend|admin}:{user_id}`)
|
||||
3. **密码加密**:bcrypt
|
||||
4. **数据库时间精度**:`TIMESTAMP(0)` 精确到秒
|
||||
5. **API 响应格式**:统一 `{ "code": 0, "message": "success", "data": ... }`
|
||||
|
||||
Reference in New Issue
Block a user