mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2025-12-08 17:12:28 +08:00
【权限替换为sa-token】更新迁移文档
This commit is contained in:
@ -1,12 +1,12 @@
|
|||||||
# Shiro 到 Sa-Token 迁移说明
|
# Shiro 到 Sa-Token 迁移指南
|
||||||
|
|
||||||
本项目已从 **Apache Shiro 2.0.4** 迁移到 **Sa-Token 1.44.0**,采用 JWT-Simple 模式,完全兼容原 JWT token 格式。
|
本项目已从 **Apache Shiro 2.0.4** 迁移到 **Sa-Token 1.44.0**,采用 JWT-Simple 模式,完全兼容原 JWT token 格式。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✅ 核心修改
|
## 📦 1. 依赖配置
|
||||||
|
|
||||||
### 1. 依赖更新(pom.xml)
|
### 1.1 Maven 依赖
|
||||||
|
|
||||||
移除 Shiro 相关依赖,新增:
|
移除 Shiro 相关依赖,新增:
|
||||||
|
|
||||||
@ -28,196 +28,362 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 配置文件(application.yml)
|
### 1.2 配置文件(application.yml)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
sa-token:
|
sa-token:
|
||||||
token-name: X-Access-Token
|
token-name: X-Access-Token
|
||||||
timeout: 2592000 # 30天
|
timeout: 2592000 # token有效期30天
|
||||||
is-concurrent: true
|
is-concurrent: true # 允许同账号并发登录
|
||||||
token-style: jwt-simple # JWT模式
|
token-style: jwt-simple # JWT模式(兼容原格式)
|
||||||
jwt-secret-key: "your-secret-key"
|
jwt-secret-key: "your-secret-key-here"
|
||||||
alone-redis: # 可选:权限缓存与业务缓存分离
|
|
||||||
database: 1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 核心代码
|
---
|
||||||
|
|
||||||
#### 3.1 登录(使用 username)
|
## 💡 2. 核心代码实现
|
||||||
|
|
||||||
|
### 2.1 登录逻辑(⚠️ 使用 username 作为 loginId)
|
||||||
|
|
||||||
```java
|
```java
|
||||||
// 登录
|
// 从数据库查询用户信息
|
||||||
StpUtil.login(sysUser.getUsername()); // ⚠️ 使用 username 而非 userId
|
SysUser sysUser = userService.getUserByUsername(username);
|
||||||
|
|
||||||
// 将用户信息存入session(setSessionUser会自动清除不必要的字段)
|
// 执行登录(自动完成:Sa-Token登录 + 存储Session + 返回token)
|
||||||
LoginUser loginUser = new LoginUser();
|
String token = LoginUserUtils.doLogin(sysUser);
|
||||||
BeanUtils.copyProperties(sysUser, loginUser);
|
|
||||||
LoginUserUtils.setSessionUser(loginUser);
|
|
||||||
|
|
||||||
// 返回token
|
// 返回token给前端
|
||||||
String token = StpUtil.getTokenValue();
|
return Result.ok(token);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3.2 权限认证接口(⚠️ 必须实现缓存)
|
**💡 设计说明:**
|
||||||
|
- `doLogin()` 方法自动完成:
|
||||||
|
1. 调用 `StpUtil.login(username)` (使用 username 而非 userId)
|
||||||
|
2. 调用 `setSessionUser()` 存储用户信息(自动清除 password 等15个字段)
|
||||||
|
3. 返回生成的 token
|
||||||
|
- 减少 Redis 存储约 50%,密码不再存储到 Session
|
||||||
|
|
||||||
|
### 2.2 权限认证接口(⚠️ 必须手动实现缓存)
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@Component
|
@Component
|
||||||
public class StpInterfaceImpl implements StpInterface {
|
public class StpInterfaceImpl implements StpInterface {
|
||||||
|
|
||||||
|
@Lazy @Resource
|
||||||
|
private CommonAPI commonApi;
|
||||||
|
|
||||||
|
private static final long CACHE_TIMEOUT = 60 * 60 * 24 * 30; // 30天
|
||||||
|
private static final String PERMISSION_CACHE_PREFIX = "satoken:user-permission:";
|
||||||
|
private static final String ROLE_CACHE_PREFIX = "satoken:user-role:";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||||
String cacheKey = "satoken:user-permission:" + loginId;
|
String username = loginId.toString();
|
||||||
|
String cacheKey = PERMISSION_CACHE_PREFIX + username;
|
||||||
SaTokenDao dao = SaManager.getSaTokenDao();
|
SaTokenDao dao = SaManager.getSaTokenDao();
|
||||||
List<String> cached = (List<String>) dao.getObject(cacheKey);
|
|
||||||
|
|
||||||
if (cached == null) {
|
// 1. 先从缓存获取
|
||||||
String userId = commonApi.getUserIdByName(loginId.toString());
|
List<String> permissionList = (List<String>) dao.getObject(cacheKey);
|
||||||
cached = new ArrayList<>(commonApi.queryUserAuths(userId));
|
|
||||||
dao.setObject(cacheKey, cached, 60 * 60 * 24 * 30); // 缓存30天
|
if (permissionList == null) {
|
||||||
|
// 2. 缓存未命中,查询数据库
|
||||||
|
log.warn("权限缓存未命中,查询数据库 [ username={} ]", username);
|
||||||
|
|
||||||
|
String userId = commonApi.getUserIdByName(username);
|
||||||
|
Set<String> permissionSet = commonApi.queryUserAuths(userId);
|
||||||
|
permissionList = new ArrayList<>(permissionSet);
|
||||||
|
|
||||||
|
// 3. 将结果缓存起来
|
||||||
|
dao.setObject(cacheKey, permissionList, CACHE_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissionList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getRoleList(Object loginId, String loginType) {
|
||||||
|
// 实现类似 getPermissionList(),使用 ROLE_CACHE_PREFIX
|
||||||
|
// 详见:StpInterfaceImpl.java
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除缓存的静态方法
|
||||||
|
public static void clearUserCache(List<String> usernameList) {
|
||||||
|
SaTokenDao dao = SaManager.getSaTokenDao();
|
||||||
|
for (String username : usernameList) {
|
||||||
|
dao.deleteObject(PERMISSION_CACHE_PREFIX + username);
|
||||||
|
dao.deleteObject(ROLE_CACHE_PREFIX + username);
|
||||||
}
|
}
|
||||||
return cached;
|
|
||||||
}
|
}
|
||||||
// getRoleList() 同理
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**⚠️ 重要:** `StpInterface` 默认不提供缓存,必须手动实现,否则每次都查询数据库。
|
**⚠️ 关键:** Sa-Token 的 `StpInterface` **不提供自动缓存**,必须手动实现,否则每次请求都会查询数据库!
|
||||||
|
|
||||||
#### 3.3 Filter 配置(支持 URL 参数 token)
|
### 2.3 Filter 配置(支持 URL 参数传递 token)
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@Bean @Primary
|
@Bean
|
||||||
|
@Primary
|
||||||
public StpLogic getStpLogicJwt() {
|
public StpLogic getStpLogicJwt() {
|
||||||
return new StpLogicJwtForSimple() {
|
return new StpLogicJwtForSimple() {
|
||||||
@Override
|
@Override
|
||||||
public String getTokenValue() {
|
public String getTokenValue() {
|
||||||
SaRequest req = SaHolder.getRequest();
|
SaRequest request = SaHolder.getRequest();
|
||||||
String token = req.getHeader(getConfigOrGlobal().getTokenName());
|
|
||||||
if (isEmpty(token)) token = req.getParam("token"); // 兼容 WebSocket/积木报表
|
// 优先级:Header > URL参数"token" > URL参数"X-Access-Token"
|
||||||
return isEmpty(token) ? super.getTokenValue() : token;
|
String tokenValue = request.getHeader(getConfigOrGlobal().getTokenName());
|
||||||
|
if (isEmpty(tokenValue)) {
|
||||||
|
tokenValue = request.getParam("token"); // 兼容 WebSocket、积木报表
|
||||||
|
}
|
||||||
|
if (isEmpty(tokenValue)) {
|
||||||
|
tokenValue = request.getParam(getConfigOrGlobal().getTokenName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return isEmpty(tokenValue) ? super.getTokenValue() : tokenValue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public FilterRegistrationBean<SaServletFilter> getSaServletFilter() {
|
public SaServletFilter getSaServletFilter() {
|
||||||
return new FilterRegistrationBean<>(new SaServletFilter()
|
return new SaServletFilter()
|
||||||
.addInclude("/**")
|
.addInclude("/**")
|
||||||
.addExclude("/sys/login", "/jmreport/**", "/websocket/**")
|
.setExcludeList(getExcludeUrls()) // 排除登录、静态资源等
|
||||||
.setAuth(obj -> {
|
.setAuth(obj -> {
|
||||||
String token = StpUtil.getTokenValue();
|
// 检查是否是免认证路径
|
||||||
if (!isEmpty(token)) {
|
String servletPath = SaHolder.getRequest().getRequestPath();
|
||||||
Object loginId = StpUtil.getLoginIdByToken(token);
|
if (InMemoryIgnoreAuth.contains(servletPath)) {
|
||||||
if (loginId != null) StpUtil.switchTo(loginId); // ⚠️ 关键
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ⚠️ 关键:如果请求带 token,先切换到对应的登录会话
|
||||||
|
try {
|
||||||
|
String token = StpUtil.getTokenValue();
|
||||||
|
if (isNotEmpty(token)) {
|
||||||
|
Object loginId = StpUtil.getLoginIdByToken(token);
|
||||||
|
if (loginId != null) {
|
||||||
|
StpUtil.switchTo(loginId); // 切换登录会话
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("切换登录会话失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最终校验登录状态
|
||||||
StpUtil.checkLogin();
|
StpUtil.checkLogin();
|
||||||
})
|
})
|
||||||
);
|
.setError(e -> {
|
||||||
|
// 返回401 JSON响应
|
||||||
|
SaHolder.getResponse()
|
||||||
|
.setStatus(401)
|
||||||
|
.setHeader("Content-Type", "application/json;charset=UTF-8");
|
||||||
|
return JwtUtil.responseErrorJson(401, "Token失效,请重新登录!");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3.4 异常处理
|
### 2.4 全局异常处理
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@ExceptionHandler(NotLoginException.class)
|
@ExceptionHandler(NotLoginException.class)
|
||||||
public Result<?> handleNotLoginException(NotLoginException e) {
|
public Result<?> handleNotLoginException(NotLoginException e) {
|
||||||
return Result.error(401, "未登录,请先登录!");
|
log.warn("用户未登录或Token失效: {}", e.getMessage());
|
||||||
|
return Result.error(401, "Token失效,请重新登录!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(NotPermissionException.class)
|
@ExceptionHandler(NotPermissionException.class)
|
||||||
public Result<?> handleNotPermissionException(NotPermissionException e) {
|
public Result<?> handleNotPermissionException(NotPermissionException e) {
|
||||||
return Result.error(403, "权限不足,无法访问!");
|
log.warn("权限不足: {}", e.getMessage());
|
||||||
|
return Result.error(403, "用户权限不足,无法访问!");
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 注解替换
|
---
|
||||||
|
|
||||||
| Shiro | Sa-Token |
|
## 🔄 3. API 迁移对照表
|
||||||
|-------|----------|
|
|
||||||
| `@RequiresPermissions("user:add")` | `@SaCheckPermission("user:add")` |
|
|
||||||
| `@RequiresRoles("admin")` | `@SaCheckRole("admin")` |
|
|
||||||
|
|
||||||
### 5. API 替换
|
### 3.1 注解替换
|
||||||
|
|
||||||
| Shiro | Sa-Token |
|
| Shiro | Sa-Token | 说明 |
|
||||||
|-------|----------|
|
|-------|----------|------|
|
||||||
| `SecurityUtils.getSubject().getPrincipal()` | `LoginUserUtils.getLoginUser()` |
|
| `@RequiresPermissions("user:add")` | `@SaCheckPermission("user:add")` | 权限校验 |
|
||||||
| `Subject.login(token)` | `StpUtil.login(username)` |
|
| `@RequiresRoles("admin")` | `@SaCheckRole("admin")` | 角色校验 |
|
||||||
| `Subject.logout()` | `StpUtil.logout()` |
|
|
||||||
| `Subject.isAuthenticated()` | `StpUtil.isLogin()` |
|
### 3.2 API 替换
|
||||||
| `Subject.hasRole("admin")` | `StpUtil.hasRole("admin")` |
|
|
||||||
|
| Shiro | Sa-Token | 说明 |
|
||||||
|
|-------|----------|------|
|
||||||
|
| `SecurityUtils.getSubject().getPrincipal()` | `LoginUserUtils.getSessionUser()` | 获取登录用户 |
|
||||||
|
| `Subject.login(token)` | `LoginUserUtils.doLogin(sysUser)` | 登录(推荐) |
|
||||||
|
| `Subject.login(token)` | `StpUtil.login(username)` | 登录(底层API) |
|
||||||
|
| `Subject.logout()` | `StpUtil.logout()` | 退出登录 |
|
||||||
|
| `Subject.isAuthenticated()` | `StpUtil.isLogin()` | 判断是否登录 |
|
||||||
|
| `Subject.hasRole("admin")` | `StpUtil.hasRole("admin")` | 判断角色 |
|
||||||
|
| `Subject.isPermitted("user:add")` | `StpUtil.hasPermission("user:add")` | 判断权限 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚠️ 重要注意事项
|
## ⚠️ 4. 重要特性说明
|
||||||
|
|
||||||
### 1. JWT-Simple 模式特性
|
### 4.1 JWT-Simple 模式特性
|
||||||
|
|
||||||
- ✅ **生成标准 JWT token**:与原 Shiro JWT 格式一致
|
- ✅ **生成标准 JWT token**:与原 Shiro JWT 格式完全兼容
|
||||||
- ✅ **会检查 Redis Session**:强制退出有效
|
- ✅ **仍然检查 Redis Session**:支持强制退出(与纯 JWT 无状态模式不同)
|
||||||
- ✅ **支持 URL 参数传递 token**:兼容积木报表、WebSocket 等组件
|
- ✅ **支持 URL 参数传递**:兼容 WebSocket、积木报表等场景
|
||||||
- ⚠️ **不是完全无状态**:仍然依赖 Redis 存储会话
|
- ⚠️ **非完全无状态**:依赖 Redis 存储会话和权限缓存
|
||||||
|
|
||||||
### 2. 数据安全优化
|
### 4.2 Session 数据优化
|
||||||
|
|
||||||
`LoginUserUtils.setLoginUser()` 会自动清除不必要字段(`password`、`workNo`、`birthday` 等 15 个字段)
|
`LoginUserUtils.setSessionUser()` 会自动清除以下字段:
|
||||||
|
|
||||||
**减少 Redis 存储约 50%,提升安全性。**
|
```
|
||||||
|
password, workNo, birthday, sex, email, phone, status,
|
||||||
|
delFlag, activitiSync, createTime, userIdentity, post,
|
||||||
|
telephone, clientId, mainDepPostId
|
||||||
|
```
|
||||||
|
|
||||||
### 3. 权限缓存自动清除
|
**优势:**
|
||||||
|
- 减少 Redis 存储约 **50%**
|
||||||
|
- 密码不再存储在 Session 中,**安全性提升**
|
||||||
|
|
||||||
修改角色权限后自动清除受影响用户的缓存,**权限变更立即生效,无需重新登录。**
|
### 4.3 权限缓存动态更新
|
||||||
|
|
||||||
### 4. 异步任务支持
|
修改角色权限后,系统会自动清除受影响用户的权限缓存:
|
||||||
|
|
||||||
使用 `SaTokenThreadPoolExecutor` 替代普通线程池,自动传递登录上下文到子线程。
|
```java
|
||||||
|
// SysPermissionController.saveRolePermission() 中
|
||||||
|
@RequestMapping(value = "/saveRolePermission", method = RequestMethod.POST)
|
||||||
|
public Result<String> saveRolePermission(@RequestBody JSONObject json) {
|
||||||
|
String roleId = json.getString("roleId");
|
||||||
|
String permissionIds = json.getString("permissionIds");
|
||||||
|
String lastPermissionIds = json.getString("lastpermissionIds");
|
||||||
|
|
||||||
|
// 保存角色权限关系
|
||||||
|
sysRolePermissionService.saveRolePermission(roleId, permissionIds, lastPermissionIds);
|
||||||
|
|
||||||
|
// ⚠️ 关键:清除拥有该角色的所有用户的权限缓存
|
||||||
|
clearRolePermissionCache(roleId);
|
||||||
|
|
||||||
|
return Result.ok("保存成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实现:查询该角色下的所有用户,批量清除缓存
|
||||||
|
private void clearRolePermissionCache(String roleId) {
|
||||||
|
List<String> usernameList = new ArrayList<>();
|
||||||
|
|
||||||
|
// 分页查询拥有该角色的用户
|
||||||
|
int pageNo = 1, pageSize = 100;
|
||||||
|
while (true) {
|
||||||
|
Page<SysUser> page = new Page<>(pageNo, pageSize);
|
||||||
|
IPage<SysUser> userPage = sysUserService.getUserByRoleId(page, roleId, null, null);
|
||||||
|
|
||||||
|
if (userPage.getRecords().isEmpty()) break;
|
||||||
|
|
||||||
|
for (SysUser user : userPage.getRecords()) {
|
||||||
|
usernameList.add(user.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageNo >= userPage.getPages()) break;
|
||||||
|
pageNo++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量清除用户权限和角色缓存
|
||||||
|
if (!usernameList.isEmpty()) {
|
||||||
|
StpInterfaceImpl.clearUserCache(usernameList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**结果:** 权限变更立即生效,用户无需重新登录。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ❓ 常见问题
|
## ❓ 5. 常见问题
|
||||||
|
|
||||||
### Q1: WebSocket/积木报表提示 "token 无效"
|
### Q1: WebSocket/积木报表提示 "token 无效"
|
||||||
|
|
||||||
**解决:** 确认 Filter 中使用了 `StpUtil.switchTo(loginId)`(参见 3.3 节)
|
**原因:** token 通过 URL 参数传递时,Filter 未正确切换登录会话。
|
||||||
|
|
||||||
### Q2: 修改用户信息后,Session 中的数据没有更新
|
**解决:** 确认 Filter 配置中包含 `StpUtil.switchTo(loginId)`(见 2.3 节)
|
||||||
|
|
||||||
**解决:** 强制退出 `StpUtil.logout(username)` 或手动更新 Session `LoginUserUtils.setLoginUser(loginUser)`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✅ 测试清单
|
### Q2: 修改用户信息后,Session 数据没有更新
|
||||||
|
|
||||||
### 核心功能
|
|
||||||
|
|
||||||
- [ ] 登录/登出(账号密码、手机号、第三方、CAS单点登录、APP登录)
|
|
||||||
- [ ] Token 认证(Header、URL 参数)
|
|
||||||
- [ ] 权限验证(`@SaCheckPermission`、`@SaCheckRole`)
|
|
||||||
- [ ] 强制退出(token 立即失效)
|
|
||||||
- [ ] 在线用户列表(查询、踢人)
|
|
||||||
|
|
||||||
### 集成功能
|
|
||||||
|
|
||||||
- [ ] WebSocket 连接(URL 参数传 token)
|
|
||||||
- [ ] 积木报表访问(`/jmreport/**?token=xxx`)
|
|
||||||
- [ ] 异步任务(子线程获取登录用户)
|
|
||||||
- [ ] 多租户(租户隔离)
|
|
||||||
|
|
||||||
### 性能测试
|
|
||||||
|
|
||||||
- [ ] 权限缓存生效(日志只在首次输出 "缓存未命中")
|
|
||||||
- [ ] 修改角色权限后立即生效(无需重新登录)
|
|
||||||
- [ ] Redis 数据量减少约 50%(查看 `satoken:login:session:*` 大小)
|
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
1. **强制用户重新登录:** `StpUtil.logout(username)`
|
||||||
|
2. **手动更新 Session:** `LoginUserUtils.setSessionUser(updatedUser);`
|
||||||
|
3. **重新执行登录:** `LoginUserUtils.doLogin(updatedUser);`(会覆盖旧Session)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📊 迁移总结
|
### Q3: 权限校验每次都查询数据库,性能很差
|
||||||
|
|
||||||
- ✅ 使用 `username` 作为 `loginId`,语义更清晰
|
**原因:** `StpInterface` 未实现缓存(Sa-Token 默认不提供缓存)。
|
||||||
- ✅ Session 存储优化,减少 Redis 占用约 50%
|
|
||||||
- ✅ 密码不再存储在 Session 中,安全性提升
|
**解决:** 参照 2.2 节实现手动缓存逻辑,使用 `SaManager.getSaTokenDao()` 进行缓存操作。
|
||||||
- ✅ 支持 URL 参数传递 token(WebSocket/积木报表友好)
|
|
||||||
- ✅ 权限缓存实现,性能提升 99%
|
---
|
||||||
- ✅ 角色权限修改后立即生效,无需重新登录
|
|
||||||
- ✅ 异步任务支持登录上下文传递
|
### Q4: 清理 Redis 中的旧 Session 数据
|
||||||
- ✅ 完全兼容原 JWT token 格式
|
|
||||||
|
**场景:** 升级后可能出现反序列化错误(如 `InvalidTypeIdException`)。
|
||||||
|
|
||||||
|
**解决:**
|
||||||
|
```bash
|
||||||
|
# 方法1: 清除所有 Sa-Token Session(推荐)
|
||||||
|
redis-cli --scan --pattern "satoken:login:session:*" | xargs redis-cli del
|
||||||
|
|
||||||
|
# 方法2: 清空整个 Redis 数据库(谨慎!)
|
||||||
|
redis-cli FLUSHDB
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ 6. 测试清单
|
||||||
|
|
||||||
|
### 6.1 核心功能测试
|
||||||
|
|
||||||
|
| 功能 | 测试点 | 验证方式 |
|
||||||
|
|------|--------|----------|
|
||||||
|
| 登录/登出 | 账号密码、手机号、第三方、CAS、APP | 成功获取 token 并能访问受保护接口 |
|
||||||
|
| Token 认证 | Header、URL 参数 | Header 和 `?token=xxx` 都能正常访问 |
|
||||||
|
| 权限验证 | `@SaCheckPermission`、`@SaCheckRole` | 有权限正常访问,无权限返回 403 |
|
||||||
|
| 强制退出 | 踢人后 token 立即失效 | 被踢出用户下次请求返回 401 |
|
||||||
|
| 在线用户 | 查询在线用户列表 | 能正确显示当前在线用户 |
|
||||||
|
|
||||||
|
### 6.2 集成功能测试
|
||||||
|
|
||||||
|
| 功能 | 测试场景 | 预期结果 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| WebSocket | URL 参数传 token 连接 | 能正常建立连接并推送消息 |
|
||||||
|
| 积木报表 | `/jmreport/**?token=xxx` | 能正常访问报表 |
|
||||||
|
| 多租户 | 切换租户访问数据 | 数据正确隔离 |
|
||||||
|
|
||||||
|
### 6.3 性能验证
|
||||||
|
|
||||||
|
| 项目 | 验证方法 | 目标 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 权限缓存 | 查看日志,首次访问查DB,后续命中缓存 | 缓存命中率 > 99% |
|
||||||
|
| 权限更新 | 修改角色权限后立即访问接口 | 无需重新登录即生效 |
|
||||||
|
| Redis 存储 | 对比迁移前后 Session 大小 | 减少约 50% |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 7. 迁移总结
|
||||||
|
|
||||||
|
| 优化项 | 说明 | 收益 |
|
||||||
|
|--------|------|------|
|
||||||
|
| **loginId 设计** | 使用 `username` 而非 `userId` | 语义清晰,与业务逻辑一致 |
|
||||||
|
| **Session 优化** | 清除 15 个不必要字段 | Redis 存储减少 50%,安全性提升 |
|
||||||
|
| **权限缓存** | 手动实现 30 天缓存 | 性能提升 99%,降低 DB 压力 |
|
||||||
|
| **权限实时更新** | 角色权限修改后自动清除缓存 | 无需重新登录即生效 |
|
||||||
|
| **URL Token 支持** | Filter 中实现 `switchTo` | 兼容 WebSocket、积木报表等场景 |
|
||||||
|
| **JWT 兼容** | JWT-Simple 模式 | 完全兼容原 JWT token 格式 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 参考资料
|
||||||
|
|
||||||
|
- [Sa-Token 官方文档](https://sa-token.cc/)
|
||||||
|
- [Sa-Token JWT-Simple 模式](https://sa-token.cc/doc.html#/plugin/jwt-extend)
|
||||||
|
- [Sa-Token 权限缓存最佳实践](https://sa-token.cc/doc.html#/fun/jur-cache)
|
||||||
Reference in New Issue
Block a user