上週寫了一個角色權限管理,就是好比說有學生角色,老師角色,防止學生角色對老師角色的相關功能進行操做,不如說對於學生做業評分,若是學生能夠對本身做業評分就亂套了,因此須要加入權限控制接口只能教師操做。可是有一部分違規操做沒法控制,好比說A學生提交了B學生的做業。提交做業接口雖然控制只能學生訪問,可是沒法控制相同角色的用戶對本身的資源的操做。這裏做業就是本身的資源,別人不該該能夠隨意寫一份提交。這時候就須要用到id資源權限控制。spring
這裏並非加一個註解那麼簡單了。大體思路就是操做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。api
/** * 擁有者權限參數. * */ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface OwnerKey { }
而後寫一個切面,在方法執行前進行權限認證。app
/** * 資源權限校驗. */ @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註解,沒法標識其關鍵字"); } } }
最後咱們在接口中運用ide
/** * 提交做業. * * @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()); }