本项目尝试使用基于接口的可扩展枚举,为 APIJSON 提供更多的权限支持。
APIJSON 本身的权限定义在枚举RequestRole 中,且与框架本身的耦合度较高,如果想要在此基础上定义新的权限级别不是很方便。虽然可以通过重载 AbstractVerifier.verifyAccess 方法来自定义鉴权,但这并不能视作为可扩展的权限系统,因为无法使用 RequestRole 中定义之外的权限。
这一问题的核心是权限定义在了枚举里,所以难以扩展。「采用接口来模仿可扩展的枚举」是 Effective Java 中介绍的一种技巧,其原理是定义一个接口,然后让枚举实现这个接口,在所有使用枚举的地方换用接口,从而模仿了一个可扩展的枚举。
在本项目中,定义了一个新的接口 IRequestRole,令 RequestRole 实现这个接口,再依次修改框架中其他相关联的位置。通过 IRequestRole.register 方法,用户可以注册自己的权限枚举。通过 IRequestRole.get 方法,可以从权限字符串转换回对应的权限枚举。
- 建立
IRequestRole(位于apijson.orm包中) - 令
RequestRole实现IRequestRole,并将其内的get方法移至IRequestRole中实现 MethodAccess注解返回String[],因为IRequestRole[]编译时不确定AbstractVerifier中增加一个convertStringToIRequestRole方法,从String[]转换到IRequestRole[]。verifyAccess方法中的 switch 转换为 if else。APIJSONApplication的 static 块中使用IRequestRole.register注册内建的RequestRole
-
首先定义自己的权限枚举类(可参照
apijson.demo.config.MyRole)public enum MyRole implements IRequestRole { STUDENT, TEACHER, PRINCIPAL; }
-
在
DemoApplication的 static 块中注册自己的权限枚举类static { IRequestRole.register(MyRole.class); APIJSONApplication.DEFAULT_APIJSON_CREATOR = // ... }
-
在自己的用户类(
model.User)中添加List<String> role属性,并添加对应的 getter, setter 方法 -
重载
AbstractVerifier.verifyAccess,补全一部分框架中未完成的鉴权逻辑(判断用户请求中声明的权限,是否的确存在于用户的权限列表中)此部分与用户自己的用户类的类名相关,因此没有在框架中实现(可能可以用
Visitor接口实现?)// 自定义的权限,需要检查是否存在于列表中 if ( !(config.getRole() instanceof RequestRole) ) { List<String> visitorRoleList = new ArrayList<>(); // 处理数据库中 role 列为 NULL if (((User) this.visitor).getRole() != null) { visitorRoleList = ((User) this.visitor).getRole(); } if (!visitorRoleList.contains(config.getRole().toString())){ // 用户声明的权限不在自己的 role 列表中(伪造权限) throw new IllegalAccessException("当前用户没有声明的权限!"); } }
-
在数据库
Access表中完成对应配置INSERT INTO apijson_role_extend.Access (id, debug, name, alias, get, head, gets, heads, post, put, `delete`, date, detail) VALUES (1, 0, 'Course', '', '[ "STUDENT", "TEACHER", "PRINCIPAL" ]', '[ "UNKNOWN", "STUDENT", "TEACHER","PRINCIPAL" ]', '[ "UNKNOWN", "STUDENT", "TEACHER","PRINCIPAL" ]', '[ "UNKNOWN", "STUDENT", "TEACHER","PRINCIPAL" ]', '["TEACHER"]', '["OWNER","PRINCIPAL"]', '["TEACHER"]', '2018-11-29 00:28:53', null);
-
在调用时先登录,然后用
@role声明对应的权限// POST /get { "[]": { "Course": { "@role": "TEACHER" } } }
- MethodAccess 类型修改,不兼容旧的实现,升级的时候需要手动修改
- 引入了一些类型不安全,主要是在
IRequestRole里有对 Class 的直接操作
本项目中内置了改动后 APIJSON ORM 和 Framework 的代码,导入 Intellij IDEA 或其他开发工具后应可以直接运行 DemoApplication。
开发环境:Windows 10,JDK 13,MariaDB 10.5.9
本项目模拟一个选课系统,为简化场景,其中业务表仅有 Course (课程)一张。
在 APIJSON 内置的权限之外,定义了三个权限:
- STUDENT,学生,仅可以查看课程,但不能修改
- TEACHER,教师,可以新建、修改、删除自己的课程,但不能删除其他教师的课程
- PRINCIPAL,校长,可以修改老师创建的课程,但不能新建和修改
此外,因为课程是教师创建的,教师对课程持有 OWNER 权限。另外,未登录的用户不能访问系统的任何功能。
基于以上场景,对 Course 表配置权限如下。
- GET/GETS/HEAD/HEADS:["STUDENT", "TEACHER","PRINCIPAL"]
- POST/DELETE:["TEACHER"]
- PUT:["OWNER","PRINCIPAL"]
内置了四个用户,密码均为 1234。用户的公开信息存储在 User表中,私密信息存储在 Credential 表中。
| id | username | password | role |
|---|---|---|---|
| 1 | jerry | 1234 | ["STUDENT", "TEACHER"] |
| 2 | neko | 1234 | ["STUDENT"] |
| 3 | doge | 1234 | ["TEACHER"] |
| 4 | principal | 1234 | ["PRINCIPAL"] |
- 在 MySQL / MariaDB 中执行项目根目录
initdb.sql,并在DemoSQLConfig中配置相关连接属性。 - (可选)使用 Postman,导入项目根目录的
role_extend.postman_collection.json,内有主要的几个测试接口。 - 使用 IDEA 或其他开发工具打开项目目录,运行
DemoApplication。
登录 /login
{
"username":"jerry",
"password":"1234"
}获取课程 /get
{
"[]": {
"Course": {
"@role": "TEACHER"
}
}
}课程计数 /head
{
"Course": {
"@role": "STUDENT"
}
}新建课程 /post
{
"Course": {
"sysid": "CS244",
"name": "分布式系统",
"teacher": "t1",
"location": "二教222",
"capacity": 80,
"@role": "TEACHER" // 换成 STUDENT 就会鉴权失败了
},
"tag": "Course"
}修改课程 /put
{
"Course": {
"id": 1627252238116,
"capacity": 20,
"@role": "OWNER" // 或者 "PRINCIPLE"
},
"tag": "Course"
}删除课程 /delete
{
"Course": {
"id": 1627772387598,
"@role": "TEACHER"
},
"tag": "Course"
}