Shiro介紹(八):資料許可權的研究@RequiresData
Shiro幫我們實現的大多為操作許可權,那麼今天我想分享一個數據許可權的方案,主要採用的仍是註解+切面攔截。
思路大概是這樣的:
- 在controller的方法引數,約定包含一個Map型別的parameters
- 通過註解宣告一下當前使用者的某個成員屬性值需要被插入到這個parameters中,並且宣告對應的欄位名稱
- 在方法體中,就可以將parameters中所有成員拿出來生成SQL,實現資料的篩選。
比如,我們需要根據當前登入使用者的名稱realName,篩選出saleName為當前使用者名稱稱的銷售資料,又或者,根據當前登入使用者的groupNames[0]為北京,篩選出所有資料欄位province為北京的計費資料。
首先,我們定義註解 RequiresData,程式碼如下:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresData {
String[] props() default "";
String[] fields() default "";
}
兩個屬性都是字串陣列,所以我們要使用時可以是這樣的:
@RequiresData(props={"realName","groupNames[0]"},fields={"saleName" ,"province"})
然後,我們需要修改AuthorizationAttributeSourceAdvisor,同樣新增對新註解的支援。
private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES =
new Class[] {
RequiresPermissions.class, RequiresRoles.class,
RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class,
RequiresAction.class,RequiresData.class
};
也同樣修改我們繼承的AopAllianceAnnotationsAuthorizingMethodInterceptor,新增新的DataAnnotationMethodInterceptor支援。
public AopAllianceAnnotationsAuthorizingMethodInterceptor(){
super();
this.methodInterceptors.add(new ActionAnnotationMethodInterceptor(new SpringAnnotationResolver()));
this.methodInterceptors.add(new DataAnnotationMethodInterceptor(new SpringAnnotationResolver()));
}
這次我們的DataAnnotationHandler是不需要做任何事情的,因為我們不是做許可權驗證,而是要修改方法引數。
所以,我們需要先定義一個介面。
public interface DataParameterRequest {
Map<String,String> getParameters();
}
保證我們在方法引數實現此介面,比如我們的引數是ConditionRequest,那麼程式碼如下:
public class ConditionRequest implements DataParameterRequest{
public String author;
private Map<String,String> params = new HashMap<String,String>();
@Override
public Map<String, String> getParameters() {
// TODO Auto-generated method stub
return params;
}
public void setParameters(Map<String,String> p){
this.params=p;
}
}
然後在Controller的方法是這樣的:
@RequiresData(props={"realName","groupNames[1]"},fields={"saleName","province"})
@RequestMapping(value = "/data", method = RequestMethod.POST, headers = {
"Content-Type=application/json;charset=utf-8", "Accept=application/json" })
public @ResponseBody Map<String,String> showData(@RequestBody ConditionRequest req){
//這裡需要根據req.getParameters()得到的Map去構造出SQL查詢條件,篩選出資料
}
下面討論一下如何利用AOP修改方法引數,主要是兩個地方要修改,一是AopAllianceAnnotationsAuthorizingMethodInterceptor中需要過載invoke,讓它能保證在遇到RequiresData時能呼叫DataAnnotationMethodInterceptor的invoke。
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
org.apache.shiro.aop.MethodInvocation mi = createMethodInvocation(methodInvocation);
assertAuthorized(mi);
Collection<AuthorizingAnnotationMethodInterceptor> aamis = getMethodInterceptors();
if (aamis != null && !aamis.isEmpty()) {
for (AuthorizingAnnotationMethodInterceptor aami : aamis) {
if (aami.supports(mi)){
//針對DataAnnotationMethodInterceptor,有特殊的處理
if(aami instanceof DataAnnotationMethodInterceptor) {
return ((DataAnnotationMethodInterceptor)aami).invoke(mi);
}
}
}
}
//其它情況均使用系統預設
return super.invoke(mi);
}
再者需要修改DataAnnotationMethodInterceptor,同樣過載invoke方法,這是主要功能邏輯所在位置。
@SuppressWarnings("unchecked")
private Map<String,String> _addParameters(String[] props,String[] fields,Class<?> clz,Object principal) throws Exception {
Map<String,String> params = new HashMap<String,String>();
for(int i=0;i<props.length;i++){
String prop = props[i];
String field = fields[i];
int index = -1;
String[] strs = StringUtils.tokenizeToStringArray(prop, "[]");
if(strs.length>1){
prop = strs[0];
index = Integer.valueOf(strs[1]);
}
String propValue = "";
Field p = clz.getDeclaredField(prop);
if(Modifier.PRIVATE==p.getModifiers()){
String m_getter_name = "get"+StringUtils.uppercaseFirstChar(prop);
Method method = clz.getDeclaredMethod(m_getter_name);
Object ret = method.invoke(principal);
if(index>-1 && ret instanceof List<?>){
propValue = ((List<Object>)ret).get(index).toString();
}
else
propValue = ret.toString();
}
else{
Object ret = p.get(principal);
if(index>-1 && ret instanceof List<?>){
propValue = ((List<Object>)ret).get(index).toString();
}
else {
propValue = ret.toString();
}
}
System.out.println(propValue);
params.put(field, propValue);
}
return params;
}
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
// TODO Auto-generated method stub
assertAuthorized(methodInvocation);
Object obj = methodInvocation.getThis();
Object[] args = methodInvocation.getArguments();
RequiresData an = (RequiresData)this.getAnnotation(methodInvocation);
Object principal = this.getSubject().getPrincipal();
Class<?> clz = principal.getClass();
String[] props = an.props();
String[] fields = an.fields();
for(Object o : args){
if( o instanceof DataParameterRequest ){
Map<String,String> m = (Map<String,String>)((DataParameterRequest)o).getParameters();
if(m!=null){
Map<String,String> mm = this._addParameters(props, fields, clz, principal);
m.putAll(mm);
}
}
}
return methodInvocation.getMethod().invoke(obj, args);
}
大概解釋一下,在invoke中,當前登入的使用者是這個Object principal = this.getSubject().getPrincipal();
,然後取出方法引數,是個陣列,Object[] args = methodInvocation.getArguments();
找到它裡面那個DataParameterRequest型別的引數,根據註解宣告的屬性方法與Map中的欄位對應關係,新增到args中的那個DataParameterRequest中的parameters裡面去。就可以了。
注意,最後需要將args傳入methodInvocation.getMethod().invoke(obj, args);
有問題歡迎交流。