買單俠任浩軍:微服務API級權限的技術架構

背景:權限是根據系統設置的安全規則或者安全策略,用戶可以訪問而且只能訪問自己被授權的資源。在實際的生產系統中,用戶數量十分龐大,權限的劃分需要結合具體的業務場景,一旦把控不住力度,工作十分繁重,如何解決這個問題?


例如,對於一個文件系統的權限來說,用戶A和B只具有查看和拷貝該文件系統下某些文件的權限,而用戶C和D不僅有查看和拷貝文件的權限,也具有修改和刪除文件的權限,這些權限的劃分和授權需要事先通過專門管理員進行操作。


業界專門提出了一套權限模型和方法,RBAC(Role-Based Access Control),即基於角色(Role)的訪問控制方法。它的核心概念如下:


角色(Role)與權限(Permission)相關聯,一個角色對應多個權限


用戶(User)與角色(Role)相關聯,一個用戶對應多個角色


權限(Permission)包含資源,或者與操作組合方式相結合


這樣,我們就實現了讓用戶通過成爲適當角色的成員而得到這些角色的權限,最終實現權限控制的目的。


結合上述的文件系統例子,我們可以去用RBAC去刻畫和描述:


文件系統中的文件是權限概念中的“資源”,對文件的刪除是“操作”,那麼我們可以定義出一個“文件刪除”的權限來。


訪問文件系統的用戶A、B、C和D即上述模型中的用戶,當然如果用戶很多,我們可以劃分出“用戶組”概念。


對於角色,我們可以劃分出兩個角色,第一個是“文件普通用戶”角色,它包含“文件查看”和“文件拷貝”兩個權限;第二個是“文件管理員用戶”角色,它包含“文件修改”和“文件刪除”兩個權限。


一言以蔽之,基於角色的訪問控制方法的訪問邏輯表達式爲“Who對What(Which)進行How的操作”,它的由內到外的邏輯結構爲權限->角色->用戶,即一個角色對應綁定多個權限,一個用戶對應綁定多個角色。


從圖中可以看到,用戶A和B賦予了“文件普通用戶”角色,即他們擁有了“文件查看”和“文件拷貝”的權限;


用戶C和D同時賦予了“文件普通用戶”和“文件管理員用戶”的兩個角色,即他們擁有了“文件查看”、“文件拷貝”、“文件修改”和“文件刪除”。


如果後面,我們覺得“文件拷貝”有文件泄密的安全問題,那麼只需要從它從“文件普通用戶”角色移除就可以了,上述4個用戶自然無法實行對文件拷貝的這個操作了,所以RBAC模型對於權限擴展和收縮非常方便。

 

闡述完權限系統的基本概念後,我們來講講,權限系統在互聯網時代的分佈式系統中,尤其是微服務架構的體系下,有什麼樣的挑戰?它又必須解決哪些問題,最適合採用什麼框架和技術去解決這些問題?



服務實例數量龐大


目前,組成買單俠業務線系統,有將近400多個微服務,我們知道微服務的優點是可以清晰的劃分出業務邏輯來,讓每個微服務承擔職責單一的功能,畢竟越簡單的東西越穩定。


但是,微服務也帶來了很多的問題,完成一個業務操作,需要跨很多個微服務的調用,那麼如何用權限系統去控制用戶對不同微服務的調用,對我們來說,是個挑戰。


用戶系統數量多類型複雜


目前,接入買單俠業務線的用戶系統數量多類型複雜,且數據分散,比如有公司的員工系統(LDAP系統),公司的銷售人員系統,公司的外包人員系統,外部互聯網用戶系統(使用APP的客戶)。


不同類型的用戶系統都有可能接入某些微服務,那麼如何用權限系統去控制不通用戶對同一個微服務的調用,對我們來說,又是一個挑戰。


微服務吞吐量大 可用性要求高


當業務微服務的調用接入權限系統後,不能拖累它們的吞吐量,當權限系統出現問題後,不能阻塞它們的業務調用進度,當然更不能改變業務邏輯。


已有業務系統快速接入權限


新的業務微服務快速接入權限系統相對容易把控,那麼對於公司已有的微服務,如何能不改動它們的架構方式的前提下,快速接入,對我們來說,也是一大挑戰。


經過不斷的業界框架的選型對比,原本想採用Spring Security框架來做我們的權限框架。


但是經過研究,它有很多優勢,但也有明顯的兩個不足:框架笨重,權限數據持久化層次結構不好,最重要的是無法做數據庫持久化。


於是我們決定自研開發,技術方案有幾個特點:


 權限系統微服務化


