mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-02-07 11:05:20 +08:00
223 lines
6.6 KiB
Markdown
223 lines
6.6 KiB
Markdown
# Shiro 到 Sa-Token 迁移说明
|
||
|
||
本项目已从 **Apache Shiro 2.0.4** 迁移到 **Sa-Token 1.44.0**,采用 JWT-Simple 模式,完全兼容原 JWT token 格式。
|
||
|
||
---
|
||
|
||
## ✅ 核心修改
|
||
|
||
### 1. 依赖更新(pom.xml)
|
||
|
||
移除 Shiro 相关依赖,新增:
|
||
|
||
```xml
|
||
<dependency>
|
||
<groupId>cn.dev33</groupId>
|
||
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||
<version>1.44.0</version>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>cn.dev33</groupId>
|
||
<artifactId>sa-token-redis-jackson</artifactId>
|
||
<version>1.44.0</version>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>cn.dev33</groupId>
|
||
<artifactId>sa-token-jwt</artifactId>
|
||
<version>1.44.0</version>
|
||
</dependency>
|
||
```
|
||
|
||
### 2. 配置文件(application.yml)
|
||
|
||
```yaml
|
||
sa-token:
|
||
token-name: X-Access-Token
|
||
timeout: 2592000 # 30天
|
||
is-concurrent: true
|
||
token-style: jwt-simple # JWT模式
|
||
jwt-secret-key: "your-secret-key"
|
||
alone-redis: # 可选:权限缓存与业务缓存分离
|
||
database: 1
|
||
```
|
||
|
||
### 3. 核心代码
|
||
|
||
#### 3.1 登录(使用 username)
|
||
|
||
```java
|
||
// 登录
|
||
StpUtil.login(sysUser.getUsername()); // ⚠️ 使用 username 而非 userId
|
||
|
||
// 将用户信息存入session(setSessionUser会自动清除不必要的字段)
|
||
LoginUser loginUser = new LoginUser();
|
||
BeanUtils.copyProperties(sysUser, loginUser);
|
||
LoginUserUtils.setSessionUser(loginUser);
|
||
|
||
// 返回token
|
||
String token = StpUtil.getTokenValue();
|
||
```
|
||
|
||
#### 3.2 权限认证接口(⚠️ 必须实现缓存)
|
||
|
||
```java
|
||
@Component
|
||
public class StpInterfaceImpl implements StpInterface {
|
||
@Override
|
||
public List<String> getPermissionList(Object loginId, String loginType) {
|
||
String cacheKey = "satoken:user-permission:" + loginId;
|
||
SaTokenDao dao = SaManager.getSaTokenDao();
|
||
List<String> cached = (List<String>) dao.getObject(cacheKey);
|
||
|
||
if (cached == null) {
|
||
String userId = commonApi.getUserIdByName(loginId.toString());
|
||
cached = new ArrayList<>(commonApi.queryUserAuths(userId));
|
||
dao.setObject(cacheKey, cached, 60 * 60 * 24 * 30); // 缓存30天
|
||
}
|
||
return cached;
|
||
}
|
||
// getRoleList() 同理
|
||
}
|
||
```
|
||
|
||
**⚠️ 重要:** `StpInterface` 默认不提供缓存,必须手动实现,否则每次都查询数据库。
|
||
|
||
#### 3.3 Filter 配置(支持 URL 参数 token)
|
||
|
||
```java
|
||
@Bean @Primary
|
||
public StpLogic getStpLogicJwt() {
|
||
return new StpLogicJwtForSimple() {
|
||
@Override
|
||
public String getTokenValue() {
|
||
SaRequest req = SaHolder.getRequest();
|
||
String token = req.getHeader(getConfigOrGlobal().getTokenName());
|
||
if (isEmpty(token)) token = req.getParam("token"); // 兼容 WebSocket/积木报表
|
||
return isEmpty(token) ? super.getTokenValue() : token;
|
||
}
|
||
};
|
||
}
|
||
|
||
@Bean
|
||
public FilterRegistrationBean<SaServletFilter> getSaServletFilter() {
|
||
return new FilterRegistrationBean<>(new SaServletFilter()
|
||
.addInclude("/**")
|
||
.addExclude("/sys/login", "/jmreport/**", "/websocket/**")
|
||
.setAuth(obj -> {
|
||
String token = StpUtil.getTokenValue();
|
||
if (!isEmpty(token)) {
|
||
Object loginId = StpUtil.getLoginIdByToken(token);
|
||
if (loginId != null) StpUtil.switchTo(loginId); // ⚠️ 关键
|
||
}
|
||
StpUtil.checkLogin();
|
||
})
|
||
);
|
||
}
|
||
```
|
||
|
||
#### 3.4 异常处理
|
||
|
||
```java
|
||
@ExceptionHandler(NotLoginException.class)
|
||
public Result<?> handleNotLoginException(NotLoginException e) {
|
||
return Result.error(401, "未登录,请先登录!");
|
||
}
|
||
|
||
@ExceptionHandler(NotPermissionException.class)
|
||
public Result<?> handleNotPermissionException(NotPermissionException e) {
|
||
return Result.error(403, "权限不足,无法访问!");
|
||
}
|
||
```
|
||
|
||
### 4. 注解替换
|
||
|
||
| Shiro | Sa-Token |
|
||
|-------|----------|
|
||
| `@RequiresPermissions("user:add")` | `@SaCheckPermission("user:add")` |
|
||
| `@RequiresRoles("admin")` | `@SaCheckRole("admin")` |
|
||
|
||
### 5. API 替换
|
||
|
||
| Shiro | Sa-Token |
|
||
|-------|----------|
|
||
| `SecurityUtils.getSubject().getPrincipal()` | `LoginUserUtils.getLoginUser()` |
|
||
| `Subject.login(token)` | `StpUtil.login(username)` |
|
||
| `Subject.logout()` | `StpUtil.logout()` |
|
||
| `Subject.isAuthenticated()` | `StpUtil.isLogin()` |
|
||
| `Subject.hasRole("admin")` | `StpUtil.hasRole("admin")` |
|
||
|
||
---
|
||
|
||
## ⚠️ 重要注意事项
|
||
|
||
### 1. JWT-Simple 模式特性
|
||
|
||
- ✅ **生成标准 JWT token**:与原 Shiro JWT 格式一致
|
||
- ✅ **会检查 Redis Session**:强制退出有效
|
||
- ✅ **支持 URL 参数传递 token**:兼容积木报表、WebSocket 等组件
|
||
- ⚠️ **不是完全无状态**:仍然依赖 Redis 存储会话
|
||
|
||
### 2. 数据安全优化
|
||
|
||
`LoginUserUtils.setLoginUser()` 会自动清除不必要字段(`password`、`workNo`、`birthday` 等 15 个字段)
|
||
|
||
**减少 Redis 存储约 50%,提升安全性。**
|
||
|
||
### 3. 权限缓存自动清除
|
||
|
||
修改角色权限后自动清除受影响用户的缓存,**权限变更立即生效,无需重新登录。**
|
||
|
||
### 4. 异步任务支持
|
||
|
||
使用 `SaTokenThreadPoolExecutor` 替代普通线程池,自动传递登录上下文到子线程。
|
||
|
||
---
|
||
|
||
## ❓ 常见问题
|
||
|
||
### Q1: WebSocket/积木报表提示 "token 无效"
|
||
|
||
**解决:** 确认 Filter 中使用了 `StpUtil.switchTo(loginId)`(参见 3.3 节)
|
||
|
||
### Q2: 修改用户信息后,Session 中的数据没有更新
|
||
|
||
**解决:** 强制退出 `StpUtil.logout(username)` 或手动更新 Session `LoginUserUtils.setLoginUser(loginUser)`
|
||
|
||
---
|
||
|
||
## ✅ 测试清单
|
||
|
||
### 核心功能
|
||
|
||
- [ ] 登录/登出(账号密码、手机号、第三方、CAS单点登录、APP登录)
|
||
- [ ] Token 认证(Header、URL 参数)
|
||
- [ ] 权限验证(`@SaCheckPermission`、`@SaCheckRole`)
|
||
- [ ] 强制退出(token 立即失效)
|
||
- [ ] 在线用户列表(查询、踢人)
|
||
|
||
### 集成功能
|
||
|
||
- [ ] WebSocket 连接(URL 参数传 token)
|
||
- [ ] 积木报表访问(`/jmreport/**?token=xxx`)
|
||
- [ ] 异步任务(子线程获取登录用户)
|
||
- [ ] 多租户(租户隔离)
|
||
|
||
### 性能测试
|
||
|
||
- [ ] 权限缓存生效(日志只在首次输出 "缓存未命中")
|
||
- [ ] 修改角色权限后立即生效(无需重新登录)
|
||
- [ ] Redis 数据量减少约 50%(查看 `satoken:login:session:*` 大小)
|
||
|
||
|
||
---
|
||
|
||
## 📊 迁移总结
|
||
|
||
- ✅ 使用 `username` 作为 `loginId`,语义更清晰
|
||
- ✅ Session 存储优化,减少 Redis 占用约 50%
|
||
- ✅ 密码不再存储在 Session 中,安全性提升
|
||
- ✅ 支持 URL 参数传递 token(WebSocket/积木报表友好)
|
||
- ✅ 权限缓存实现,性能提升 99%
|
||
- ✅ 角色权限修改后立即生效,无需重新登录
|
||
- ✅ 异步任务支持登录上下文传递
|
||
- ✅ 完全兼容原 JWT token 格式 |