Files
erp-backend/docs/jwt-redis-usage.md

451 lines
11 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.
# JWT + Redis + Session 双认证方式使用文档
## 概述
本文档描述了ERP系统中同时支持两种认证方式的使用说明
- **Web端后台管理**传统Session方式基于Cookie
- **小程序端**JWT方式基于Token存储于Redis
两种认证方式同时启用,互不影响。
## 1. 认证方式对比
| 特性 | Web端Session | 小程序JWT |
|------|-----------------|--------------|
| 认证机制 | Cookie + Session | TokenHeader |
| 登录接口 | POST `/auth/login` | POST `/api/auth/login` |
| 登出接口 | POST `/auth/logout` | POST `/api/auth/logout` |
| Token存储 | 服务端Session + Cookie | Redis + 客户端存储 |
| 适用场景 | 浏览器后台管理 | 小程序/移动端 |
| 单点登录 | 支持(同一账号同时只能一处登录) | 支持多设备登录 |
## 2. Web端Session方式
### 2.1 登录
**接口:** `POST /auth/login`
**请求方式:** 表单提交Content-Type: application/x-www-form-urlencoded
**请求参数:**
```
username=admin&password=123456
```
**响应示例:**
```json
{
"code": 0,
"message": "success"
}
```
**前端使用:**
```typescript
const loginParams = new URLSearchParams();
loginParams.append("username", username);
loginParams.append("password", password);
fetch("/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: loginParams,
credentials: "include", // 重要携带Cookie
});
```
### 2.2 登出
**接口:** `POST /auth/logout`
**前端使用:**
```typescript
fetch("/auth/logout", {
method: "POST",
credentials: "include",
});
```
### 2.3 检测登录状态
**接口:** `GET /open/check-login`
**响应示例(已登录):**
```json
{
"code": 0,
"message": "success",
"data": {
"isLoggedIn": true,
"userInfo": {
"userId": 1,
"loginName": "admin",
"userName": "管理员"
},
"message": "登录状态有效Session"
}
}
```
## 3. 小程序端JWT方式
### 3.1 登录
**接口:** `POST /api/auth/login`
**Content-Type** `application/json`
**请求体:**
```json
{
"username": "admin",
"password": "123456"
}
```
**响应示例:**
```json
{
"code": 0,
"message": "success",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"expiresIn": 86400,
"refreshExpiresIn": 604800,
"userId": 1,
"loginName": "admin",
"userName": "管理员"
}
}
```
### 3.2 请求携带Token
在请求头中添加Authorization字段
```
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
```
**示例:**
```typescript
fetch("/api/some-endpoint", {
method: "GET",
headers: {
"Authorization": "Bearer " + accessToken,
},
});
```
### 3.3 刷新Token
**接口:** `POST /api/auth/refresh`
**请求参数:**
```
refreshToken=eyJhbGciOiJIUzI1NiJ9...
```
**响应示例:** 返回新的accessToken和refreshToken
### 3.4 登出
**接口:** `POST /api/auth/logout`
**请求头:**
```
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
```
**说明:** 将当前token加入黑名单使其失效
## 4. Redis缓存服务
### 4.1 缓存服务接口 (CacheService)
位置:`com.niuan.erp.common.cache.CacheService`
提供以下操作:
#### 基本操作
```java
// 设置缓存
<T> void set(String key, T value);
// 设置缓存并设置过期时间
<T> void set(String key, T value, long timeout, TimeUnit unit);
// 获取缓存
<T> T get(String key);
// 删除缓存
Boolean delete(String key);
// 判断key是否存在
Boolean hasKey(String key);
```
#### Hash操作
```java
// 设置字段值
<T> void hSet(String key, String field, T value);
// 获取字段值
<T> T hGet(String key, String field);
// 获取所有字段和值
<T> Map<String, T> hGetAll(String key);
// 删除字段
Long hDelete(String key, Object... fields);
```
#### Set操作
```java
// 添加成员
<T> Long sAdd(String key, T... members);
// 获取所有成员
<T> Set<T> sMembers(String key);
// 删除成员
<T> Long sRemove(String key, T... members);
```
### 4.2 使用示例
```java
@Service
@RequiredArgsConstructor
public class SomeService {
private final CacheService cacheService;
public void example() {
// 设置缓存1小时过期
cacheService.set("user:1", user, 3600, TimeUnit.SECONDS);
// 获取缓存
User user = cacheService.get("user:1");
// Hash操作
cacheService.hSet("user:1:profile", "email", "user@example.com");
String email = cacheService.hGet("user:1:profile", "email");
}
}
```
## 5. 用户强制下线(踢用户)
### 5.1 机制说明
当权限、角色或用户数据发生变化时,系统会自动踢掉相关用户,强制其重新登录。
**Session方式Web端**
- 清除用户缓存
- 用户下次请求时Session验证失败
**JWT方式小程序端**
- 将用户Token加入Redis黑名单
- 用户下次请求时Token验证失败
### 5.2 触发场景
| 操作 | Web端影响 | 小程序端影响 |
|------|----------|-------------|
| 权限修改 | 踢掉所有拥有该权限的用户 | 踢掉所有拥有该权限的用户 |
| 权限删除 | 踢掉所有拥有该权限的用户 | 踢掉所有拥有该权限的用户 |
| 角色修改 | 踢掉所有拥有该角色的用户 | 踢掉所有拥有该角色的用户 |
| 角色删除 | 踢掉所有拥有该角色的用户 | 踢掉所有拥有该角色的用户 |
| 用户修改 | 踢掉该用户 | 踢掉该用户 |
| 用户删除 | 踢掉该用户 | 踢掉该用户 |
### 5.3 手动踢用户
```java
@Service
@RequiredArgsConstructor
public class SomeService {
private final UserTokenService userTokenService;
public void kickUserExample(Long userId) {
// 踢掉指定用户同时踢掉Session和JWT
userTokenService.kickUser(userId);
// 踢掉拥有指定角色的所有用户
userTokenService.kickUsersByRole(roleId);
// 踢掉拥有指定权限的所有用户
userTokenService.kickUsersByPermission(permissionId);
}
}
```
## 6. 配置说明
### 6.1 application.yml
```yaml
spring:
data:
redis:
host: localhost
port: 6379
password:
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: 1000ms
# JWT配置
jwt:
secret: your-256-bit-secret-key-here-must-be-at-least-32-characters
expiration: 86400000 # 访问令牌过期时间24小时
refresh-expiration: 604800000 # 刷新令牌过期时间7天
```
### 6.2 依赖
已在build.gradle.kts中添加
```kotlin
// Redis
implementation("org.springframework.boot:spring-boot-starter-data-redis")
implementation("org.apache.commons:commons-pool2")
```
## 7. 接口清单
### 7.1 Web端接口Session
| 接口 | 方法 | 说明 |
|------|------|------|
| `/auth/login` | POST | 登录(表单提交) |
| `/auth/logout` | POST | 登出 |
| `/open/check-login` | GET | 检测登录状态 |
### 7.2 小程序接口JWT
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/auth/login` | POST | 登录JSON |
| `/api/auth/refresh` | POST | 刷新Token |
| `/api/auth/logout` | POST | 登出 |
| `/open/check-login` | GET | 检测登录状态 |
### 7.3 API演示接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/demo/public` | GET | 公开接口(无需认证) |
| `/api/demo/protected` | GET | 受保护接口(需要认证) |
| `/api/demo/user-info` | GET | 获取当前用户信息 |
| `/api/demo/cache-test` | POST | 缓存测试 |
| `/api/demo/kick-user` | POST | 踢掉指定用户 |
## 8. 文件清单
### 新增文件
1. `com.niuan.erp.common.cache.CacheService` - 缓存服务接口
2. `com.niuan.erp.common.cache.RedisCacheServiceImpl` - Redis缓存实现
3. `com.niuan.erp.common.config.RedisConfig` - Redis配置
4. `com.niuan.erp.common.security.JwtProperties` - JWT配置属性
5. `com.niuan.erp.common.security.JwtTokenProvider` - JWT令牌提供者
6. `com.niuan.erp.common.security.TokenService` - Token服务
7. `com.niuan.erp.common.security.JwtAuthenticationFilter` - JWT认证过滤器
8. `com.niuan.erp.common.security.UserTokenService` - 用户令牌管理服务
9. `com.niuan.erp.module.open.controller.OpenController` - 公开接口控制器
10. `com.niuan.erp.module.api.controller.MiniProgramAuthController` - 小程序认证接口
11. `com.niuan.erp.module.api.controller.ApiDemoController` - API演示控制器
### 修改文件
1. `com.niuan.erp.common.config.SecurityConfig` - 安全配置同时支持Session和JWT
2. `com.niuan.erp.common.bean.UrlPermissionAuthorizationManager` - 添加open和api路径放行
3. `com.niuan.erp.module.sys.service.SysUserService` - 添加loadUserById等方法
4. `com.niuan.erp.module.sys.service.impl.SysUserServiceImpl` - 实现新方法
5. `com.niuan.erp.module.sys.mapper.SysUserMapper` - 添加查询方法
6. `com.niuan.erp.module.sys.service.impl.SysPermissionServiceImpl` - 添加踢用户逻辑
7. `com.niuan.erp.module.sys.service.impl.SysRoleServiceImpl` - 添加踢用户逻辑
8. `build.gradle.kts` - 添加Redis依赖
9. `application.yml` - 添加Redis和JWT配置
10. `teek-design-vue3-template-main/src/common/api/user.ts` - 添加checkLogin方法
## 9. 注意事项
1. **Redis必须运行**系统启动前确保Redis服务已启动
2. **JWT密钥安全**:生产环境请使用复杂的密钥,并妥善保管
3. **跨域配置**小程序端需要配置相应的CORS允许域名
4. **Token过期处理**小程序端需要处理Token过期自动调用刷新接口
5. **权限配置**API Demo接口需要 `api:demo` 权限,需要在数据库中添加该权限
## 10. 前端使用示例
### Web端Vue
```typescript
// 登录
const login = async (username: string, password: string) => {
const params = new URLSearchParams();
params.append("username", username);
params.append("password", password);
const response = await fetch("/auth/login", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params,
credentials: "include",
});
return response.json();
};
// 检测登录状态
const checkLogin = async () => {
const response = await fetch("/open/check-login", {
credentials: "include",
});
return response.json();
};
```
### 小程序端
```typescript
// 登录
const login = async (username: string, password: string) => {
const response = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
const data = await response.json();
// 保存token
if (data.code === 0) {
wx.setStorageSync("accessToken", data.data.accessToken);
wx.setStorageSync("refreshToken", data.data.refreshToken);
}
return data;
};
// 请求携带token
const requestWithToken = async (url: string, options: any = {}) => {
const token = wx.getStorageSync("accessToken");
const response = await fetch(url, {
...options,
headers: {
...options.headers,
"Authorization": "Bearer " + token,
},
});
return response.json();
};
```