基於shiro的自訂註解的擴充
這裡我們主要採取了shiro的自訂註解的方案。本篇文章主要解決以下的問題。
如何通過邏輯進行頁面與api介面的關聯。
shiro的自身註解的用法。
如何編寫自訂註解。
如何通過邏輯進行頁面與api介面的關聯
在表與表的結構關係中,頁面和介面表最終都是與許可權表進行的關聯(詳情請查看我的上一篇文章《許可權設計的雜談》)。
我們現在希望用另一種方案去替代他,實現一個低成本同時兼顧一定程度的許可權控制。這裡我們引入兩個概念。業務模組,操作類型。
業務模組
操作類型
概念:將系統中的所有的操作類型抽象成一種資料,我們也可以用字串的形式去表示,例如:新增對應的是add、分配對應的是allot等等。我們將系統中所有的操作類型根據業務模組通過“資料許可證”進行劃分,最終形成一批可分配的資料。
使用原則:頁面是展示,功能點是動作,而介面是最終動作的資源提供,通過“業務模組”確定了調取的資源,通過“操作類型”確定了資源的使用方式。通過兩者可以大致無誤的判斷頁面的功能點觸發的介面是否在鑒權之內。
現在提出了這兩個概念,他們最終的實際的使用方式是什麼,我們先從以下幾個角度去思考一下。
資料庫中的頁面表或的api介面表中的資料就是真實有效嗎?
頁面或介面的實際使用,是以功能存在為前提,還是以資料庫表中的資料存在為前提。
許可權結構中,“控制對象”的儲存只有資料庫這一種途徑嗎?
我們從結論出發來看這幾個問題,首先“控制對象”的儲存除了在資料庫中也可以代碼中,也可以在設定檔中,並不一定非得在資料庫;那麼接著回答第二個問題,當資料庫存在的介面資訊,而服務端並沒有開發這個介面的時候,資料庫的信本身就有問題,亦或者,資料庫裡新增的介面必定是服務端上已經部署的介面才會生效;接著就是第一個問題,那麼資料庫中關於“控制對象”的表中的資料並不一定是真實有效。所以我們可以得出以下的解決方案
我們可以在介面上用註解的形式補充“業務模組”和“操作類型”的資料資訊,這兩類資訊都可以存於常量類中,
在資料庫添加建立頁面表結構和頁面功能表結構的時候,添加“業務模組”和“操作類型”欄位。
可以將“業務模組”和“操作類型”的資訊存於資料庫的字典表中。
模組的新增或操作的新增,必定帶來了介面的新增,那麼就會帶來一次系統部署活動,這個營運成本是無法減少的,並不能通過表結構來減少。
但是這種方案僅適用於非強控制介面型的項目,在強控制型的介面項目仍然要將頁面與介面進行綁定,雖然這會帶來巨大的營運成本。另外也可以通過介面路由規則進行劃分,例如:/api/page/xxxx/(僅對頁面使用),/api/mobile/xxxxx(僅對移動端使用)將僅供頁面使用的介面進行分類,這類介面僅做認證不做授權,也可以達到目的。
shiro的自身註解的用法
通過一個理論上的思路認可之後,剩下的則是付諸技術上的實踐,我們這邊採用的是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這個我們儘可能不採用,因為角色的組合形式太多,角色名稱沒有辦法在介面中具象唯一化(很難指定介面歸某個角色調用,但是一定能知道介面歸屬於某些業務模組的某些操作。)
現在我們來回顧一下整個運轉的流程。
如何編寫自訂註解
但是僅僅是擁有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的自訂註解啟用類。其他的自訂的註解的編寫思路和這個也是類似的。