根據個人上一篇文章,權限設計的雜談中,涉及到了有關於先後端分離中,頁面和api接口斷開表與表層面的關聯,另闢蹊徑從其餘角度找到方式進行關聯。這裏咱們主要採起了shiro的自定義註解的方案。本篇文章主要解決如下的問題。java
在表與表的結構關係中,頁面和接口表最終都是與權限表進行的關聯(詳情請查看個人上一篇文章《權限設計的雜談》)。
咱們如今但願用另外一種方案去替代他,實現一個低成本同時兼顧必定程度的權限控制。這裏咱們引入兩個概念。業務模塊,操做類型。數據庫
業務模塊apache
操做類型segmentfault
如今提出了這兩個概念,他們最終的實際的使用方式是什麼,咱們先從如下幾個角度去思考一下。後端
咱們從結論出發來看這幾個問題,首先「控制對象」的存儲除了在數據庫中也能夠代碼中,也能夠在配置文件中,並不必定非得在數據庫;那麼接着回答第二個問題,當數據庫存在的接口信息,而服務端並無開發這個接口的時候,數據庫的信自己就有問題,亦或者,數據庫裏新增的接口一定是服務端上已經部署的接口才能生效;接着就是第一個問題,那麼數據庫中關於「控制對象」的表中的數據並不必定是真實有效的。因此咱們能夠得出如下的解決方案api
可是這種方案僅適用於非強控制接口型的項目,在強控制型的接口項目仍然要將頁面與接口進行綁定,雖然這會帶來巨大的運維成本。另外也能夠經過接口路由規則進行劃分,例如:/api/page/xxxx/(僅對頁面使用),/api/mobile/xxxxx(僅對移動端使用)將僅供頁面使用的接口進行分類,這類接口僅作認證不作受權,也能夠達到目的。
經過一個理論上的思路承認以後,剩下的則是付諸技術上的實踐,咱們這邊採用的是Apache Shiro的安全框架,在Spring Boot的環境下應用。簡要說明如下幾個shiro的註解。安全
註解名 | 做用 |
---|---|
@RequiresAuthentication | 做用於的類、方法、實例上。調用時,當前的subject是必須通過了認證的。 |
@RequiresGuest | 做用於的類、方法、實例上。調用時,subject能夠是guest狀態。 |
@RequiresPermissions | 做用於的類、方法、實例上。調用時,須要判斷suject中是否包含當前接口中的Permission(權限信息)。 |
@RequiresRoles | 做用於的類、方法、實例上。調用時,須要判斷subject中是否包含當前接口中的Role(角色信息)。 |
@RequiresUser | 做用於的類、方法、實例上。調用時,須要判斷subject中是否當前應用中的用戶。 |
/** * 1.當前接口須要通過"認證"過程 * @return */ @RequestMapping(value = "/info",method = RequestMethod.GET) @RequiresAuthentication public String test(){ return "恭喜你,拿到了參數信息"; } /** * 2.1.當前接口須要通過權限校驗(需包含 角色的查詢 或 菜單的查詢) * @return */ @RequestMapping(value = "/info",method = RequestMethod.GET) @RequiresPermissions(value={"role:search","menu":"search"},logical=Logical.OR) public String test(){ return "恭喜你,拿到了參數信息"; } /** * 2.2.當前接口須要通過權限校驗(需包含 角色的查詢 與 菜單的查詢) * @return */ @RequestMapping(value = "/info",method = RequestMethod.GET) @RequiresPermissions(value={"role:search","menu":"search"},logical=Logical.OR) public String test(){ return "恭喜你,拿到了參數信息"; } /** * 3.1.當前接口須要通過角色校驗(需包含admin的角色) * @return */ @RequestMapping(value = "/info",method = RequestMethod.GET) @RequiresRoles(value={"admin"}) public String test(){ return "恭喜你,拿到了參數信息"; } /** * 3.2.當前接口須要通過角色與權限的校驗(需包含admin的角色,以及角色的查詢 或 菜單的查詢) * @return */ @RequestMapping(value = "/info",method = RequestMethod.GET) @RequiresRoles(value={"admin"}) @RequiresPermissions(value={"role:search","menu":"search"},logical=Logical.OR) public String test(){ return "恭喜你,拿到了參數信息"; }
在咱們的實際使用過程當中,實際上只須要使用@RequiresPermissions和@RequiresAuthentication就能夠了這一個註解就能夠了,在上一小節的結尾,咱們採起了業務模塊與操做的結合方案來解耦頁面和api接口的關係,和apache Shiro的這種方式正好一致。可是@RequiresRoles這個咱們儘量不採用,由於角色的組合形式太多,角色名沒有辦法在接口中具象惟一化(很難指定接口歸某個角色調用,可是必定能知道接口歸屬於某些業務模塊的某些操做。)app
如今咱們來回顧一下整個運轉的流程。框架
可是僅僅是擁有shiro中的這5個註解確定是不夠使用的。在實際的使用過程當中,根據需求,咱們會在權限認證中加入咱們本身特有的業務邏輯的,咱們爲了便捷則能夠採用自定義註解的方式進行使用。這種方法不只僅適用於Apache Shiro,不少其餘的框架如:Hibernate Validator、SpringMVC、甚至咱們能夠寫一套校驗體系,在aop中去驗證權限,這都是沒問題的。因此自定義註解的做用很廣。可是在這裏,我僅僅基於shiro的來實現適用於它的自定義註解。前後端分離
/** * 用於認證的接口的註解,組合形式默認是「或」的關係 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Auth { /** * 業務模塊 * @return */ String[] module(); /** * 操做類型 */ String[] action(); }
/** * Auth註解的操做類 */ public class AuthHandler extends AuthorizingAnnotationHandler { public AuthHandler() { //寫入註解 super(Auth.class); } @Override public void assertAuthorized(Annotation a) throws AuthorizationException { if (a instanceof Auth) { Auth annotation = (Auth) a; String[] module = annotation.module(); String[] action = annotation.action(); //1.獲取當前主題 Subject subject = this.getSubject(); //2.驗證是否包含當前接口的權限有一個經過則經過 boolean hasAtLeastOnePermission = false; for(String m:module){ for(String ac:action){ //使用hutool的字符串工具類 String permission = StrFormatter.format("{}:{}",m,ac); if(subject.isPermitted(permission)){ hasAtLeastOnePermission=true; break; } } } if(!hasAtLeastOnePermission){ throw new AuthorizationException("沒有訪問此接口的權限"); } } } }
/** * 攔截器 */ public class AuthMethodInterceptor extends AuthorizingAnnotationMethodInterceptor { public AuthMethodInterceptor() { super(new AuthHandler()); } public AuthMethodInterceptor(AnnotationResolver resolver) { super(new AuthHandler(), resolver); } @Override public void assertAuthorized(MethodInvocation mi) throws AuthorizationException { // 驗證權限 try { ((AuthHandler) this.getHandler()).assertAuthorized(getAnnotation(mi)); } catch (AuthorizationException ae) { if (ae.getCause() == null) { ae.initCause(new AuthorizationException("當前的方法沒有經過鑑權: " + mi.getMethod())); } throw ae; } } }
/** * shiro的aop切面 */ public class AuthAopInterceptor extends AopAllianceAnnotationsAuthorizingMethodInterceptor { public AuthAopInterceptor() { super(); // 添加自定義的註解攔截器 this.methodInterceptors.add(new AuthMethodInterceptor(new SpringAnnotationResolver())); } }
/** * 啓動自定義註解 */ public class ShiroAdvisor extends AuthorizationAttributeSourceAdvisor { public ShiroAdvisor() { // 這裏能夠添加多個 setAdvice(new AuthAopInterceptor()); } @SuppressWarnings({"unchecked"}) @Override public boolean matches(Method method, Class targetClass) { Method m = method; if (targetClass != null) { try { m = targetClass.getMethod(m.getName(), m.getParameterTypes()); return this.isFrameAnnotation(m); } catch (NoSuchMethodException ignored) { } } return super.matches(method, targetClass); } private boolean isFrameAnnotation(Method method) { return null != AnnotationUtils.findAnnotation(method, Auth.class); } }
整體的思路順序:定義 註解類(定義業務可以使用的變量)->定義 註解處理類(經過註解中的變量作業務邏輯處理)->定義 註解的攔截器->定義 aop的切面類->最後定義shiro的自定義註解啓用類。其餘的自定義的註解的編寫思路和這個也是相似的。