451 lines
11 KiB
Markdown
451 lines
11 KiB
Markdown
# JWT + Redis + Session 双认证方式使用文档
|
||
|
||
## 概述
|
||
|
||
本文档描述了ERP系统中同时支持两种认证方式的使用说明:
|
||
- **Web端(后台管理)**:传统Session方式(基于Cookie)
|
||
- **小程序端**:JWT方式(基于Token,存储于Redis)
|
||
|
||
两种认证方式同时启用,互不影响。
|
||
|
||
## 1. 认证方式对比
|
||
|
||
| 特性 | Web端(Session) | 小程序(JWT) |
|
||
|------|-----------------|--------------|
|
||
| 认证机制 | Cookie + Session | Token(Header) |
|
||
| 登录接口 | 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();
|
||
};
|
||
```
|