490 lines
13 KiB
Markdown
490 lines
13 KiB
Markdown
# 权限注解转换指南
|
||
|
||
## 目标
|
||
|
||
将 urbanLifeline 的权限体系完全迁移到 pigx 平台的权限模型。
|
||
|
||
## 核心概念对比
|
||
|
||
| 特性 | urbanLifeline | pigx |
|
||
|------|---------------|------|
|
||
| 权限注解 | `@PreAuthorize("hasAuthority()")` | `@PreAuthorize("@pms.hasPermission()")` |
|
||
| 权限格式 | `module:resource:action` | `module_resource_action` |
|
||
| 用户获取 | `JwtUtils.getUserId()` | `SecurityUtils.getUser()` |
|
||
| 用户服务 | `UserService` (本地) | `RemoteUserService` (Feign) |
|
||
| 响应格式 | `ResultDomain<T>` | `R<T>` |
|
||
| 租户支持 | 无 | 有 (tenant_id) |
|
||
|
||
## 转换步骤详解
|
||
|
||
### 步骤 1:权限注解转换
|
||
|
||
#### 1.1 基本转换规则
|
||
|
||
```java
|
||
// ❌ 旧代码 (urbanLifeline)
|
||
@PreAuthorize("hasAuthority('workcase:ticket:create')")
|
||
public ResultDomain<TbWorkcaseDTO> createWorkcase(@RequestBody TbWorkcaseDTO dto) {
|
||
// ...
|
||
}
|
||
|
||
// ✅ 新代码 (pigx)
|
||
@PreAuthorize("@pms.hasPermission('workcase_ticket_add')")
|
||
public R<TbWorkcaseDTO> createWorkcase(@RequestBody TbWorkcaseDTO dto) {
|
||
// ...
|
||
}
|
||
```
|
||
|
||
#### 1.2 多权限组合
|
||
|
||
```java
|
||
// ❌ 旧代码 - OR 条件
|
||
@PreAuthorize("hasAuthority('workcase:ticket:update') or hasAuthority('workcase:ticket:admin')")
|
||
|
||
// ✅ 新代码 - OR 条件
|
||
@PreAuthorize("@pms.hasPermission('workcase_ticket_edit') or @pms.hasPermission('workcase_ticket_admin')")
|
||
|
||
// ❌ 旧代码 - AND 条件
|
||
@PreAuthorize("hasAuthority('workcase:ticket:view') and hasAuthority('workcase:export:data')")
|
||
|
||
// ✅ 新代码 - AND 条件
|
||
@PreAuthorize("@pms.hasPermission('workcase_ticket_view') and @pms.hasPermission('workcase_export_data')")
|
||
```
|
||
|
||
#### 1.3 动态权限检查
|
||
|
||
```java
|
||
// ❌ 旧代码
|
||
@Service
|
||
public class WorkcaseService {
|
||
public boolean canEdit(Long workcaseId) {
|
||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||
return auth.getAuthorities().stream()
|
||
.anyMatch(a -> a.getAuthority().equals("workcase:ticket:update"));
|
||
}
|
||
}
|
||
|
||
// ✅ 新代码
|
||
@Service
|
||
public class WorkcaseService {
|
||
@Autowired
|
||
private PermissionService permissionService;
|
||
|
||
public boolean canEdit(Long workcaseId) {
|
||
return permissionService.hasPermission("workcase_ticket_edit");
|
||
}
|
||
}
|
||
```
|
||
|
||
### 步骤 2:用户信息获取转换
|
||
|
||
#### 2.1 获取当前用户
|
||
|
||
```java
|
||
// ❌ 旧代码
|
||
Long userId = JwtUtils.getUserId();
|
||
String username = JwtUtils.getUsername();
|
||
String role = JwtUtils.getRole();
|
||
|
||
// ✅ 新代码
|
||
PigxUser user = SecurityUtils.getUser();
|
||
Long userId = user.getId();
|
||
String username = user.getUsername();
|
||
Long tenantId = user.getTenantId(); // 新增:租户ID
|
||
Long deptId = user.getDeptId(); // 新增:部门ID
|
||
List<String> roles = user.getRoles(); // 角色列表
|
||
```
|
||
|
||
#### 2.2 Service 层用户信息处理
|
||
|
||
```java
|
||
// ❌ 旧代码
|
||
@Service
|
||
public class WorkcaseServiceImpl implements WorkcaseService {
|
||
@Autowired
|
||
private UserService userService;
|
||
|
||
public void assignWorkcase(Long workcaseId, Long assigneeId) {
|
||
User assignee = userService.getById(assigneeId);
|
||
// 处理逻辑...
|
||
}
|
||
}
|
||
|
||
// ✅ 新代码
|
||
@Service
|
||
public class WorkcaseServiceImpl implements WorkcaseService {
|
||
@Autowired
|
||
private RemoteUserService remoteUserService;
|
||
|
||
public void assignWorkcase(Long workcaseId, Long assigneeId) {
|
||
// 使用 Feign 远程调用
|
||
R<SysUser> result = remoteUserService.selectById(assigneeId);
|
||
if (result.isSuccess() && result.getData() != null) {
|
||
SysUser assignee = result.getData();
|
||
// 处理逻辑...
|
||
} else {
|
||
throw new BusinessException("用户不存在");
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 步骤 3:响应格式转换
|
||
|
||
#### 3.1 成功响应
|
||
|
||
```java
|
||
// ❌ 旧代码
|
||
return ResultDomain.success(data);
|
||
return ResultDomain.success(list, total);
|
||
return ResultDomain.success("操作成功", data);
|
||
|
||
// ✅ 新代码
|
||
return R.ok(data);
|
||
return R.ok(list, total); // 分页响应
|
||
return R.ok(data, "操作成功");
|
||
```
|
||
|
||
#### 3.2 错误响应
|
||
|
||
```java
|
||
// ❌ 旧代码
|
||
return ResultDomain.failure("参数错误");
|
||
return ResultDomain.failure(ErrorCode.INVALID_PARAM);
|
||
throw new BusinessException("业务异常");
|
||
|
||
// ✅ 新代码
|
||
return R.failed("参数错误");
|
||
return R.failed(CommonConstants.FAIL, "参数错误");
|
||
throw new ServiceException("业务异常");
|
||
```
|
||
|
||
#### 3.3 分页响应
|
||
|
||
```java
|
||
// ❌ 旧代码
|
||
public ResultDomain<List<TbWorkcaseDTO>> list(PageParam param) {
|
||
Page<TbWorkcase> page = workcaseMapper.selectPage(param);
|
||
return ResultDomain.success(page.getRecords(), page.getTotal());
|
||
}
|
||
|
||
// ✅ 新代码
|
||
public R<IPage<TbWorkcaseDTO>> list(Page page, TbWorkcaseDTO query) {
|
||
IPage<TbWorkcaseDTO> result = workcaseMapper.selectPageVo(page, query);
|
||
return R.ok(result);
|
||
}
|
||
```
|
||
|
||
### 步骤 4:租户隔离实现
|
||
|
||
#### 4.1 实体类添加租户字段
|
||
|
||
```java
|
||
@Data
|
||
@TableName("tb_workcase")
|
||
public class TbWorkcase {
|
||
@TableId
|
||
private Long id;
|
||
|
||
private String title;
|
||
|
||
// ✅ 新增租户字段
|
||
@TableField("tenant_id")
|
||
private Long tenantId;
|
||
|
||
// 其他字段...
|
||
}
|
||
```
|
||
|
||
#### 4.2 Service 层自动注入租户
|
||
|
||
```java
|
||
@Service
|
||
public class WorkcaseServiceImpl implements WorkcaseService {
|
||
|
||
@Override
|
||
public R<TbWorkcaseDTO> save(TbWorkcaseDTO dto) {
|
||
// ✅ 自动注入当前租户
|
||
PigxUser user = SecurityUtils.getUser();
|
||
dto.setTenantId(user.getTenantId());
|
||
dto.setCreateBy(user.getUsername());
|
||
dto.setCreateTime(LocalDateTime.now());
|
||
|
||
workcaseMapper.insert(dto);
|
||
return R.ok(dto);
|
||
}
|
||
|
||
@Override
|
||
public R<IPage<TbWorkcaseDTO>> page(Page page, TbWorkcaseDTO query) {
|
||
// ✅ 查询条件自动添加租户过滤
|
||
PigxUser user = SecurityUtils.getUser();
|
||
query.setTenantId(user.getTenantId());
|
||
|
||
return R.ok(workcaseMapper.selectPageVo(page, query));
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.3 Mapper 层租户隔离
|
||
|
||
```xml
|
||
<!-- WorkcaseMapper.xml -->
|
||
<select id="selectPageVo" resultType="com.pig4cloud.pigx.app.api.dto.TbWorkcaseDTO">
|
||
SELECT * FROM tb_workcase
|
||
<where>
|
||
<!-- ✅ 租户隔离条件 -->
|
||
<if test="query.tenantId != null">
|
||
AND tenant_id = #{query.tenantId}
|
||
</if>
|
||
<if test="query.title != null and query.title != ''">
|
||
AND title LIKE CONCAT('%', #{query.title}, '%')
|
||
</if>
|
||
</where>
|
||
ORDER BY create_time DESC
|
||
</select>
|
||
```
|
||
|
||
### 步骤 5:配置 RemoteUserService
|
||
|
||
#### 5.1 添加 Feign 客户端接口
|
||
|
||
```java
|
||
package com.pig4cloud.pigx.app.api.feign;
|
||
|
||
import com.pig4cloud.pigx.common.core.constant.ServiceNameConstants;
|
||
import com.pig4cloud.pigx.common.core.util.R;
|
||
import com.pig4cloud.pigx.upms.api.entity.SysUser;
|
||
import org.springframework.cloud.openfeign.FeignClient;
|
||
import org.springframework.web.bind.annotation.*;
|
||
|
||
@FeignClient(contextId = "remoteUserService",
|
||
value = ServiceNameConstants.UPMS_SERVICE)
|
||
public interface RemoteUserService {
|
||
|
||
/**
|
||
* 根据用户ID查询用户信息
|
||
*/
|
||
@GetMapping("/user/info/{id}")
|
||
R<SysUser> selectById(@PathVariable("id") Long id);
|
||
|
||
/**
|
||
* 根据用户名查询用户信息
|
||
*/
|
||
@GetMapping("/user/info")
|
||
R<SysUser> selectByUsername(@RequestParam("username") String username);
|
||
|
||
/**
|
||
* 批量查询用户信息
|
||
*/
|
||
@PostMapping("/user/list")
|
||
R<List<SysUser>> selectBatchIds(@RequestBody List<Long> ids);
|
||
}
|
||
```
|
||
|
||
#### 5.2 使用 RemoteUserService
|
||
|
||
```java
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
public class WorkcaseServiceImpl implements WorkcaseService {
|
||
|
||
private final RemoteUserService remoteUserService;
|
||
|
||
public TbWorkcaseDTO getWorkcaseDetail(Long id) {
|
||
TbWorkcase workcase = workcaseMapper.selectById(id);
|
||
TbWorkcaseDTO dto = BeanUtil.copyProperties(workcase, TbWorkcaseDTO.class);
|
||
|
||
// 获取创建人信息
|
||
if (dto.getCreatorId() != null) {
|
||
R<SysUser> creatorResult = remoteUserService.selectById(dto.getCreatorId());
|
||
if (creatorResult.isSuccess() && creatorResult.getData() != null) {
|
||
dto.setCreatorName(creatorResult.getData().getUsername());
|
||
dto.setCreatorDeptName(creatorResult.getData().getDeptName());
|
||
}
|
||
}
|
||
|
||
// 获取处理人信息
|
||
if (dto.getAssigneeId() != null) {
|
||
R<SysUser> assigneeResult = remoteUserService.selectById(dto.getAssigneeId());
|
||
if (assigneeResult.isSuccess() && assigneeResult.getData() != null) {
|
||
dto.setAssigneeName(assigneeResult.getData().getUsername());
|
||
}
|
||
}
|
||
|
||
return dto;
|
||
}
|
||
}
|
||
```
|
||
|
||
## 批量转换工具
|
||
|
||
### 使用 IDE 批量替换(推荐)
|
||
|
||
#### IntelliJ IDEA
|
||
|
||
1. **权限注解替换**
|
||
- 查找:`@PreAuthorize\("hasAuthority\('([^:]+):([^:]+):([^']+)'\)"\)`
|
||
- 替换:`@PreAuthorize("@pms.hasPermission('$1_$2_$3')")`
|
||
- 选项:勾选 "Regex"
|
||
|
||
2. **响应格式替换**
|
||
- 查找:`ResultDomain\.success\((.*?)\)`
|
||
- 替换:`R.ok($1)`
|
||
|
||
3. **用户信息获取**
|
||
- 查找:`JwtUtils\.getUserId\(\)`
|
||
- 替换:`SecurityUtils.getUser().getId()`
|
||
|
||
#### VS Code
|
||
|
||
使用 Find and Replace (Ctrl+Shift+H),启用正则表达式模式。
|
||
|
||
### 使用脚本批量转换
|
||
|
||
创建 `convert-permissions.sh`:
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
|
||
# 转换权限注解
|
||
find ./src -name "*.java" -type f -exec sed -i \
|
||
's/@PreAuthorize("hasAuthority('\''\\([^:]*\\):\\([^:]*\\):\\([^'\'']*\\)'\''")"/@PreAuthorize("@pms.hasPermission('\''\\1_\\2_\\3'\'')"/g' {} \;
|
||
|
||
# 转换响应格式
|
||
find ./src -name "*.java" -type f -exec sed -i \
|
||
's/ResultDomain\.success(\(.*\))/R.ok(\1)/g' {} \;
|
||
|
||
# 转换用户信息获取
|
||
find ./src -name "*.java" -type f -exec sed -i \
|
||
's/JwtUtils\.getUserId()/SecurityUtils.getUser().getId()/g' {} \;
|
||
|
||
echo "转换完成!"
|
||
```
|
||
|
||
## 测试验证
|
||
|
||
### 1. 单元测试示例
|
||
|
||
```java
|
||
@SpringBootTest
|
||
@AutoConfigureMockMvc
|
||
public class WorkcaseControllerTest {
|
||
|
||
@Autowired
|
||
private MockMvc mockMvc;
|
||
|
||
@Test
|
||
@WithMockUser(username = "admin", authorities = {"workcase_ticket_add"})
|
||
public void testCreateWorkcase() throws Exception {
|
||
TbWorkcaseDTO dto = new TbWorkcaseDTO();
|
||
dto.setTitle("测试工单");
|
||
|
||
mockMvc.perform(post("/workcase")
|
||
.contentType(MediaType.APPLICATION_JSON)
|
||
.content(JSON.toJSONString(dto)))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.code").value(0))
|
||
.andExpect(jsonPath("$.data.title").value("测试工单"));
|
||
}
|
||
|
||
@Test
|
||
@WithMockUser(username = "user", authorities = {})
|
||
public void testCreateWorkcaseNoPermission() throws Exception {
|
||
mockMvc.perform(post("/workcase")
|
||
.contentType(MediaType.APPLICATION_JSON)
|
||
.content("{}"))
|
||
.andExpect(status().isForbidden());
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 集成测试检查清单
|
||
|
||
- [ ] 权限注解正确转换
|
||
- [ ] 用户信息正确获取
|
||
- [ ] 租户数据正确隔离
|
||
- [ ] 响应格式符合规范
|
||
- [ ] RemoteUserService 调用成功
|
||
- [ ] 菜单权限正确配置
|
||
- [ ] 角色权限正确分配
|
||
|
||
## 常见问题解决
|
||
|
||
### Q1: @pms.hasPermission() 不生效
|
||
|
||
**原因**:没有正确配置 PermissionService Bean
|
||
|
||
**解决**:
|
||
```java
|
||
@Configuration
|
||
public class SecurityConfig {
|
||
@Bean("pms")
|
||
public PermissionService permissionService() {
|
||
return new PermissionService();
|
||
}
|
||
}
|
||
```
|
||
|
||
### Q2: RemoteUserService 调用失败
|
||
|
||
**原因**:Feign 客户端未正确配置
|
||
|
||
**解决**:
|
||
1. 检查 `@EnableFeignClients` 注解
|
||
2. 确认服务名称正确
|
||
3. 添加熔断处理
|
||
|
||
```java
|
||
@FeignClient(contextId = "remoteUserService",
|
||
value = ServiceNameConstants.UPMS_SERVICE,
|
||
fallback = RemoteUserServiceFallback.class)
|
||
```
|
||
|
||
### Q3: 租户数据泄露
|
||
|
||
**原因**:查询时未添加租户过滤
|
||
|
||
**解决**:
|
||
1. 使用 MyBatis-Plus 租户插件
|
||
2. 手动添加租户条件
|
||
|
||
```java
|
||
@Configuration
|
||
public class MybatisPlusConfig {
|
||
@Bean
|
||
public TenantLineInnerInterceptor tenantLineInnerInterceptor() {
|
||
return new TenantLineInnerInterceptor(new TenantLineHandler() {
|
||
@Override
|
||
public Expression getTenantId() {
|
||
PigxUser user = SecurityUtils.getUser();
|
||
return new LongValue(user.getTenantId());
|
||
}
|
||
|
||
@Override
|
||
public String getTenantIdColumn() {
|
||
return "tenant_id";
|
||
}
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
## 迁移验证
|
||
|
||
完成转换后,执行以下验证:
|
||
|
||
1. **编译检查**:确保所有代码编译通过
|
||
2. **启动检查**:应用能正常启动
|
||
3. **权限测试**:各接口权限控制正确
|
||
4. **数据隔离**:租户数据正确隔离
|
||
5. **功能测试**:业务功能正常运行
|
||
|
||
## 总结
|
||
|
||
权限迁移是整个系统迁移的核心部分,需要:
|
||
1. 仔细转换每个权限注解
|
||
2. 正确处理用户信息获取
|
||
3. 实现租户数据隔离
|
||
4. 充分测试验证
|
||
|
||
建议分模块逐步迁移,每完成一个模块就进行测试验证。 |