1. 程式人生 > >類Shiro權限校驗框架的設計和實現(2)--對復雜權限表達式的支持

類Shiro權限校驗框架的設計和實現(2)--對復雜權限表達式的支持

treemap evel 註冊 st3 builder row blog array www.

前言:
  我看了下shiro好像默認不支持復雜表達式的權限校驗, 它需要開發者自己去做些功能擴展的工作. 針對這個問題, 同時也會為了彌補上一篇文章提到的支持復雜表示需求, 特地嘗試寫一下解決方法.
  本文主要借助groovy腳本來實現復雜表達式的計算, 其思想是借鑒了Oval支持復雜表達式(groovy/javascript/ruby)的實現方式.

文章系列:
  1. springmvc簡單集成shiro
  2. 類Shiro權限校驗框架的設計和實現
  3. 權限系統(RBAC)的數據模型設計

目標設定:
  引入註解@MyEvaluateExpression, 其支持Groovy語法的布爾表達式(&&, ||, !), 用於復雜的權限計算評估.
  表達式內部定義了兩個函數:

// *) 判斷是否擁有該角色
boolean hasRole(String role);
// *) 判斷是否擁有該權限	
boolean hasPermission(String permission); 

  舉例如下:
  case 1:

@MyEvaluateExpression(expr="hasRole(‘developer‘) || hasPermission(‘blog:write‘)")

  表示了要麽角色為developer, 要麽該 用戶有blog的寫權限, 這個接口數據就能訪問.
  case 2:

@MyEvaluateExpression(expr="!hasRole(‘developer‘) && hasPermission(‘blog:write‘)")

  表示了要麽角色不能是developer, 同時該用戶要有blog的寫權限, 這個接口數據才能訪問.

實現:
  目標設定了, 選型也明確了, 那一切就好辦了, ^_^.
  對自定義函數hasRole, hasPermission, 其實是個偽函數, 這個表達式在使用groovy執行引擎執行前, 會被代碼替換.
  比如表達式: hasRole(‘developer‘) || hasPermission(‘blog:write‘)
  會被替換為: rolesSet.contains(‘developer‘) || permissionSet.contains(‘blog:write‘)
  其中rolesSet/permissionSet分別是角色/權限集合的set變量, 它們是外部註入腳本的bind變量.
  然後在groovy引擎中執行時, 就很容易了.

完整代碼:
  參考先前的一篇文章: Groovy實現代碼熱載的機制和原理.

public class MyShiroGroovyHelper {

    private static ConcurrentHashMap<String, Class<Script>> zlassMaps
            = new ConcurrentHashMap<String, Class<Script>>();

    // *) 具體執行groovy代碼
    public static Object invoke(String scriptText, Map<String, Object> params) {
        String key = fingerKey(scriptText);
        Class<Script> script = zlassMaps.get(key);
        if ( script == null ) {
            synchronized (key.intern()) {
                // Double Check
                script = zlassMaps.get(key);
                if ( script == null ) {
                    GroovyClassLoader classLoader = new GroovyClassLoader();
                    script = classLoader.parseClass(scriptText);
                    zlassMaps.put(key, script);
                }
            }
        }

        Binding binding = new Binding();
        for ( Map.Entry<String, Object> ent : params.entrySet() ) {
            binding.setVariable(ent.getKey(), ent.getValue());
        }
        Script scriptObj = InvokerHelper.createScript(script, binding);
        return scriptObj.run();

    }

    // *) 為腳本代碼生成md5指紋
    private static String fingerKey(String scriptText) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] bytes = md.digest(scriptText.getBytes("utf-8"));

            final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
            StringBuilder ret = new StringBuilder(bytes.length * 2);
            for (int i=0; i<bytes.length; i++) {
                ret.append(HEX_DIGITS[(bytes[i] >> 4) & 0x0f]);
                ret.append(HEX_DIGITS[bytes[i] & 0x0f]);
            }
            return ret.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

  在類Shiro權限校驗框架的設計和實現, 繼續做擴充
  定義註解@MyEvaluateExpression:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyEvaluateExpression {
    String expr();
}

  擴展MyShiroHelper類

