今天想分享的是一个自定义的注解,@RequiresMethodPermissions,为什么想到要自定义这个注解?因为Shiro为我们提供的关于权限的注解@RequiresPermissions需要一个Permission的参数。
实践中,需要我们事先对所有权限字串(假如使用通配符Permission)做出一个规划,然后所有Coder在编码过程中严格按这个权限表来coding。
于是,我们可以偷懒一下,将权限字串规划这样定义,就使用完整的类名加方法名,即: [ClassName]:[MethodName],那么我们的注解就变成了这样的模样:
@RequiresPermissions("cn.sharetop.example.HelloController:addUser")
那么,问题来了,能不能更简化一下,并且增加一点可读性,比如这样呢:
@RequiresPermissions("新增用户")
所以,我们需要自定义这个注解了,且叫做 @RequiresMethodPermissions 吧。
第一步,定义注解。这里的value是作为权限的中文描述,并非用来判断的权限字串,真正用来判断的字串其实是被注解的方法及所在的类名:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresMethodPermissions {
String value() default "";
}
第二步,重载Shiro的Advisor。
/**
* 实现全套Permission的注解处理
*
* @author yancheng
*
* 逻辑:
*
* 从advisor 将当前注解的class/method,拼出className:methodName,作为权限字串
* 将自已advisor作为引用传给authzMethodInterceptor
* 再透传给MethodPermission的MethodInterceptor
* 再透传到MethodPermission的AnnotationHandler
* 在Handler的判断方法assertAuthorized中,校验的依据不是来自Annotation中的value而是这个透传来的advisor.permissionString
*
* */
@SuppressWarnings({"rawtypes"})
public class AuthzAttributeSourceAdvisor extends AuthorizationAttributeSourceAdvisor {
private static final long serialVersionUID = -2886148353542507772L;
protected String permissionString;
public String getPermissionString() {
return permissionString;
}
public void setPermissionString(String permissionString) {
this.permissionString = permissionString;
}
private boolean _checkMethodPermissions(Method m,Class targetClass){
Annotation a = AnnotationUtils.findAnnotation(m,RequiresMethodPermissions.class);
if(a!=null){
StringBuilder sb = new StringBuilder();
sb.append(targetClass.getName());
sb.append(":");
sb.append(m.getName());
setPermissionString(sb.toString());
return true;
}
return false;
}
public boolean matches(Method method, Class targetClass) {
if( _checkMethodPermissions(method,targetClass) )
return true;
return super.matches(method, targetClass);
}
public AuthzAttributeSourceAdvisor() {
setAdvice(new AopAnnotationAuthzMethodInterceptor(this));
}
}
从代码可以看出来,在这个advisor中,我们用类名与方法名拼装出一个真正用于判断权限的字串,并保存在自己身上(注:这只是示例代码,真实使用中需要考虑一下线程安全性)。然后就是传递了……
第三步:传递自己,上一段代码中,构造函数中,将自己(advisor)传给了AopAnnotationAuthzMethodInterceptor,并且后者将这个引用又透传到一个专门处理此注解的方法拦截器MethodPermissionAnnotationMethodInterceptor,最终的被透传到真正处理我们这个注解的Handler中。
public class MethodPermissionAnnotationHandler extends AuthorizingAnnotationHandler {
protected AuthzAttributeSourceAdvisor advisor;
public MethodPermissionAnnotationHandler(){
super(RequiresMethodPermissions.class);
this.advisor=new AuthzAttributeSourceAdvisor();
}
public MethodPermissionAnnotationHandler(AuthzAttributeSourceAdvisor advisor) {
super(RequiresMethodPermissions.class);
this.advisor=advisor;
}
@Override
public void assertAuthorized(Annotation a) throws AuthorizationException {
// TODO Auto-generated method stub
RequiresMethodPermissions rpAnnotation = (RequiresMethodPermissions)a;
String value = rpAnnotation.value();
Subject subject = getSubject();
String permissionString=this.advisor.getPermissionString();
if(!StringUtils.isEmpty(permissionString)){
subject.checkPermission(permissionString);
return;
}
}
}
在Handler中,我们改写权限的判断,不能使用注解带来的value,必须使用advisor中的permissionString来做判断确定是否拥有权限。
至此代码就完工了,可以尝试一下。
@RequiresMethodPermissions("进入管理台")
@RequestMapping(value="/admin"
,method=RequestMethod.GET
,headers = {"Accept=text/html"})
public ModelAndView showAdmin(){
ModelAndView mv = new ModelAndView();
String cnt = RandomUtils.nextLong()+"";
mv.addObject("message", cnt);
mv.setViewName("admin");
return mv;
}
证明是OK的。
现在有三个问题:
- 更偷懒的是,我们还需要一个能自动将被注解的Controller的Method以及对应的字串加入数据库,以方便我们通过后台配置。我觉得,可以使用Maven的插件,在package阶段来做这件事,随后再尝试。
- 在advisor中保存这么一个字串,其实不安全,因为spring缺省情况下,装配出的advisor是单体。我觉得应该放在一个Map中会好一些吧。
- 因为Shiro对注解的处理,套了很多层,所以上面的修改其实并不美观,有没有更好的方法呢?