我们看一下真实项目的权限设计案例,典型的错误用例。
首先我们从Controller的Mapping看起。
/**
* 新增
*
* @param entity
* @return
*/
@PostMapping
@Transactional
@PreAuthorize("@ps.check(@CONTRACTOR.INSERT)")
public Object create(@RequestBody Contractor entity) {
......
}
这里使用@PreAuthorize注解来对API进行授权。
用户权限加载
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account user = userService.getByPhoneNumber(username);
if (user == null) {
throw new UsernameNotFoundException("没有找到该用户!");
}
Role role = roleService.getById(user.getRoleId());
if (role == null) {
throw new AccessDeniedException("该用户没有被授权,或授权角色已被删除。");
}
return new FioUserDetails(user, role);
}
public class FioUserDetails implements UserDetails, Serializable {
Account account;
Role role;
public FioUserDetails(Account account, Role role) {
this.account = account;
this.role = role;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (String role : RoleUtils.getRoles(this.role)) {
authorities.add(new SimpleGrantedAuthority(role));
}
if (this.account.getId() <= 10) {
authorities.add(new SimpleGrantedAuthority("ADMIN"));
}
return authorities;
}
......
}
}
/**
* 权限 util
*
* @author liangzhuowei
* @date 2021/11/27
* @since open-jdk 11
*/
public class RoleUtils {
/**
* 扫描被权限注解的类包,返回相应需要的权限
*
* @return
*/
public static List<String> getRoles(Role role) {
List<String> roles = new ArrayList<>();
// 现场操作者权限
ClassUtil.scanPackageByAnnotation("包名...", FioOperateRole.class)
.forEach(item -> {
String className = item.getSimpleName().toUpperCase(Locale.ROOT);
FioOperateRole operateRole = item.getAnnotation(FioOperateRole.class);
RolePrefix prefix = FioOperateRole.class.getAnnotation(RolePrefix.class);
RoleTypes types = FioOperateRole.class.getAnnotation(RoleTypes.class);
int idx = operateRole.value();
String fix = prefix.value().name();
var ts = Arrays.asList(types.value());
if (ts.contains(RoleTypes.Type.INSERT) && (role.getOperateInsert() >> idx & 0x1) == 1) {
roles.add(String.join("_", List.of(fix, className, RoleTypes.Type.INSERT.name())));
}
if (ts.contains(RoleTypes.Type.DELETE) && (role.getOperateDelete() >> idx & 0x1) == 1) {
roles.add(String.join("_", List.of(fix, className, RoleTypes.Type.DELETE.name())));
}
if (ts.contains(RoleTypes.Type.UPDATE) && (role.getOperateModify() >> idx & 0x1) == 1) {
roles.add(String.join("_", List.of(fix, className, RoleTypes.Type.UPDATE.name())));
}
if (ts.contains(RoleTypes.Type.SELECT) && (role.getOperateSelect() >> idx & 0x1) == 1) {
roles.add(String.join("_", List.of(fix, className, RoleTypes.Type.SELECT.name())));
}
});
// 其他附属权限
ClassUtil.scanPackageByAnnotation("包名...", FioExtraRole.class)
.forEach(item -> {
String className = item.getSimpleName().toUpperCase(Locale.ROOT);
FioExtraRole extraRole = item.getAnnotation(FioExtraRole.class);
RolePrefix prefix = FioExtraRole.class.getAnnotation(RolePrefix.class);
RoleTypes types = FioExtraRole.class.getAnnotation(RoleTypes.class);
int idx = extraRole.value();
String fix = prefix.value().name();
var ts = Arrays.asList(types.value());
if (ts.contains(RoleTypes.Type.INSERT) && (role.getExtraInsert() >> idx & 0x1) == 1) {
roles.add(String.join("_", List.of(fix, className, RoleTypes.Type.INSERT.name())));
}
if (ts.contains(RoleTypes.Type.DELETE) && (role.getExtraDelete() >> idx & 0x1) == 1) {
roles.add(String.join("_", List.of(fix, className, RoleTypes.Type.DELETE.name())));
}
if (ts.contains(RoleTypes.Type.UPDATE) && (role.getExtraModify() >> idx & 0x1) == 1) {
roles.add(String.join("_", List.of(fix, className, RoleTypes.Type.UPDATE.name())));
}
if (ts.contains(RoleTypes.Type.SELECT) && (role.getExtraSelect() >> idx & 0x1) == 1) {
roles.add(String.join("_", List.of(fix, className, RoleTypes.Type.SELECT.name())));
}
});
// 办公权限
ClassUtil.scanPackageByAnnotation("包名...", FioOfficeRole.class)
.forEach(item -> {
String className = item.getSimpleName().toUpperCase(Locale.ROOT);
FioOfficeRole officeRole = item.getAnnotation(FioOfficeRole.class);
RolePrefix prefix = FioOfficeRole.class.getAnnotation(RolePrefix.class);
RoleTypes types = FioOfficeRole.class.getAnnotation(RoleTypes.class);
int idx = officeRole.value();
String fix = prefix.value().name();
var ts = Arrays.asList(types.value());
if (ts.contains(RoleTypes.Type.INSERT) && (role.getOfficeInsert() >> idx & 0x1) == 1) {
roles.add(String.join("_", List.of(fix, className, RoleTypes.Type.INSERT.name())));
}
if (ts.contains(RoleTypes.Type.DELETE) && (role.getOfficeDelete() >> idx & 0x1) == 1) {
roles.add(String.join("_", List.of(fix, className, RoleTypes.Type.DELETE.name())));
}
if (ts.contains(RoleTypes.Type.UPDATE) && (role.getOfficeModify() >> idx & 0x1) == 1) {
roles.add(String.join("_", List.of(fix, className, RoleTypes.Type.UPDATE.name())));
}
if (ts.contains(RoleTypes.Type.SELECT) && (role.getOfficeSelect() >> idx & 0x1) == 1) {
roles.add(String.join("_", List.of(fix, className, RoleTypes.Type.SELECT.name())));
}
});
// 管理权限
ClassUtil.scanPackageByAnnotation("包名...", FioManageRole.class)
.forEach(item -> {
String className = item.getSimpleName().toUpperCase(Locale.ROOT);
FioManageRole manageRole = item.getAnnotation(FioManageRole.class);
RolePrefix prefix = FioOfficeRole.class.getAnnotation(RolePrefix.class);
RoleTypes types = FioOfficeRole.class.getAnnotation(RoleTypes.class);
int idx = manageRole.value();
String fix = prefix.value().name();
var ts = Arrays.asList(types.value());
if (ts.contains(RoleTypes.Type.INSERT) && (role.getOfficeInsert() >> idx & 0x1) == 1) {
roles.add(String.join("_", List.of(fix, className, RoleTypes.Type.INSERT.name())));
}
if (ts.contains(RoleTypes.Type.DELETE) && (role.getOfficeDelete() >> idx & 0x1) == 1) {
roles.add(String.join("_", List.of(fix, className, RoleTypes.Type.DELETE.name())));
}
if (ts.contains(RoleTypes.Type.UPDATE) && (role.getOfficeModify() >> idx & 0x1) == 1) {
roles.add(String.join("_", List.of(fix, className, RoleTypes.Type.UPDATE.name())));
}
if (ts.contains(RoleTypes.Type.SELECT) && (role.getOfficeSelect() >> idx & 0x1) == 1) {
roles.add(String.join("_", List.of(fix, className, RoleTypes.Type.SELECT.name())));
}
});
return roles;
}
/**
* 检查 权限值 是否唯一
*/
public static void check() {
ClassUtil.scanPackageByAnnotation("包名...", FioOfficeRole.class)
.stream()
.collect(Collectors.groupingBy(clazz -> clazz.getAnnotation(FioOfficeRole.class).value(), Collectors.toList()))
.forEach((k, v) -> {
if (v.size() > 1) {
Class<?> aClass = v.get(0);
throw new RuntimeException(aClass.getSimpleName() + "的" + FioOfficeRole.class.getSimpleName() + "值重复,值:" + k);
}
});
ClassUtil.scanPackageByAnnotation("com.gason.backend.entity", FioOperateRole.class)
.stream()
.collect(Collectors.groupingBy(clazz -> clazz.getAnnotation(FioOperateRole.class).value(), Collectors.toList()))
.forEach((k, v) -> {
if (v.size() > 1) {
Class<?> aClass = v.get(0);
throw new RuntimeException(aClass.getSimpleName() + "的" + FioOperateRole.class.getSimpleName() + "值重复,值:" + k);
}
});
ClassUtil.scanPackageByAnnotation("包名...", FioExtraRole.class)
.stream()
.collect(Collectors.groupingBy(clazz -> clazz.getAnnotation(FioExtraRole.class).value(), Collectors.toList()))
.forEach((k, v) -> {
if (v.size() > 1) {
Class<?> aClass = v.get(0);
throw new RuntimeException(aClass.getSimpleName() + "的" + FioExtraRole.class.getSimpleName() + "值重复,值:" + k);
}
});
ClassUtil.scanPackageByAnnotation("包名...", FioManageRole.class)
.stream()
.collect(Collectors.groupingBy(clazz -> clazz.getAnnotation(FioManageRole.class).value(), Collectors.toList()))
.forEach((k, v) -> {
if (v.size() > 1) {
Class<?> aClass = v.get(0);
throw new RuntimeException(aClass.getSimpleName() + "的" + FioManageRole.class.getSimpleName() + "值重复,值:" + k);
}
});
}
}
说明:Account表保存用户的账号信息,唯一的username,唯一的roleId
-
FioUserDetails 根据用户的唯一角色roleId(包含了 Office, Operate, Extra, Manage), 查找所有注解对应的实体类, 将相应的增删改查权限装入用户的Authorities
格式为: OFFCIE_CONTRACTOR_INSERT
此格式为 OfficeAuthority 类加载:
/**
* 权限
*
* @author liangzhuowei
* @date 2021/12/10
* @since open-jdk 11
*/
public class OfficeAuthority {
/**
* 0
* 角色管理权限
*/
@Service("ROLE")
public static class ROLE {
public final String INSERT = "OFFICE_ROLE_INSERT";
public final String DELETE = "OFFICE_ROLE_DELETE";
public final String UPDATE = "OFFICE_ROLE_UPDATE";
public final String SELECT = "OFFICE_ROLE_SELECT";
}
/**
* 1
* 用户管理权限
*/
@Service("ACCOUNT")
public static class ACCOUNT {
public final String INSERT = "OFFICE_ACCOUNT_INSERT";
public final String DELETE = "OFFICE_ACCOUNT_DELETE";
public final String UPDATE = "OFFICE_ACCOUNT_UPDATE";
public final String SELECT = "OFFICE_ACCOUNT_SELECT";
}
...
}
实体类和权限绑定
/**
* <p>
* 公司档案字段表
* </p>
*
* @author MyBatisPlusGenerator
* @since 2023-08-22
*/
@Data
@FioOfficeRole(52)
@TableName("fio_contractor")
@ApiModel(value = "Contractor对象", description = "公司档案字段表")
public class Contractor extends BasicCustomEntity {
......
}
权限检查
/**
* 权限检验
*
* @author liangzhuowei
* @date 2021/11/29
* @since open-jdk 11
*/
@Service("ps")
public class PermissionService {
....
}
============================================================================
以上为真实项目的权限设计,点评如下:
-
授权的资源对象应该是Controller里的UrlMapping对应的API,这里的设计直接改为了实体类,并且每个实体类对应的权限固定为4个:增,删,改,查
-
授权操作应该是创建角色并将角色与资源对象关联,这里的设计没有角色的创建,直接创建了角色与资源对象操作的关联,请注意,这里的角色并没有与资源对象关联,而是直接关联资源对象的操作(增,删,改,查)
-
这里的设计是将资源对象分为4大类:OFFICE, OPERATE,MANAGE,EXTRA,授权操作不能直接作用的资源对象
-
每个用户的角色只有一个...
正确的使用方式请参考SpringSecurity的官方文档, 正确区分权限框架的使用场景