類Shiro權限校驗框架的設計和實現(2)--對復雜權限表達式的支持
前言:
我看了下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)--對復雜權限表達式的支持