根據個人上一篇文章,權限設計的雜談中,涉及到了有關於先後端分離中,頁面和api接口斷開表與表層面的關聯,另闢蹊徑從其餘角度找到方式進行關聯。這裏咱們主要採起了shiro的自定義註解的方案。本篇文章主要解決如下的問題。java
在表與表的結構關係中,頁面和接口表最終都是與權限表進行的關聯(詳情請查看個人上一篇文章《權限設計的雜談》)。 數據庫
咱們如今但願用另外一種方案去替代他,實現一個低成本同時兼顧必定程度的權限控制。這裏咱們引入兩個概念。 業務模塊, 操做類型。如今提出了這兩個概念,他們最終的實際的使用方式是什麼,咱們先從如下幾個角度去思考一下。apache
咱們從結論出發來看這幾個問題,首先「控制對象」的存儲除了在數據庫中也能夠代碼中,也能夠在配置文件中,並不必定非得在數據庫;那麼接着回答第二個問題,當數據庫存在的接口信息,而服務端並無開發這個接口的時候,數據庫的信自己就有問題,亦或者,數據庫裏新增的接口一定是服務端上已經部署的接口才能生效;接着就是第一個問題,那麼數據庫中關於「控制對象」的表中的數據並不必定是真實有效的。因此咱們能夠得出如下的解決方案後端
可是這種方案僅適用於非強控制接口型的項目,在強控制型的接口項目仍然要將頁面與接口進行綁定,雖然這會帶來巨大的運維成本。另外也能夠經過接口路由規則進行劃分,例如:/api/page/xxxx/(僅對頁面使用),/api/mobile/xxxxx(僅對移動端使用)將僅供頁面使用的接口進行分類,這類接口僅作認證不作受權,也能夠達到目的。api
經過一個理論上的思路承認以後,剩下的則是付諸技術上的實踐,咱們這邊採用的是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的自定義註解啓用類。其餘的自定義的註解的編寫思路和這個也是相似的。運維