既然有那麼多微服務,那麼我們把權限系統也微服務化,通過微服務來控制其他微服務的權限,保證整體系統架構的一致性。

 

 微服務的統一性和獨立性


未來買單俠所有的業務微服務都將接入到權限微服務中,做統一控制。


權限微服務即爲獨立的公共服務,作爲衆多微服務中的一員,它必定將遵循買單俠微服務架構的線路,即業務微服務的權限驗證,要走阿里雲SLB->Zuul API Gateway,如果是基於Web的權限驗證,還需要套入Ngnix REST請求代理。


  業務微服務的代碼微侵入


我們將採用自定義權限的註解(Annotation),儘可能增加新的代碼到業務代碼層面,減輕業務線的負擔。

 

高可用,分佈式的權限緩存


基本權限/角色數據我們通過MySQL的數據中,權限驗證數據,則通過Redis集羣緩存。


那麼意味着,對於足夠多的權限驗證數據緩存Redis集羣后,權限微服務全部崩潰也沒關係;反之,當Redis集羣崩潰,只要權限微服務運行正常,也不影響權限驗證,只是性能會稍差而已。

 

支持多類型權限,多調用方式


我們將支持業務服務通過RPC方式進行權限驗證,支持其他系統(例如Web)通過REST方式進行權限驗證。


對於業務服務的,主要是支持接口加註解進行權限攔截驗證,即API權限;對於其他系統,一般主要是體現在界面元素的權限校驗(例如Web頁面上按鈕的Enabled/Disabled,通過權限系統來控制),即界面權限。

 

友好多維的權限/角色/用戶

錄入和綁定界面



 權限數據導入導出


結合Spring OAuth單點登錄,Spring Session等,實現安全體系範疇的權限擴展。

 

接下去,我們具體來闡述,權限微服務的核心技術方案,基於界面的權限控制相對容易,就略過了,主要講一下基於API權限的實現。


對於API權限,我們實現基於註解(Annotation)的掃描入庫和攔截,不需要業務服務自行在權限Web界面上錄入。


權限定義

API權限以每個接口或者實現類中的方法作爲權限資源,每個權限和微服務名(Service Name)掛鉤。

 

我們通過在業務服務的API上添加註解的方式,進行權限定義。基礎架構部會提供一個權限組件(Permission Component)Jar給業務服務部門,裏面包含了自定義的註解,這樣的實現方式,對業務服務的影響非常小,增加權限機制只是在代碼層面加幾個註解而已。具體使用方式如下。


     對於一個普通的接口類,可以這樣定義:



@Group(name = "User Permission Group", label ="用戶權限組", description = "用戶權限組")

public interface UserService {

    @Permission(name = "AddUser", label = "添加用戶")

    boolean addUser(@UserId StringuserId, @UserType String userType, User user);

}


對於通過Swagger方式暴露出去的API,可以這樣定義:


@Path("/user")

@Consumes(MediaType.APPLICATION_JSON)

@Produces(MediaType.APPLICATION_JSON)

@Api(value = "User resource operations")

@Group(name = "User Permission Group", label ="用戶權限組", description = "用戶權限組")

public interface UserService {

    @POST

    @Path("/addUser/{userId}/{userType}")

    @Permission(name = "AddUser", label = "添加用戶")

    booleanaddUser(@PathParam("userId") @UserId String userId, @PathParam("userType")@UserType String userType, User user);

}


在上述簡短的代碼中,我們可以發現有四個自定義的註解,@Group、@Permission、@UserId和@UserType。


@Permission,即爲每個API(接口方法)定義一個權限,要求有name(英文格式),label(中文格式)和description(權限描述)。


@Group,即定義的權限歸屬哪個權限組,考慮到一個接口中包含很多個API,接口數目又比較多,那麼我們可以爲每個接口下的所有方法歸爲一個組。業務服務可自行定義權限組,也可以選擇不定義,那麼會歸屬到默認預定義的權限組中。


@UserId,即業務服務需要在他們的API上加入用戶ID的參數,當AOP切面攔截做權限驗證時候,用戶ID是需要傳入的必要參數。


@UserType,即業務服務需要在他們的API上加入用戶類型的參數,當AOP切面攔截做權限驗證時候,用戶類型是需要傳入的必要參數。用戶類型可以讓同一個業務服務支持多個用戶系統。


權限入庫和攔截


當API權限定義好以後,我們在權限組件裏面加入掃描權限入庫和攔截的算法。採用Spring AutoProxy自動代理的框架來實現我們的掃描算法。


實現Object invoke(MethodInvocation invocation)方法,獲取註解值。


根據不同註解進行不同的切面攔截,實現對@Group,@Permission、@UserId和@UserType四個註解的權限攔截邏輯


