13 KiB
13 KiB
权限注解转换指南
目标
将 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 基本转换规则
// ❌ 旧代码 (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 多权限组合
// ❌ 旧代码 - 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 动态权限检查
// ❌ 旧代码
@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 获取当前用户
// ❌ 旧代码
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 层用户信息处理
// ❌ 旧代码
@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 成功响应
// ❌ 旧代码
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 错误响应
// ❌ 旧代码
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 分页响应
// ❌ 旧代码
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 实体类添加租户字段
@Data
@TableName("tb_workcase")
public class TbWorkcase {
@TableId
private Long id;
private String title;
// ✅ 新增租户字段
@TableField("tenant_id")
private Long tenantId;
// 其他字段...
}
4.2 Service 层自动注入租户
@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 层租户隔离
<!-- 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 客户端接口
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
@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
-
权限注解替换
- 查找:
@PreAuthorize\("hasAuthority\('([^:]+):([^:]+):([^']+)'\)"\) - 替换:
@PreAuthorize("@pms.hasPermission('$1_$2_$3')") - 选项:勾选 "Regex"
- 查找:
-
响应格式替换
- 查找:
ResultDomain\.success\((.*?)\) - 替换:
R.ok($1)
- 查找:
-
用户信息获取
- 查找:
JwtUtils\.getUserId\(\) - 替换:
SecurityUtils.getUser().getId()
- 查找:
VS Code
使用 Find and Replace (Ctrl+Shift+H),启用正则表达式模式。
使用脚本批量转换
创建 convert-permissions.sh:
#!/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. 单元测试示例
@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
解决:
@Configuration
public class SecurityConfig {
@Bean("pms")
public PermissionService permissionService() {
return new PermissionService();
}
}
Q2: RemoteUserService 调用失败
原因:Feign 客户端未正确配置
解决:
- 检查
@EnableFeignClients注解 - 确认服务名称正确
- 添加熔断处理
@FeignClient(contextId = "remoteUserService",
value = ServiceNameConstants.UPMS_SERVICE,
fallback = RemoteUserServiceFallback.class)
Q3: 租户数据泄露
原因:查询时未添加租户过滤
解决:
- 使用 MyBatis-Plus 租户插件
- 手动添加租户条件
@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";
}
});
}
}
迁移验证
完成转换后,执行以下验证:
- 编译检查:确保所有代码编译通过
- 启动检查:应用能正常启动
- 权限测试:各接口权限控制正确
- 数据隔离:租户数据正确隔离
- 功能测试:业务功能正常运行
总结
权限迁移是整个系统迁移的核心部分,需要:
- 仔细转换每个权限注解
- 正确处理用户信息获取
- 实现租户数据隔离
- 充分测试验证
建议分模块逐步迁移,每完成一个模块就进行测试验证。