public class MyShiroHelper {

    private static final String MY_SHIRO_AUTHRIZE_KEY = "my_shiro_authorize_key";

	// *) 評估復雜表達式
    public static boolean validateExpression(String expr) throws Exception {
        if ( expr == null ) {
            throw new Exception("invalid expression");
        }
        try {
            HttpSession session = getSession();
            MyAuthorizeInfo authorizeInfo = (MyAuthorizeInfo)session
                    .getAttribute(MY_SHIRO_AUTHRIZE_KEY);
            if ( authorizeInfo == null ) {
                return false;
            }
            String scriptText = expr.replaceAll("hasRole", "roleSet.contains");
            scriptText = scriptText.replaceAll("hasPermission", "permissionSet.contains");

            Map<String, Object> params = new TreeMap<String, Object>();
            params.put("roleSet", authorizeInfo.getRoles());
            params.put("permissionSet", authorizeInfo.getPermissions());
            return (Boolean) MyShiroGroovyHelper.invoke(scriptText, params);
        } catch (Exception e) {
            throw new Exception("permission invalid state");
        } finally {
        }
    }

	private static HttpSession getSession() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .getRequest();
        return request.getSession();
    }

}

  擴展MyShiroAdvice類

@Aspect
@Component
public class MyShiroAdvice {

    /**
     * 定義切點
     */
    @Pointcut("@annotation(com.springapp.mvc.myshiro.MyEvaluateExpression)")
    public void checkExprs() {
    }

    @Before("checkExprs()")
    public void doCheckExprs(JoinPoint jp) throws Exception {

        // *) 獲取對應的註解
        MyEvaluateExpression mrp = extractAnnotation(
                (MethodInvocationProceedingJoinPoint)jp,
                MyEvaluateExpression.class
        );

        try {
            if ( !MyShiroHelper.validateExpression(mrp.expr()) ) {
                throw new Exception("access disallowed");
            }
        } catch (Exception e) {
            throw new Exception("invalid state");
        }

    }

    // *) 獲取註解信息
    private static <T extends Annotation> T extractAnnotation(
            MethodInvocationProceedingJoinPoint mp, Class<T> clazz) throws Exception {

        Field proxy = mp.getClass().getDeclaredField("methodInvocation");
        proxy.setAccessible(true);

        ReflectiveMethodInvocation rmi = (ReflectiveMethodInvocation) proxy.get(mp);
        Method method = rmi.getMethod();

        return (T) method.getAnnotation(clazz);
    }

}

  

測試代碼:
  在之前的Controller類上添加方法.

@RestController
@RequestMapping("/")
public class HelloController {

	@RequestMapping(value="/login", method={RequestMethod.POST, RequestMethod.GET})
	public String login() {
		// 1) 完成登陸驗證
		// TODO

		// 2) 查詢權限信息
		// TODO

		// 3) 註冊權限信息
		MyAuthorizeInfo authorizeInfo = new MyAuthorizeInfo();
		authorizeInfo.addRole("admin");

		authorizeInfo.addPermission("blog:write");
		authorizeInfo.addPermission("blog:read");

		// *) 授權
		MyShiroHelper.authorize(authorizeInfo);
		return "ok";
	}

	@RequestMapping(value="/test3", method={RequestMethod.GET, RequestMethod.POST})
	@MyEvaluateExpression(expr="hasRole(‘admin‘) && !hasPermission(‘blog:write‘)")
	public String test3() {
		return "test3";
	}

}

  測試符合預期.

總結:
  本文利用了Aspectj和Groovy, 實現了一個簡易的支持復雜表達式權限驗證的功能. 可能還不是特別完善, 但是對於小業務而言, 還是能夠滿足需求的, ^_^.

  

類Shiro權限校驗框架的設計和實現(2)--對復雜權限表達式的支持