Web权限控制从无到有的思考过程
之前在看弹幕的时候,有同学问我是否可以录制一下权限控制的课程,刚好最近我在公司就做这个。
如果我是一个小白,不是小黑,那我怎么去实现这个功能呢?
当然,我还是知道RBAC的,Role Base Access Control,基于角色的访问控制。
什么是RBAC
Role Base Access Control,基于角色的访问控制。
重点是基于角色,然后通过角色控制是否可以访问。举个简单的例子,校长,老师,学生,这些都是角色。那权限呢?删除学生就是权限了吧。校长有没有这个权限呢?老师有没有这个权限呢?学生有没有这个权限。
这就是角色有权限去做什么事情。
如果你是校长,那么你有删除学生的权限。再推广到权限管理系统你就是用户,角色就是校长,权限就是删除学生。如果你是学生,你就没有删除学生这个权限了。
具体的细节,代码体现不用考虑,只知道这个几个对象和关系即可。
权限控制的应用和实现宏观角度
我们不关注细节,只关注大的方向。
首先是应用场景,什么时候需要权限控制呀?一般是控制台吧,后台控制面板,管理端之类的。
高并发的情况多吗?不多,毕竟管理的用户占很少一部分。
所以我们可以把接口(功能,权限)划分为普通用户的接口和管理员用户的接口。
由这里,我们从网关那里就可以过滤掉,不让普通用户访问后台管理的接口。
接下来呢,能进管理后台的,都是管理员了。
但是不同的管理是有不同角色的。比如说老板,比如说财务,比如说商务,比如说运营人员。不同的职位,不同的角色能看到的东西不一样,能操作的东西不一样。
而权限控制,我们就是控制这块内容。
而这部分内容又分前端和后端。
后端是真正控制权限的地方,但是前端,为了方便用户使用,我们会隐藏当前用户操作不了的接口,无法使用的UI。
而后端才是真正控制权限的地方,就算你不通过网页,你自己创建请求,模拟请求,你不是对应的角色,还是访问不了对应的接口。
表
实现以上这些功能我们要多少张表呢?需要什么字段呢?关系又是如何的呢?
- 用户表,这是最基本的吧
- 角色表,用于描述用户是什么角色
- 权限表,描述角色的权限
这是三张最基本的表,但是呢?这表之间的关系如何描述呢?
比如说,某用户是什么角色,那么就需要一张用户-角色表了
如何描述这个角色有什么权限呢?我们又需要一张角色-权限表了。
以上,我们通过5张表,就够可以完成权限的管理了。
数据从何而来?
- 用户数据,用户数据当然是用户注册,或者管理员添加
- 角色也是,先有角色,添加用户的时候,可以设定用户的角色,当然也可以修改啦。
- 那权限的数据由谁添加呢?目前我看到很多例子都是手动添加的,与后台关联起来。其实我们可以通过注解的方式,添加在接口的地方,控制接口或者控制service层本质上就是控制了权限。
当我们应用起来的时候,自动去扫描相关的注解,然后权限入库。这样子就不需要手动去添加权限了。
这里的数据,相信前面两个对于同学们来说没有太多的难度,CRUD的操作。
对于权限入库的方式,准备以下代码参考参考吧
权限入库思路
我们通过注解的方式在app起来的时候去去扫描相关的类,找到类以后呢,再后去到所有方法,再过滤出有特定注解的内容即可。
- 定义扫描的注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({PermissionScanRegister.class})
public @interface PermissionScan {
String[] value() default {};
}
权限描述的注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PermissionInfo {
//权限的名称
String name() default "";
//接口名称,这个你不给也行,直接获取到PreAuthorize里的内容
String api() default "";
//模块
String model() default "content";
}
- PermissionScanRegister
这个其实就是处理扫描类的,我们会在这里面去扫描对应包下的类。
@Slf4j
public class PermissionScanRegister
implements ImportBeanDefinitionRegistrar,
ResourceLoaderAware {
private ResourceLoader resourceLoader;
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
//先拿到要扫描的包
AnnotationAttributes annotationAttributes =
AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(PermissionScan.class.getName()));
String[] packagesList = annotationAttributes.getStringArray("value");
if (packagesList.length == 0) {
throw new IllegalArgumentException("You must set up PermissionScan default value.");
}
//在这个包下,所有包含estController注解的类,然后找到里面所有包含的权限描述
//不使用默认的过滤器,我们自己设置过滤器
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, false) {
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
for (String basePackage : basePackages) {
Set<BeanDefinition> candidates = this.findCandidateComponents(basePackage);
Iterator<BeanDefinition> iterator = candidates.iterator();
while (iterator.hasNext()) {
BeanDefinition next = iterator.next();
String beanClassName = next.getBeanClassName();
log.info("beanClassName ==> {}", beanClassName);
try {
//获取到字节码
Class<?> aClass = Class.forName(beanClassName);
Method[] methods = aClass.getMethods();
for (Method method : methods) {
//每一个方法
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
PermissionInfo permissionInfo = method.getAnnotation(PermissionInfo.class);
if (preAuthorize != null && permissionInfo != null) {
String authorizeContent = preAuthorize.value()
.replaceAll("@permission.check\\('", "")
.replaceAll("'\\)", "");
String name = permissionInfo.permissionName();
String model = permissionInfo.model();
//这些获取就是我们需要入库的内容了
log.info("name => {},model => {},api==> {}", name, model, authorizeContent);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
return null;
}
};
//这些包下
//过滤出有RestController注解的类
scanner.addIncludeFilter(new AnnotationTypeFilter(RestController.class));
scanner.setResourceLoader(this.resourceLoader);
scanner.scan(packagesList);
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
- 使用
在启动类上添加这个注解
@PermissionScan({"net.sob.content.api",
"net.sob.content.api.admin",
"net.sob.content.api.ucenter"})
这样子,启动的时候就知道我们要扫描的包有哪些了。
我们是通过RestController.class来找到对应的接口类的,这些包下,并且是有这个注解的才是我们要找到的。
在接口上添加注解
@PermissionInfo(name = "设置文章置顶", model = "ct")
@PreAuthorize("@permission.check('article:top:put')")
@PutMapping("/ct/admin/article/top/{articleId}")
public R setIsTop(@PathVariable("articleId") String articleId) {
return articleService.updateBlogIsTop(articleId);
}
这样子,我们的权限描述就会在我们的注册里获取到了,数据有了,入库的话同学们应该不在话下了吧。
我写了几个,运行结果:
PermissionScanRegister - name => 获取文章总数,model => ct,api==> article:total-count:get
PermissionScanRegister - name => 设置文章置顶,model => ct,api==> article:top:put
以上则我们权限控制的后台技术,关于前端的,我们可以动态地去创建菜单之类的,不难。
如果有时间我们就做一套权限管理的课程吧。实践和思考并行的方式,以解决问题为目的的研究过程,从无到有。