# 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 // 设置缓存 void set(String key, T value); // 设置缓存并设置过期时间 void set(String key, T value, long timeout, TimeUnit unit); // 获取缓存 T get(String key); // 删除缓存 Boolean delete(String key); // 判断key是否存在 Boolean hasKey(String key); ``` #### Hash操作 ```java // 设置字段值 void hSet(String key, String field, T value); // 获取字段值 T hGet(String key, String field); // 获取所有字段和值 Map hGetAll(String key); // 删除字段 Long hDelete(String key, Object... fields); ``` #### Set操作 ```java // 添加成员 Long sAdd(String key, T... members); // 获取所有成员 Set sMembers(String key); // 删除成员 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(); }; ```