這裡我們主要採取了shiro的自訂註解的方案。本篇文章主要解決以下的問題。
如何透過邏輯進行頁面與api介面的關聯。
shiro的自身註解的用法。
如何寫自訂註解。
在表格與表格的結構關係中,頁面和介面表最終都是與權限表進行的關聯(詳情請查看我的上一篇文章《權限設計的雜談》)。
我們現在希望用另一個方案去取代他,實現一個低成本同時兼顧一定程度的權限控制。這裡我們引入兩個概念。 業務模組,作業類型。
業務模組
#概念:將系統中的業務模組抽象化成一種數據,我們可以用字串的形式去表示,例如:角色管理對應是role-manage、使用者管理對應是user-manage等等。我們將系統中所存在的業務模組透過「最小特權原則」來劃分,最終形成一批可分配的資料。
使用原則:api介面與頁面以及功能本質上來說,都和業務模組有邏輯關係,於是,我們可以對api介面與頁面(以及功能點)進行邏輯匹配,來判斷頁面與介面的關係。
操作類型
#概念:將系統中的所有的運算類型抽象化成一種數據,我們也可以用字串的形式去表示,例如:新增對應的是add、分配對應的是allot等等。我們將系統中所有的操作類型根據業務模組透過「資料許可證」進行劃分,最終形成一批可分配的資料。
使用原則:頁面是展示,功能點是動作,而介面是最終動作的資源提供,透過「業務模組」確定了調取的資源,透過「操作類型」確定了資源的使用方式。透過兩者可以大致無誤的判斷頁面的功能點觸發的介面是否在鑑權之內。
現在提出了這兩個概念,他們最終的實際的使用方式是什麼,我們先從以下幾個角度去思考一下。
資料庫中的頁表或的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這個我們盡可能不採用,因為角色的組合形式太多,角色名沒有辦法在接口中具象唯一化(很難指定接口歸某個角色調用,但是一定能知道接口歸屬於某些業務模組的某些操作。)
現在我們來回顧整個運作的流程。
但是僅僅是擁有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("没有访问此接口的权限"); } } } }
定義shiro攔截處理類別
/** * 拦截器 */ 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切面類別
/** * shiro的aop切面 */ public class AuthAopInterceptor extends AopAllianceAnnotationsAuthorizingMethodInterceptor { public AuthAopInterceptor() { super(); // 添加自定义的注解拦截器 this.methodInterceptors.add(new AuthMethodInterceptor(new SpringAnnotationResolver())); } }
定義shiro的自訂註解啟動類別
/** * 启动自定义注解 */ 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的自訂註解啟用類別。其他的自訂的註解的編寫思路和這個也是類似的。
相關推薦:
以上是基於shiro的自訂註解的擴展-圖文詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!