單點登入和許可權認證
1、登入認證
1、Session-Cookie認證
基於Session-Cookie
機制的認證是比較原始的一種認證方式,由於HTTP
協議是純文字,無狀態的傳輸協議,那麼在一些需要記錄狀態的場景就很麻煩,如淘寶的購物車,不同使用者登入後看到的購物車資料是不一致的;所以需要一種機制能讓服務端知道請求的客戶端是誰。這樣Cookie就應運而生,在客戶端登入成功後,服務端返回的響應報文頭中會帶上一個set-cookie
,客戶端瀏覽器判斷拿到這個請求頭後會放在本地,每次訪問cookie中指定的路徑時都會帶上到請求頭中。
1、缺點
基於Session-Cookie
會帶來一系列的問題。比如:
- 服務端的
Session
都儲存在記憶體中,登入使用者過多後會對服務端造成較大的壓力 - 可能會引起
CSRF
攻擊 - 分散式系統中無法進行登入認證
2、解決方案
為解決Session
儲存在服務端導致的效能瓶頸的痛點,可以將session
資訊放入到快取或者是資料庫中進行儲存,服務端在校驗登入進來的資訊時直接從快取或者資料庫中獲取對應的資訊進行比對,業內一般將Session
資訊放入到Redis中進行儲存。
SpringSession
1 引入pom
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session</artifactId> <version>1.2.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-redis</artifactId> </dependency>
2 application.yaml配置
spring:
redis:
database: 1
host: localhost
pool:
max-active: 20
3 開啟@EnableRedisHttpSession
SpringSession原理
先從@EnableRedisHttpSession
註解類分析開始
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented @Import({RedisHttpSessionConfiguration.class}) @Configuration public @interface EnableRedisHttpSession { int maxInactiveIntervalInSeconds() default 1800; String redisNamespace() default "spring:session"; RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE; String cleanupCron() default "0 * * * * *"; }
可以看到該註解用到了Spring
的Import
來載入需要的bean
在RedisHttpSessionConfiguration
配置類中主要的功能有以下:
@Bean
public RedisOperationsSessionRepository sessionRepository() {
RedisTemplate<Object, Object> redisTemplate = this.createRedisTemplate();
RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(redisTemplate);
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
if (this.defaultRedisSerializer != null) {
sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
if (StringUtils.hasText(this.redisNamespace)) {
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
sessionRepository.setRedisFlushMode(this.redisFlushMode);
int database = this.resolveDatabase();
sessionRepository.setDatabase(database);
return sessionRepository;
}
@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository);
sessionRepositoryFilter.setServletContext(this.servletContext);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
建立一個RedisOperationsSessionRepository
物件提供給SessionRepositoryFilter
操作session的工具。
核心就在SessionRepositoryFilter
過濾器中:
@Order(-2147483598)
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
// 省略不必要的程式碼
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response, this.servletContext);
SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
} finally {
wrappedRequest.commitSession();
}
}
}
可以看到該過濾器的優先順序非常的高,那麼請求進入伺服器第一個就會來到該過濾器。該過濾器將HttpServletRequest
和HttpServletResponse
進行包裝然後開始執行下一個過濾器鏈路,其實包裝的本質也就是將HttpServletRequest
的getSession進行了加強,改為通過RedisOperationsSessionRepository
從redis
中獲取
2、Token認證
由於Session-Cookie
的諸多不便的缺點,現在大部分公司採用的技術方案是Token
。Token
的機制和Cookie
其實差不多,本質都是用來解決HTTP
協議的無狀態痛點。此外Token認證的方式還能解決CSRF
攻擊的問題。
1、認證token
一般是將使用者id和使用者名稱稱和進行雙向加密後返回給客戶端,客戶端再每次請求時都需要再請求頭中帶上Token
。服務端接收到token
後將token
發往SSO服務進行校驗,校驗通過後會獲得登入使用者的許可權和基本資訊,登入失敗後將拒絕訪問。
此方式和Session-Cookie
的區別在於客戶端的儲存方式,Cookie
是直接儲存在瀏覽器中,訪問的時候直接帶過去,在web場景中這樣是可以的,但是一旦涉及到移動端此方式就會有問題,所以對於跨平臺的介面還是採用Token
的方式相容性最好
2、JWT
JWT全名為:json web token。WT是由三段資訊構成的,將這三段資訊文字用
.
連結一起就構成了Jwt字串。就像這樣:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT構成:
- 頭部
- 載荷
- 簽證
頭部資訊header:
加密的演算法
{ 'typ': 'JWT', 'alg': 'HS256' }
載荷payload:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
載荷中有以下幾種資料:
保留資料:
iss(Issuser):代表這個JWT的簽發主體; sub(Subject):代表這個JWT的主體,即它的所有人; aud(Audience):代表這個JWT的接收物件; exp(Expiration time):是一個時間戳,代表這個JWT的過期時間; nbf(Not Before):是一個時間戳,代表這個JWT生效的開始時間,意味著在這個時間之前驗證JWT是會失敗的; iat(Issued at):是一個時間戳,代表這個JWT的簽發時間; jti(JWT ID):是JWT的唯一標識。
私有資料:
登入人員的基本資訊
簽章
Signature
Jwt
的第三部分是一個簽證資訊,這個簽證資訊由三部分組成:
- header (base64後的)
- payload (base64後的)
- secret
這個部分需要base64加密後的header和base64加密後的payload使用
.
連線組成的字串,然後通過header中宣告的加密方式進行加鹽secret
組合加密,然後就構成了jt的第三部分var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); var signature = HMACSHA256(encodedString, 'secret');
最後
JWT
總體=base64UrlEncode(header) + '.' + base64UrlEncode(payload) + '.' + signature;
JWT
和普通的Token
區別在於JWT
的載荷是存放在客戶端中的,一般的Token
都是客戶端存放一個GUID
通過GUID
去服務端中拿到對應的登入人員資訊,JWT
就可以將這一步直接省略掉。這樣做的好處的可以減少服務端計算的壓力。
缺點:
- 服務端無法登出,由於
JWT
的有效性和服務端沒有依賴關係,所以服務端也無法使JWT
失效。 JWT
無法被續簽。
解決方案
針對JWT
無法被服務端登出問題:
- 將
token
存入資料庫中,每次拿到token
後和資料庫中的token
進行比對,如果沒有找到則說明token
已經失效,但是這樣做就散失了JWT
的優勢,違背了JWT
無狀態原則 - 使用記憶體維護一個黑名單列表,需要失效則將
token
存放到黑名單中,每次認證需要先判斷是不是已經被拉黑的token
- 修改
JWT
的Secret
,不推薦。會將所有之前簽發的token
全部失效 - 保持令牌的短有效期,需要經常去簽發新的
token
針對JWT
無法續簽問題:
- 服務端發現
Token
快過期就簽發一個新的token
給客戶端。客戶端判斷介面是否傳送新的token
,如果是則用新token來替換舊的 - 將
token
有效期設定到半夜,保證大部分使用者白天都能登入,適用於安全性要求不高的系統 - 雙
token
機制:accessToken
和refreshToken
,accessToken
有效期較短如半個小時,refreshToken
有效期較長如一天。accessToken
用來獲取受限的服務資源,當accessToken
失效時,通過refreshToken
來獲取新的accessToken
3、OAuth2協議
1、名詞說明
Third-party application
:第三方應用程式,本文中又稱"客戶端"(client),比如開啟知乎,使用第三方登入,選擇qq登入,這時候知乎就是客戶端。HTTP service
:HTTP服務提供商,本文中簡稱"服務提供商",即上例的qq。Resource Owner
:資源所有者,本文中又稱"使用者"(user),即登入使用者。User Agent
:使用者代理,本文中就是指瀏覽器。Authorization server
:認證伺服器,即服務提供商專門用來處理認證的伺服器。
一般流行的使用都是OAuth2的授權碼模式,授權碼模式功能最完整,流程最嚴密。
2、授權碼模式流程
- 使用者訪問客戶端,客戶端將頁面導向認證伺服器認證頁面,使用者點選授權,認證訪問會將頁面導向到客戶端事先指定的重定向URL,同時附上一個授權碼
- 客戶端收到驗證碼,向認證服務申請token:
/oauth/token?response_type=code&client_id=test&redirect_uri=重定向頁面連結
- 認證服務核對了授權碼和重定向地址無誤後給客戶端傳送兩個令牌:
accessToken
和refreshToken
,refreshToken
擁有較長的過期時間,但accessToken
失效後,通過refreshToken
進行重新整理。
2、RBAC模型
1.3 - What ANSI RBAC is — Apache Directory
許可權系統通常基於
RBAC
(Role-Based Access Control)的思想設計,擁有4個關鍵元素:使用者 – 角色 – 許可權 – 資源。
- 資源
被安全管理的物件(
Resources
頁面、選單、按鈕、訂單等)
- 許可權
訪問和操作資源的許可(
Permit
刪除、編輯、審批等)
- 角色
我們通過業務流程確定一個角色,實際是確定角色並角色具備的那些許可權的過程,角色所以是許可權的集合,是眾多許可權顆粒組成;
我們通過把許可權給這個角色,再把角色給使用者,從而實現使用者的許可權,因此它承擔了一個橋樑的作用。
引入角色這個概念,可以幫助我們靈活的擴充套件,使一個使用者可以具備多種角色。
- 使用者
系統實際的操作員(
User
)【使用者(
user
:誰)】被賦予【角色(role
:具有1-n個許可權)】,通過角色關聯的【許可權(permit
:許可)】去訪問/操作【資源(resource
)】
RBAC
解決了什麼痛點?
在傳統模型中無角色概念,直接對使用者進行授權,這樣會導致一些問題:
- 配置許可權很麻煩
- 無法快速配置類似角色的許可權
- 使用者多身份下的角色配置很麻煩
在RBAC模型中一共分為四種:RBAC0
,RBAC1
,RBAC2
,RBAC3
,其中,RBAC1
和RBAC2
是基於RBCA0
升級衍生出來的,而RBAC3
則是融合了RBAC1
和RBAC2的優點
創造的新的模型,一般來說RBAC3
是夠用的。
1、RBAC0 基本模型
RBAC0定義了能構成RBAC控制系統的最小的元素集合,RBAC0由四部分構成:
- 使用者
- 角色
- 許可權
- 會話
當一個使用者被指定角色時,該使用者就擁有了此角色的所有許可權。RBAC0是最基礎的許可權模型,許可權設計時將許可權賦予給角色,而不是使用者,一個使用者可以擁有若干角色,從而使使用者可以獲得更廣泛的許可權。就此構造成“使用者- 角色- 許可權”的授權模型。在這種模型中,使用者與角色之間,角色與許可權之間,一般者是多對多的關係。角色和許可權繫結,使用者被賦予相應的角色,通過多對多的關係來實現授權和授權的快速變更,從而控制使用者對系統的功能使用和資料訪問許可權。
2、RBAC1 許可權繼承
RBAC1在RBAC0的基礎上增加許可權繼承的改進,為什麼要加入許可權繼承呢?
設想一下這個場景,公司招了一位管理人員進入,那麼管理人員的許可權就得是下面被管理人員的許可權的超集,如果被管理人員的角色和鏈路很長的話,那麼這個角色需要新增很多的許可權,非常的麻煩。但是引入了許可權繼承體系就會變得很簡單,管理人員的角色只需要繼承所有他下面一級角色就可以了。
角色間的繼承關係可分為一般繼承(General
)和受限繼承(Limited
)。
- 一般繼承關係:
要求角色繼承關係是一個絕對偏序關係(A角色繼承於B角色,那麼B必須是A許可權的一個子集,不可以冗餘多餘的許可權),允許角色間的多繼承。
- 受限繼承關係:
進一步要求角色繼承關係是一個樹結構,實現角色間的單繼承。受限繼承則增強了職責關係的分離
3、RBAC2 許可權約束
他是RBAC
的約束模型,RBAC3
也是在RBAC0
的基礎上構建的,在RBAC0
的基礎上增加了約束的概念,主要引入了靜態職責分離SSD(Static Separation of Duty)和動態職責分離DSD(Dynamic Separation of Duty)。
- 靜態職責分離SSD
在設定使用者角色許可權的時候就應該判斷,如果有衝突發生在設定時候就應該拒絕。靜態職責分離有以下幾種
- 角色互斥:使用者不能同時擁有兩個互斥的角色,例如會計和審計就不能由同一個使用者擁有
- 基數互斥:一個使用者能夠擁有的角色是有限的,一個角色擁有的許可權也是有限的,如:某個角色是為CEO準備的,那就不能有多個
- 先決條件角色:使用者想要獲得較高的許可權,受限需要獲得低一級的許可權,如:有副經理的許可權才能有總經理的許可權
- 動態職責分離DSD
在角色分配時可以將衝突的角色賦予給同一個使用者,但是在使用者使用系統時,一次會話中不能同時啟用兩個角色。
4、RBAC3 綜合許可權模型
也就是最全面級的許可權管理,它是基於RBAC0的基礎上,將RBAC1和RBAC2進行整合了,最前面,也最複雜的
5、表結構的設計
RBAC
最簡單的表結構設計如下:
一個使用者擁有若干角色,每一個角色擁有若干許可權。這樣,就構造成“使用者-角色-許可權”的授權模型。在這種模型中,使用者與角色之間,角色與許可權之間,一般者是多對多的關係
1、使用者組和角色組
當用戶量非常多的時候,需要用系統逐一給使用者授權是一件很繁瑣的事情,就需要使用使用者組,除了可以給使用者授權意外還可以給使用者組進行授權,然後將使用者加入到對應的使用者組中,這樣使用者擁有的許可權就等於使用者組的許可權+使用者個人角色的許可權之和。
角色組不參與權限控制,當角色量較多時,可以通過樹狀圖來展示角色,這樣方便使用者去使用。
2、資源細分
接下來可以再進一步對許可權進行拆分,如對於上傳檔案的操作,選單的訪問,頁面上的按鈕都屬於許可權控制的範圍,在設計上將功能操作分為一類,將檔案,選單,頁面內容分成資源一類,這樣可以把操作和資源進行管理,粒度更細。
全域性:
3、許可權繼承
如果需要對許可權要求更高還可以變成RBAC3
型的許可權設計,即加上許可權繼承和許可權約束。採用受限繼承式的方式完成:
只需要在角色表中新增一個父角色ID就可以,當用戶登入進入系統時,會通過使用者ID找到對應的角色集合,對集合進行一次遍歷:遞迴找到該角色對應父類的許可權並加入集合中。再對所有角色進行遍歷完成後會得到一個使用者所有的許可權。這樣受限繼承式的許可權繼承就完成了。
如果需要使用普通繼承式,則需要再新建一個表:
這樣的處理會簡單一點,當獲取到使用者的角色集合時,遍歷獲取每個角色對應的父角色ID對應的許可權,然後放入到許可權集合中。
6、其他幾種許可權模型
1、ACL模型
ACL稱之為許可權控制列表,規定資源可以被哪些主體進行哪些操作,是ACL模型的一種靈活實現
場景:部門隔離 適用資源:客戶頁面、人事頁面
在ACL
許可權模型下,許可權管理是圍繞資源來設定的。我們可以對不同部門的頁面設定可以訪問的使用者。配置形式如下:
ACL配置表
資源: 客戶頁面
主體: 銷售部(組)
操作:增刪改查
主體: 王總(使用者)
操作: 增刪改查
資源: 人事頁面
主體: 王總(組)
操作: 增刪改查
注:主體可以是使用者,也可以是組。
在維護性上,一般在粗粒度和相對靜態的情況下,比較容易維護。
在細粒度情況下,比如將不同的客戶視為不同的資源,1000個客戶就需要配置1000張ACL表。如果1000個客戶的許可權配置是有規律的,那麼就要對每種資源做相同的操作;如果許可權配置是無規律的,那麼ACL不妨也是一種恰當的解決方案。
在動態情況下,許可權經常變動,每新增一名員工,都需要配置所有他需要訪問的資源,這在頻繁變動的大型系統裡,也是很難維護的。
2、DAC模型
DAC
稱之為自主訪問控制(DAC
: Discretionary Access Control
)
系統會識別使用者,然後根據被操作物件(Subject
)的許可權控制列表(ACL: Access Control List
)或者許可權控制矩陣(ACL: Access Control Matrix
)的資訊來決定使用者的是否能對其進行哪些操作,例如讀取或修改。DAC
是ACL
的一種實現,強調靈活性。純粹的ACL,許可權由中心管理員統一分配,缺乏靈活性。為了加強靈活性,在ACL的基礎上,DAC模型將授權的權力下放,允許擁有許可權的使用者,可以自主地將許可權授予其他使用者。
而擁有物件許可權的使用者,又可以將該物件的許可權分配給其他使用者,所以稱之為自主控制。在檔案系統的設計中常用此模式,如微軟的NTFS。
DAC
的缺點在於許可權控制較於分散,不方便去管理,且也是使用者直接和許可權掛鉤。適用於許可權結構簡單,使用者型別和數量不多的場景。
3、MAC模型
MAC稱之為強制訪問控制(
MAC: Mandatory Access Control
)是ACL模型的另一種實現,主要體現在了安全性訪問許可權有兩個規則判斷
- 規定資源可以被哪些類別的主體進行哪些操作
- 規定主體可以對哪些等級的資源進行哪些操作
1和2同時滿足才可以通過許可權認證
場景:保密系統 適用資源:機密檔案
MAC
是ACL
的另一種實現,強調安全性。MAC
會在系統中,對資源與主體,都劃分類別與等級。比如,等級分為:祕密級、機密級、絕密級;類別分為:軍事人員、財務人員、行政人員。比如,一份機密級的財務檔案,可以確保只有主體的等級是機密級,且是財務人員才能訪問。如果是機密級的行政人員就無法訪問。MAC
的優勢就是實現資源與主體的雙重驗證,確保資源的交叉隔離,提高安全性。
資源配置表
資源: 財務文件
主體: 財務人員
等級:機密級
操作:檢視
主體配置表
主體: 李女士
類別: 財務人員
等級:機密級
4、ABAC模型
基於屬性的許可權驗證(
ABAC: Attribute-Based Access Control
) 規定哪些屬性的主體可以對哪些屬性的資源在哪些屬性的情況下進行哪些操作,
場景:防火牆 適用資源:埠訪問
ABAC中主要的一些引數:
- 主體屬性,主體相關資訊,如姓名,性別,職位,年齡等等
- 資源屬性,資源相關的屬性,如資源建立時間,建立位置,保密等級等等
- 情況屬性,指的是客觀情況的屬性,如當前時間,當前位置,當前場景
- 操作,增刪改查
設定一個許可權,就是定義一條含有四類屬性資訊的策略(Policy
)。
例如:
策略表
效果:允許
操作:流入
主體:來自上海IP的客戶端
資源:所有以33開頭的埠(如3306)
情況:在北京時間 9:00~18:00
效果:禁止
操作:流出
主體:任何
資源:任何
情況:任何
一個請求來到系統中,會逐條的匹配策略。匹配的規則有很多種:
- 如果沒有匹配到,則返回預設策略,拒絕或者接受
- 匹配到多個策略
- 必須完全匹配接受
- 有任意一個接受則接受
3、OpenAPI安全
介面需要開放給網際網路呼叫需要做好安全控制,不然服務會被惡意攻擊拖垮,如何做好OpenAPI,我認為有以下幾點需要:
- 安全認證
- 開發者門戶,用於開發應用的註冊和操作
- 介面熔斷降級處理,一般來說通過Hytrix
- 日誌記錄,在輸入和輸出要進行日誌記錄
首先客戶端呼叫開放介面之前需要去開發者門戶註冊一個應用賬戶,應用會得到自身的AppKey
和AppSecret
,其中AppKey
是相當於使用者名稱,AppSecret
相當於密碼。OpenApi呼叫的過程如下:
- 先通過
HTTPS
協議登入獲取Access_Token
(以下稱之為Token
)和Refresh_Token
Token
認證指客戶端請求黑名單介面時,認證中心基於Token
生成簽名
Token表結構:
介面請求:
請求引數中至少有以下幾個屬性:
- 方法名,指定呼叫哪個介面
- 業務引數,介面的請求引數用JSON字串
- token值,登入後獲取到的token
- 簽名,將AppKey,APPSecret,Token和時間戳,和業務引數進行一次雜湊演算法得到簽名值,防止請求引數被篡改
- 時間戳,服務端用來判斷請求時間是否有效作用
服務端簽名驗證的具體流程: