本项目尝试使用基于接口的可扩展枚举,为 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"
}