1. 程式人生 > >Shiro介紹(八):資料許可權的研究@RequiresData

Shiro介紹(八):資料許可權的研究@RequiresData

Shiro幫我們實現的大多為操作許可權,那麼今天我想分享一個數據許可權的方案,主要採用的仍是註解+切面攔截。

思路大概是這樣的:

  1. 在controller的方法引數,約定包含一個Map型別的parameters
  2. 通過註解宣告一下當前使用者的某個成員屬性值需要被插入到這個parameters中,並且宣告對應的欄位名稱
  3. 在方法體中,就可以將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);

有問題歡迎交流。