前言
上周写了一个角色权限管理,就是比如说有学生角色,老师角色,防止学生角色对老师角色的相关功能进行操作,不如说对于学生作业评分,如果学生可以对自己作业评分就乱套了,所以需要加入权限控制接口只能教师操作。但是有一部分违规操作无法控制,比如说A学生提交了B学生的作业。提交作业接口虽然控制只能学生访问,但是无法控制相同角色的用户对自己的资源的操作。这里作业就是自己的资源,别人不应该可以随意写一份提交。这时候就需要用到id资源权限控制。
实现
这里并不是加一个注解那么简单了。大致思路就是操作id资源时会传入id, 只要在修改前验证id对应资源对应所属用户是否是当前登录用户即可。那上边例子来说就是提交作业时验证id对应作业对应所属用户是否为当前登录用户。如果id对应作业对应所属用户为A,当前登录用户为B,就禁止其操作。
实现起来也十分简单。
public Work submit(Long id, Work work) {
Work oldWork = this.getById(id);
if (!oldWork.getStudent().getId().equals(this.studentService.getCurrentStudent().getId())) {
throw new AccessDeniedException("无权更新其它学生的作业");
}
...
return this.workRepository.save(oldWork);
}
但是这并不符合规范,好的代码应该是其他操作与业务逻辑相抽离,这就用到了spring强大的Aop,面向切面编程。
在上面的代码中,我们提交作业中保存作业到数据库就是业务逻辑。而角色判断是权限判断,需要进行抽离。
这里拿老项目的代码学习一下。
首先加一个方法注解,
注解哪个接口需要id资源权限控制。
/**
* 拥有者权限认证.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OwnerSecured {
Class<? extends OwnerAuthority> value();
}
再注解哪个id是我们需要认证的id,注解哪个id使我们拿来认证的id,有可能方法参数里传入很多个id。
/**
* 拥有者权限参数.
*
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface OwnerKey {
}
然后写一个切面,在方法执行前进行权限认证。
/**
* 资源权限校验.
*/
@Aspect
@Component
public class OwnerSecuredAspect {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private final ApplicationContext applicationContext;
public OwnerSecuredAspect(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/**
* 切入点.
*
* @param ownerSecured 注解
*/
@Pointcut("@annotation(club.yunzhi.api.workReview.annotation.OwnerSecured) "
+ "&& @annotation(ownerSecured)")
public void annotationPointCut(OwnerSecured ownerSecured) {
}
/**
* 在切点前执行,权限不通过报403.
*
* @param joinPoint 切点
* @param ownerSecured 拥有者权限注解
*/
@Before("annotationPointCut(ownerSecured)")
public void before(JoinPoint joinPoint, OwnerSecured ownerSecured) {
// 根据切点的@OwnerKey注解获取我们判断所属用户的id
Object paramKey = this.getOwnerKeyValueFromMethodParam(joinPoint);
try {
// 根据我们在@OwnerSecured注解里传入的值获取相应的认证器
OwnerAuthority ownerAuthority = applicationContext.getBean(ownerSecured.value());
// 认证
if (!ownerAuthority.checkAccess(paramKey)) {
throw new AccessDeniedException("您无权对该资源进行操作");
}
} catch (BeansException beansException) {
logger.error("未获取到类型" + ownerSecured.value().toString() + "的bean,请添加");
beansException.printStackTrace();
}
}
/**
* 获取在参数中使用@OwnerKey注解的值.
*
* @param joinPoint 切点
* @return 参数值
*/
private Object getOwnerKeyValueFromMethodParam(JoinPoint joinPoint) {
Object result = null;
boolean found = false;
Object[] methodArgs = joinPoint.getArgs();
int numArgs = methodArgs.length;
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Annotation[][] annotationMatrix = methodSignature.getMethod().getParameterAnnotations();
for (int i = 0; i < numArgs; i++) {
Annotation[] annotations = annotationMatrix[i];
for (Annotation annotation : annotations) {
if (annotation.annotationType().equals(OwnerKey.class)) {
if (!found) {
result = methodArgs[i];
found = true;
} else {
this.logger.warn("找到多个OwnerKey注解,将以首个OwnerKey注解为主,非首注解将被忽略");
}
}
}
}
if (result != null) {
return result;
} else {
throw new RuntimeException("未在方法中找到OwnerKey注解,无法标识其关键字");
}
}
}
最后我们在接口中运用
/**
* 提交作业.
*
* @param id
* @param work
* @return
*/
@PutMapping("{id}")
@JsonView(SubmitJsonView.class)
@OwnerSecured(WorkService.class)
@Secured(YunzhiSecurityRole.ROLE_STUDENT)
public Work submit(@OwnerKey @PathVariable Long id, @RequestBody Work work) {
return this.workService.submit(id, work);
}
相关认证代码
@Override
public boolean checkAccess(Object key) {
if (!(key instanceof Long)) {
throw new ValidationException("接收的参数类型只能为Long");
}
Long id = (Long) key;
// 验证是否为当前学生
return this.workRepository.existsByIdAndStudent(
id, this.studentService.getCurrentStudent());
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。