问题描述
在做权限验证的时候,我们经常会遇到这样的情况:教师拥有多个学生,但是在处理学生信息的时候,教师只能操作自己班级的学生。所以,我们要做的就是,当教师尝试处理别的班的学生的时候,抛出异常。
实体关系
用户 1:1 教师,教师 m:n 班级,班级 1:n 学生
实现思路
以 findById 为例。因为从整体上看,用户和学生是 m:n 的关系,所以在调用这个接口的时候,获取该学生的所有用户,然后跟当前登录用户进行对比,如果不在其中,抛出异常。
利用切面,我们可以在 findById、update、delete 方法上进行验证。
注解
我们会在方法上添加注解,以表示对该方法进行权限验证。
@Target(ElementType.METHOD) // 注解使用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
public @interface AuthorityAnnotation {
/**
* 仓库名
*/
@Required
Class repository();
}
因为我们需要获取出学生,但是并不限于学生,所以就要将仓库 repository 作为一个参数传入。
实体
上面我们说过,需要获取学生中的用户,所以我们可以在实体中定义一个方法,获取所有有权限的用户:getBelongUsers()
但是,我们知道,学生和用户没用直接的关系,而且为了复用,在对其他实体进行验证的时候也能使用,可以考虑创建一个接口,让需要验证的实体去实现他。
这样,我们可以在让每个实体都集成这个接口,然后形成链式调用,这样就解决了上面你的两个问题。
public interface BaseEntity {
List<User> getBelongToUsers();
}
教师:
@Entity
public class Teacher implements YunzhiEntity, BaseEntity {
…
@Override
public List<User> getBelongToUsers() {
List<User> userList = new ArrayList<>();
userList.add(this.getUser());
return userList;
}
}
班级:
@Entity
public class Klass implements BaseEntity {
…
@Override
public List<User> getBelongToUsers() {
List<User> userList = new ArrayList<>();
for (Teacher teacher: this.getTeacherList()) {
userList.addAll(teacher.getBelongToUsers());
}
return userList;
}
}
学生:
@Entity
public class Student implements BaseEntity {
…
@Override
public List<User> getBelongToUsers() {
return this.getKlass().getBelongToUsers();
}
}
切面
有了实体后,我们就可以建立切面实现验证功能了。
@Aspect
@Component
public class OwnerAuthorityAspect {
private static final Logger logger = LoggerFactory.getLogger(OwnerAuthorityAspect.class.getName());
/**
* 使用注解,并第一个参数为 id
*/
@Pointcut(“@annotation(com.yunzhiclub.alice.annotation.AuthorityAnnotation) && args(id,..) && @annotation(authorityAnnotation)”)
public void doAccessCheck(Long id, AuthorityAnnotation authorityAnnotation) {}
@Before(“doAccessCheck(id, authorityAnnotation)”)
public void before(Long id, AuthorityAnnotation authorityAnnotation) {
}
首先,我们要获取到待操作对象。但是在获取对象之前,我们必须获取到 repository。
这里我们利用 applicationContext 来获取仓库 bean,然后再利用获取到的 bean,生成 repository 对象。
@Aspect
@Component
public class OwnerAuthorityAspect implements ApplicationContextAware {
private ApplicationContext applicationContext = null; // 初始化上下文
……
@Before(“doAccessCheck(id, authorityAnnotation)”)
public void before(Long id, AuthorityAnnotation authorityAnnotation) {
logger.debug(“ 获取注解上的 repository, 并通过 applicationContext 来获取 bean”);
Class<?> repositoryClass = authorityAnnotation.repository();
Object object = applicationContext.getBean(repositoryClass);
logger.debug(“ 将 Bean 转换为 CrudRepository”);
CrudRepository<BaseEntity, Object> crudRepository = (CrudRepository<BaseEntity, Object>)object;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
该类实现了 ApplicationContextAware 接口,通过 setApplicationContext 函数获取到了 applicationContext。
接下来,就是利用 repository 获取对象,然后获取他的所属用户,再与当前登录用户进行比较。
@Before(“doAccessCheck(id, authorityAnnotation)”)
public void before(Long id, AuthorityAnnotation authorityAnnotation) {
logger.debug(“ 获取注解上的 repository, 并通过 applicationContext 来获取 bean”);
Class<?> repositoryClass = authorityAnnotation.repository();
Object object = applicationContext.getBean(repositoryClass);
logger.debug(“ 将 Bean 转换为 CrudRepository”);
CrudRepository<BaseEntity, Object> crudRepository = (CrudRepository<BaseEntity, Object>)object;
logger.debug(“ 获取实体对象 ”);
Optional<BaseEntity> baseEntityOptional = crudRepository.findById(id);
if(!baseEntityOptional.isPresent()) {
throw new RuntimeException(“ 对不起,未找到相关的记录 ”);
}
BaseEntity baseEntity = baseEntityOptional.get();
logger.debug(“ 获取登录用户以及拥有者,并进行比对 ”);
List<User> belongToTUsers = baseEntity.getBelongToUsers();
User currentLoginUser = userService.getCurrentLoginUser();
Boolean havePermission = false;
if (currentLoginUser != null && belongToTUsers.size() != 0) {
for (User user: belongToTUsers) {
if (user.getId().equals(currentLoginUser.getId())) {
havePermission = true;
break;
}
}
if (!havePermission) {
throw new RuntimeException(“ 权限不允许 ”);
}
}
}
使用
在控制器的方法上使用注解:@AuthorityAnnotation,传入 repository。
@RestController
@RequestMapping(“/student”)
public class StudentController {
private final StudentService studentService; // 学生
@Autowired
public StudentController(StudentService studentService) {
this.studentService = studentService;
}
/**
* 通过 id 获取学生
*
* @param id
* @return
*/
@AuthorityAnnotation(repository = StudentRepository.class)
@GetMapping(“/{id}”)
@JsonView(StudentJsonView.get.class)
public Student findById(@PathVariable Long id) {
return studentService.findById(id);
}
}
出现的问题
实现之后,进行单元测试的过程中出现了问题。
@Test
public void update() throws Exception {
logger.info(“ 获取一个保存学生 ”);
Student student = studentService.getOneSaveStudent();
Long id = student.getId();
logger.info(“ 获取一个更新学生 ”);
Student newStudent = studentService.getOneUnSaveStudent();
String jsonString = JSONObject.toJSONString(newStudent);
logger.info(“ 发送更新请求 ”);
this.mockMvc
.perform(put(baseUrl + “/” + id)
.cookie(this.cookie)
.content(jsonString)
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk());
}
400 的错误,说明参数错误,参数传的是实体,看下传了什么:
我们看到,这个字段并不是我们实体中的字段,但是为什么序列化的时候出现了这个字段呢?
原因是这样的,我们在实体中定义了一个 getBelongToUsers 函数,然后 JSONobject 在进行序列化的时候会根据实体中的 getter 方法,获取 get 后面的为 key,也就是将 belongToUsers 看做了字段。
所以就出现了上面传实体字段多出的情况,从而引发了 400 的错误。
解决
我们不想 JSONobject 在序列化的时候处理 getBelongToUsers,就需要声明一下,这里用到了注解:@JsonIgnore。这样在序列化的时候就会忽略它。
@Entity
public class Student implements BaseEntity {
……
@JsonIgnore
@Override
public List<User> getBelongToUsers() {
return this.getKlass().getBelongToUsers();
}
}
修改后的学生实体如上,其他实现了 getBelongToUsers 方法的,都需要做相同处理。
总结
在解决这个问题的时候,开始就是自己埋头写,很多细节都没有处理好。然后偶然 google 到了潘老师之前写过的一篇文章,就对前面写的进行了完善。虽然自己解决问题的过程还是有很多收获的,但是如果开始直接参考这篇文章,会省不少事。
其实是这样的,我们写博客,一方面是让自己有所提升,另一方面也是为了团队中的其他成员少走一些弯路。看来这次我是没有好好利用资源了。
相关参考:https://my.oschina.net/dashan…https://segmentfault.com/a/11…