創建PermissionAutoProxy.java,繼承Spring的org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator類,步驟如下:


在構造方法裏設置好Interceptor通用代理器(即實現了MethodInterceptor接口的攔截類PermissionInterceptor.java)


shouldProxyTargetClass用來決定是接口代理,還是類代理。在權限定義的時候,其實我們還支持把註解加在實現類上,而不僅僅在接口上,這樣靈活運用註解放置的方式。


getAdvicesAndAdvisorsForBean是最核心的方法,用來決定哪個類、哪個方法上的註解要被掃描入庫,也決定哪個類、哪個方法要被代理。


如果我們做的更加通用一點,那麼可以抽象出三個方法,供getAdvicesAndAdvisorsForBean調用。


// 返回攔截類,攔截類必須實現MethodInterceptor接口,即PermissionInterceptor

protected abstract Class<? extendsMethodInterceptor> getInterceptorClass();

// 返回接口或者類的方法名上的註解,如果接口或者類中方法名上存在該註解,即認爲該接口或者類需要被代理

protected abstract Class<? extendsAnnotation> getMethodAnnotationClass();

 

// 掃描到接口或者類的方法名上的註解後,所要做的處理

protected abstract voidmethodAnnotationScanned(Class<?> targetClass);



創建PermissionScanListener.java,實現Spring的org.springframework.context.ApplicationListener.ApplicationListener<ContextRefreshedEvent>接口,步驟如下


onApplicationEvent(ContextRefreshedEvent event)方法裏實現入庫代碼。


在業務服務的Spring容器啓動的時候,將自動觸發權限數據入庫的事件。

 

通過上述闡述,我們就實現了權限的掃描入庫和攔截。


角色是一組API權限的彙總,每個角色也將和微服務名掛鉤。角色組的作用是爲了彙總和管理衆多的角色。


角色管理需要人工在界面上進行操作,角色管理分爲角色組增刪改查,以及每個角色組下的角色增刪改查。

權限不能直接和用戶綁定,必須通過角色作爲中間橋樑進行關聯。那麼我們要實現:


角色與權限的綁定,即一個角色和多個權限的關聯


用戶與角色的綁定,即一個用戶和多個角色的關聯


通過遠程RPC方式的調用,即通過權限API的方式注入,進行遠程調用。

 

Rest調用的驗證方式

http://host:port/authorization/authorize/{userId}/{userType}/{permissionName}/{PermissionType}/{serviceName}


通過User ID、User Type、Permission Name(權限名,映射於對應的方法名)、Permission Type(區別是API權限還是界面權限),Service Name(應用名)來判斷是否被授權,返回結果是true或者false。


用戶服務即整合了LDAP系統的用戶和橋接業務用戶系統。


權限服務接入用戶服務後,可以在權限授權頁面上選取相應的用戶進行權限授權。


由於權限服務屬於公共服務,它提供面向買單俠所有業務服務的權限接入,所以承受的性能壓力會很大,我們通過運用Redis分佈式緩存系統緩存已經驗證過的權限。


但其中需要注意一個策略,當跟某個用戶有關的角色,權限添加刪除,或者所屬的綁定關係發生變更的時候,需要讓緩存中的權限數據失效和刪除。


存在的問題:


由於接入權限服務的業務微服務數量還不夠多,隨着後期接入數量的增加,可能會有更多問題暴露出來,比如高併發要求、低延遲要求等等。


業務權限的API上都要加User ID和User Type兩個參數,給他們帶來些許的不適,期望未來的版本能通過前置埋點的方式解決。


未來的規劃,我們會考慮通過多種機制實現服務級別的訪問控制:


黑/白IP名單機制。當A服務調用B服務的時候,B服務會實現維護一個黑/白IP列表,表示B服務只允許在某個IP網段的A服務纔能有權限調用B服務。


服務間約定SecretKey實現安全訪問。當A服務調用B服務的時候,兩個服務之間實現約定API訪問密鑰,此密鑰不能輕易泄密。這樣就規避了B服務被模擬Rest請求調用(例如通過PostMan調用)。


服務的API簽名。當A服務調用B服務的時候,A服務需要獲得正確的B服務API的簽名,纔有權限去調用。


作者任浩軍

現任上海秦蒼(買單俠)信息科技有限公司基礎架構部架構師,畢業於浙江大學,加入秦蒼之前,曾在朗訊、惠普、泰克、快錢司任職。


專注於Java Core 、基於NIO(Netty)的分佈式RPC框架、MQ、Spring Cloud等相關領域,積累了豐富的互聯網架構設計經驗,具有豐富的開源項目和精神。(https://github.com/Nepxion/)


目前主要負責秦蒼公共服務,安全領域相關技術。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章