摘要:的安全性不好,攻擊者可以通過獲取本地進(jìn)行欺騙或者利用進(jìn)行攻擊。
好久沒寫博客了,因?yàn)樽罱疽笪覍W(xué)spring cloud ,早點(diǎn)將以前軟件遷移到新的架構(gòu)上。所以我那個(gè)拼命的學(xué)吶,總是圖快,很多關(guān)鍵的筆記沒有做好記錄,現(xiàn)在又遺忘了很多關(guān)鍵的技術(shù)點(diǎn),極其罪惡!
現(xiàn)在想一想,還是踏踏實(shí)實(shí)的走比較好。這不,今天我冒了個(gè)泡,來補(bǔ)一補(bǔ)前面我所學(xué)所忘的知識點(diǎn)。
想要解鎖更多新姿勢?請?jiān)L問我的博客。
常見的認(rèn)證機(jī)制今天我么聊一聊JWT。
關(guān)于JWT,相信很多人都已經(jīng)看過用過,他是基于json數(shù)據(jù)結(jié)構(gòu)的認(rèn)證規(guī)范,簡單的說就是驗(yàn)證用戶登沒登陸的玩意。這時(shí)候你可能回想,哎喲,不是又那個(gè)session么,分布式系統(tǒng)用redis做分布式session,那這個(gè)jwt有什么好處呢?
請聽我慢慢訴說這歷史!
最原始的辦法--HTTP BASIC AUTHHTTP BASIC auth,別看它名字那么長那么生,你就認(rèn)為這個(gè)玩意很高大上。其實(shí)原理很簡單,簡單的說就是每次請求API的時(shí)候,都會把用戶名和密碼通過restful API傳給服務(wù)端。這樣就可以實(shí)現(xiàn)一個(gè)無狀態(tài)思想,即每次HTTP請求和以前都沒有啥關(guān)系,只是獲取目標(biāo)URI,得到目標(biāo)內(nèi)容之后,這次連接就被殺死,沒有任何痕跡。你可別一聽無狀態(tài),正是現(xiàn)在的熱門思想,就覺得很厲害。其實(shí)他的缺點(diǎn)還是又的,我們通過http請求發(fā)送給服務(wù)端的時(shí)候,很有可能將我們的用戶名密碼直接暴漏給第三方客戶端,風(fēng)險(xiǎn)特別大,因此生產(chǎn)環(huán)境下用這個(gè)方法很少。
Session和cookiesession和cookie老生常談了。開始時(shí),都會在服務(wù)端全局創(chuàng)建session對象,session對象保存著各種關(guān)鍵信息,同時(shí)向客戶端發(fā)送一組sessionId,成為一個(gè)cookie對象保存在瀏覽器中。
當(dāng)認(rèn)證時(shí),cookie的數(shù)據(jù)會傳入服務(wù)端與session進(jìn)行匹配,進(jìn)而進(jìn)行數(shù)據(jù)認(rèn)證。
此時(shí),實(shí)現(xiàn)的是一個(gè)有狀態(tài)的思想,即該服務(wù)的實(shí)例可以將一部分?jǐn)?shù)據(jù)隨時(shí)進(jìn)行備份,并且在創(chuàng)建一個(gè)新的有狀態(tài)服務(wù)時(shí),可以通過備份恢復(fù)這些數(shù)據(jù),以達(dá)到數(shù)據(jù)持久化的目的。
缺點(diǎn)這種認(rèn)證方法基本是現(xiàn)在軟件最常用的方法了,它有一些自己的缺點(diǎn):
安全性。cookies的安全性不好,攻擊者可以通過獲取本地cookies進(jìn)行欺騙或者利用cookies進(jìn)行CSRF攻擊。
跨域問題。使用cookies時(shí),在多個(gè)域名下,會存在跨域問題。
有狀態(tài)。session在一定的時(shí)間里,需要存放在服務(wù)端,因此當(dāng)擁有大量用戶時(shí),也會大幅度降低服務(wù)端的性能。
狀態(tài)問題。當(dāng)有多臺機(jī)器時(shí),如何共享session也會是一個(gè)問題,也就是說,用戶第一個(gè)訪問的時(shí)候是服務(wù)器A,而第二個(gè)請求被轉(zhuǎn)發(fā)給了服務(wù)器B,那服務(wù)器B如何得知其狀態(tài)。
移動手機(jī)問題。現(xiàn)在的智能手機(jī),包括安卓,原生不支持cookie,要使用cookie挺麻煩。
Token認(rèn)證(使用jwt規(guī)范)token 即使是在計(jì)算機(jī)領(lǐng)域中也有不同的定義,這里我們說的token,是指 訪問資源的憑據(jù) 。使用基于 Token 的身份驗(yàn)證方法,在服務(wù)端不需要存儲用戶的登錄記錄。大概的流程是 這樣的:
客戶端使用用戶名跟密碼請求登錄
服務(wù)端收到請求,去驗(yàn)證用戶名與密碼
驗(yàn)證成功后,服務(wù)端會簽發(fā)一個(gè) Token,再把這個(gè) Token 發(fā)送給客戶端
客戶端收到 Token 以后可以把它存儲起來,比如放在 Cookie 里
客戶端每次向服務(wù)端請求資源的時(shí)候需要帶著服務(wù)端簽發(fā)的 Token
服務(wù)端收到請求,然后去驗(yàn)證客戶端請求里面帶著的 Token,如果驗(yàn)證成功,就向客戶端返回請求的數(shù)據(jù)
Token機(jī)制,我認(rèn)為其本質(zhì)思想就是將session中的信息簡化很多,當(dāng)作cookie用,也就是客戶端的“session”。
好處那Token機(jī)制相對于Cookie機(jī)制又有什么好處呢?
支持跨域訪問: Cookie是不允許垮域訪問的,這一點(diǎn)對Token機(jī)制是不存在的,前提 是傳輸?shù)挠脩粽J(rèn)證信息通過HTTP頭傳輸.
無狀態(tài):Token機(jī)制本質(zhì)是校驗(yàn), 他得到的會話狀態(tài)完全來自于客戶端, Token機(jī)制在服務(wù)端不需要存儲session信息,因?yàn)?Token 自身包含了所有登錄用戶的信息,只需要在客戶端的cookie或本地介質(zhì)存儲狀態(tài)信息.
更適用CDN: 可以通過內(nèi)容分發(fā)網(wǎng)絡(luò)請求你服務(wù)端的所有資料(如:javascript, HTML,圖片等),而你的服務(wù)端只要提供API即可.
去耦: 不需要綁定到一個(gè)特定的身份驗(yàn)證方案。Token可以在任何地方生成,只要在 你的API被調(diào)用的時(shí)候,你可以進(jìn)行Token生成調(diào)用即可.
更適用于移動應(yīng)用: 當(dāng)你的客戶端是一個(gè)原生平臺(iOS, Android,Windows 8等) 時(shí),Cookie是不被支持的(你需要通過Cookie容器進(jìn)行處理),這時(shí)采用Token認(rèn) 證機(jī)制就會簡單得多。 CSRF:因?yàn)椴辉僖蕾囉贑ookie,所以你就不需要考慮對CSRF(跨站請求偽造)的防 范。
性能: 一次網(wǎng)絡(luò)往返時(shí)間(通過數(shù)據(jù)庫查詢session信息)總比做一次HMACSHA256 計(jì)算 的Token驗(yàn)證和解析要費(fèi)時(shí)得多. 不需要為登錄頁面做特殊處理: 如果你使用Protractor 做功能測試的時(shí)候,不再需要 為登錄頁面做特殊處理.
基于標(biāo)準(zhǔn)化:你的API可以采用標(biāo)準(zhǔn)化的 JSON Web Token (JWT). 這個(gè)標(biāo)準(zhǔn)已經(jīng)存在 多個(gè)后端庫(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如: Firebase,Google, Microsoft)
缺陷在哪?說了那么多token認(rèn)證的好處,但他其實(shí)并沒有想象的那么神,token 也并不是沒有問題。
占帶寬
正常情況下要比 session_id 更大,需要消耗更多流量,擠占更多帶寬,假如你的網(wǎng)站每月有 10 萬次的瀏覽器,就意味著要多開銷幾十兆的流量。聽起來并不多,但日積月累也是不小一筆開銷。實(shí)際上,許多人會在 JWT 中存儲的信息會更多。
無論如何你需要操作數(shù)據(jù)庫
在網(wǎng)站上使用 JWT,對于用戶加載的幾乎所有頁面,都需要從緩存/數(shù)據(jù)庫中加載用戶信息,如果對于高流量的服務(wù),你確定這個(gè)操作合適么?如果使用redis進(jìn)行緩存,那么效率上也并不能比 session 更高效
無法在服務(wù)端注銷,那么久很難解決劫持問題
性能問題
JWT 的賣點(diǎn)之一就是加密簽名,由于這個(gè)特性,接收方得以驗(yàn)證 JWT 是否有效且被信任。但是大多數(shù) Web 身份認(rèn)證應(yīng)用中,JWT 都會被存儲到 Cookie 中,這就是說你有了兩個(gè)層面的簽名。聽著似乎很牛逼,但是沒有任何優(yōu)勢,為此,你需要花費(fèi)兩倍的 CPU 開銷來驗(yàn)證簽名。對于有著嚴(yán)格性能要求的 Web 應(yīng)用,這并不理想,尤其對于單線程環(huán)境。
JWT現(xiàn)在我們來說說今天的主角,JWT
JSON Web Token(JWT)是一個(gè)非常輕巧的規(guī)范。這個(gè)規(guī)范允許我們使用JWT在用 戶和服務(wù)器之間傳遞安全可靠的信息
組成一個(gè)JWT實(shí)際上就是一個(gè)字符串,它由三部分組成,頭部、載荷與簽名。
頭部(header)頭部用于描述關(guān)于該JWT的最基本的信息,例如其類型以及簽名所用的算法等。這也可以 被表示成一個(gè)JSON對象。
{ "typ":"JWT", "alg":"HS256" }
這就是頭部的明文內(nèi)容,第一部分說明他是一個(gè)jwt,第二部分則指出簽名算法用的是HS256算法。
然后將這個(gè)頭部進(jìn)行BASE64編碼,編碼后形成頭部:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9載荷(payload)
載荷就是存放有效信息的地方,有效信息包含三個(gè)部分:
(1)標(biāo)準(zhǔn)中注冊的聲明(建議但不強(qiáng)制使用)
iss: jwt簽發(fā)者
sub: jwt所面向的用戶
aud: 接收jwt的一方
exp: jwt的過期時(shí)間,這個(gè)過期時(shí)間必須要大于簽發(fā)時(shí)間
nbf: 定義在什么時(shí)間之前,該jwt都是不可用的.
iat: jwt的簽發(fā)時(shí)間
jti: jwt的唯一身份標(biāo)識,主要用來作為一次性token,從而回避重放攻擊。
(2)公共的聲明
公共的聲明可以添加任何的信息,一般添加用戶的相關(guān)信息或其他業(yè)務(wù)需要的必要信息. 但不建議添加敏感信息,因?yàn)樵摬糠衷诳蛻舳丝山饷?
(3)私有的聲明
私有聲明是提供者和消費(fèi)者所共同定義的聲明,一般不建議存放敏感信息,因?yàn)閎ase64 是對稱解密的,意味著該部分信息可以歸類為明文信息。
{ "sub":"1234567890", "name":"tengshe789", "admin": true }
上面就是一個(gè)簡單的載荷的明文,接下來使用base64加密:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9簽證(signature)
jwt的第三部分是一個(gè)簽證信息,這個(gè)簽證信息由三部分組成:
header (base64后的)
payload (base64后的)
secret
這個(gè)部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進(jìn)行加鹽secret組合加密,然后就構(gòu)成了jwt的第 三部分。
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ合成
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I kpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7Hg Q實(shí)現(xiàn)JWT
現(xiàn)在一般實(shí)現(xiàn)jwt,都使用Apache 的開源項(xiàng)目JJWT(一個(gè)提供端到端的JWT創(chuàng)建和驗(yàn)證的Java庫)。
依賴創(chuàng)建token的demoio.jsonwebtoken jjwt 0.7.0
public class CreateJWT { public static void main(String[] args) throws Exception{ JwtBuilder builder = Jwts.builder().setId("123") .setSubject("jwt所面向的用戶") .setIssuedAt(new Date()) .signWith(SignatureAlgorithm.HS256,"tengshe789"); String s = builder.compact(); System.out.println(s); //eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjMiLCJzdWIiOiJqd3TmiYDpnaLlkJHnmoTnlKjmiLciLCJpYXQiOjE1NDM3NTk0MjJ9.1sIlEynqqZmA4PbKI6GgiP3ljk_aiypcsUxSN6-ATIA } }
結(jié)果如圖:
(注意,jjwt不支持jdk11,0.9.1以后的jjwt必須實(shí)現(xiàn)signWith()方法才能實(shí)現(xiàn))
解析Token的demopublic class ParseJWT { public static void main(String[] args) { String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjMiLCJzdWIiOiJqd3TmiYDpnaLlkJHnmoTnlKjmiLciLCJpYXQiOjE1NDM3NTk0MjJ9.1sIlEynqqZmA4PbKI6GgiP3ljk_aiypcsUxSN6-ATIA"; Claims claims = Jwts.parser().setSigningKey("tengshe789").parseClaimsJws(token).getBody(); System.out.println("id"+claims.getId()); System.out.println("Subject"+claims.getSubject()); System.out.println("IssuedAt"+claims.getIssuedAt()); } }
結(jié)果如圖:
生產(chǎn)中的JWT在企業(yè)級系統(tǒng)中,通常內(nèi)部會有非常多的工具平臺供大家使用,比如人力資源,代碼管理,日志監(jiān)控,預(yù)算申請等等。如果每一個(gè)平臺都實(shí)現(xiàn)自己的用戶體系的話無疑是巨大的浪費(fèi),所以公司內(nèi)部會有一套公用的用戶體系,用戶只要登陸之后,就能夠訪問所有的系統(tǒng)。
這就是 單點(diǎn)登錄(SSO: Single Sign-On)
SSO 是一類解決方案的統(tǒng)稱,而在具體的實(shí)施方面,一般有兩種策略可供選擇:
SAML 2.0
OAuth 2.0
欲揚(yáng)先抑,先說說幾個(gè)重要的知識點(diǎn)。
Authentication VS AuthorisationAuthentication: 身份鑒別,鑒權(quán),以下簡稱認(rèn)證
認(rèn)證 的作用在于認(rèn)可你有權(quán)限訪問系統(tǒng),用于鑒別訪問者是否是合法用戶。負(fù)責(zé)認(rèn)證的服務(wù)通常稱為 Authorization Server 或者 Identity Provider,以下簡稱 IdP
Authorisation: 授權(quán)
授權(quán) 用于決定你有訪問哪些資源的權(quán)限。大多數(shù)人不會區(qū)分這兩者的區(qū)別,因?yàn)檎驹谟脩舻牧錾稀6鳛橄到y(tǒng)的設(shè)計(jì)者來說,這兩者是有差別的,這是不同的兩個(gè)工作職責(zé),我們可以只需要認(rèn)證功能,而不需要授權(quán)功能,甚至不需要自己實(shí)現(xiàn)認(rèn)證功能,而借助 Google 的認(rèn)證系統(tǒng),即用戶可以用 Google 的賬號進(jìn)行登陸。負(fù)責(zé)提供資源(API調(diào)用)的服務(wù)稱為 Resource Server 或者 Service Provider,以下簡稱 SP
SMAL 2.0 OAuth(JWT)OAuth(開放授權(quán))是一個(gè)開放的授權(quán)標(biāo)準(zhǔn),允許用戶讓第三方應(yīng)用訪問該用戶在 某一web服務(wù)上存儲的私密的資源(如照片,視頻,聯(lián)系人列表),而無需將用戶名和密碼提供給第三方應(yīng)用。
流程可以參考如下:
簡單的來說,就是你要訪問一個(gè)應(yīng)用服務(wù),先找它要一個(gè)request token(請求令牌),再把這個(gè)request token發(fā)到第三方認(rèn)證服務(wù)器,此時(shí)第三方認(rèn)證服務(wù)器會給你一個(gè)aceess token(通行令牌), 有了aceess token你就可以使用你的應(yīng)用服務(wù)了。
注意圖中第4步兌換 access token 的過程中,很多第三方系統(tǒng),如Google ,并不會僅僅返回 access token,還會返回額外的信息,這其中和之后更新相關(guān)的就是 refresh token。一旦 access token 過期,你就可以通過 refresh token 再次請求 access token。
當(dāng)然了,流程是根據(jù)你的請求方式和訪問的資源類型而定的,業(yè)務(wù)很多也是不一樣的,我這是簡單的聊聊。
現(xiàn)在這種方法比較常見,常見的譬如使用QQ快速登陸,用的基本的都是這種方法。
開源項(xiàng)目我們用一個(gè)很火的開源項(xiàng)目Cloud-Admin為栗子,來分析一下jwt的應(yīng)用。
Cloud-Admin是基于Spring Cloud微服務(wù)化開發(fā)平臺,具有統(tǒng)一授權(quán)、認(rèn)證后臺管理系統(tǒng),其中包含具備用戶管理、資源權(quán)限管理、網(wǎng)關(guān)API管理等多個(gè)模塊,支持多業(yè)務(wù)系統(tǒng)并行開發(fā)。
目錄結(jié)構(gòu)鑒權(quán)中心功能在ace-auth與ace-gate下。
模型下面是官方提供的架構(gòu)模型。
可以看到,AuthServer在架構(gòu)的中心環(huán)節(jié),要訪問服務(wù),必須需要鑒權(quán)中心的JWT鑒權(quán)。
鑒權(quán)中心服務(wù)端代碼解讀 實(shí)體類先看實(shí)體類,這里鑒權(quán)中心定義了一組客戶端實(shí)體,如下:
@Table(name = "auth_client") @Getter @Setter public class Client { @Id private Integer id; private String code; private String secret; private String name; private String locked = "0"; private String description; @Column(name = "crt_time") private Date crtTime; @Column(name = "crt_user") private String crtUser; @Column(name = "crt_name") private String crtName; @Column(name = "crt_host") private String crtHost; @Column(name = "upd_time") private Date updTime; @Column(name = "upd_user") private String updUser; @Column(name = "upd_name") private String updName; @Column(name = "upd_host") private String updHost; private String attr1; private String attr2; private String attr3; private String attr4; private String attr5; private String attr6; private String attr7; private String attr8;
對應(yīng)數(shù)據(jù)庫:
CREATE TABLE `auth_client` ( `id` int(11) NOT NULL AUTO_INCREMENT, `code` varchar(255) DEFAULT NULL COMMENT "服務(wù)編碼", `secret` varchar(255) DEFAULT NULL COMMENT "服務(wù)密鑰", `name` varchar(255) DEFAULT NULL COMMENT "服務(wù)名", `locked` char(1) DEFAULT NULL COMMENT "是否鎖定", `description` varchar(255) DEFAULT NULL COMMENT "描述", `crt_time` datetime DEFAULT NULL COMMENT "創(chuàng)建時(shí)間", `crt_user` varchar(255) DEFAULT NULL COMMENT "創(chuàng)建人", `crt_name` varchar(255) DEFAULT NULL COMMENT "創(chuàng)建人姓名", `crt_host` varchar(255) DEFAULT NULL COMMENT "創(chuàng)建主機(jī)", `upd_time` datetime DEFAULT NULL COMMENT "更新時(shí)間", `upd_user` varchar(255) DEFAULT NULL COMMENT "更新人", `upd_name` varchar(255) DEFAULT NULL COMMENT "更新姓名", `upd_host` varchar(255) DEFAULT NULL COMMENT "更新主機(jī)", `attr1` varchar(255) DEFAULT NULL, `attr2` varchar(255) DEFAULT NULL, `attr3` varchar(255) DEFAULT NULL, `attr4` varchar(255) DEFAULT NULL, `attr5` varchar(255) DEFAULT NULL, `attr6` varchar(255) DEFAULT NULL, `attr7` varchar(255) DEFAULT NULL, `attr8` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4;
這些是每組微服務(wù)客戶端的信息
第二個(gè)實(shí)體類,就是客戶端_服務(wù)的實(shí)體,也就是對應(yīng)著那些微服務(wù)客戶端能調(diào)用哪些微服務(wù)客戶端:
大概對應(yīng)的就是微服務(wù)間調(diào)用權(quán)限關(guān)系。
@Table(name = "auth_client_service") public class ClientService { @Id private Integer id; @Column(name = "service_id") private String serviceId; @Column(name = "client_id") private String clientId; private String description; @Column(name = "crt_time") private Date crtTime; @Column(name = "crt_user") private String crtUser; @Column(name = "crt_name") private String crtName; @Column(name = "crt_host") private String crtHost;}接口層
我們跳著看,先看接口層
@RestController @RequestMapping("jwt") @Slf4j public class AuthController { @Value("${jwt.token-header}") private String tokenHeader; @Autowired private AuthService authService; @RequestMapping(value = "token", method = RequestMethod.POST) public ObjectRestResponsecreateAuthenticationToken( @RequestBody JwtAuthenticationRequest authenticationRequest) throws Exception { log.info(authenticationRequest.getUsername()+" require logging..."); final String token = authService.login(authenticationRequest); return new ObjectRestResponse<>().data(token); } @RequestMapping(value = "refresh", method = RequestMethod.GET) public ObjectRestResponse refreshAndGetAuthenticationToken( HttpServletRequest request) throws Exception { String token = request.getHeader(tokenHeader); String refreshedToken = authService.refresh(token); return new ObjectRestResponse<>().data(refreshedToken); } @RequestMapping(value = "verify", method = RequestMethod.GET) public ObjectRestResponse> verify(String token) throws Exception { authService.validate(token); return new ObjectRestResponse<>(); } }
這里放出了三個(gè)接口
先說第一個(gè)接口,創(chuàng)建token。
具體邏輯如下:
每一個(gè)用戶登陸進(jìn)來時(shí),都會進(jìn)入這個(gè)環(huán)節(jié)。根據(jù)request中用戶的用戶名和密碼,利用feign客戶端的攔截器攔截request,然后使用作者寫的JwtTokenUtil里面的各種方法取出token中的key和密鑰,驗(yàn)證token是否正確,正確則用authService.login(authenticationRequest);的方法返回出去一個(gè)新的token。
public String login(JwtAuthenticationRequest authenticationRequest) throws Exception { UserInfo info = userService.validate(authenticationRequest); if (!StringUtils.isEmpty(info.getId())) { return jwtTokenUtil.generateToken(new JWTInfo(info.getUsername(), info.getId() + "", info.getName())); } throw new UserInvalidException("用戶不存在或賬戶密碼錯誤!"); }
下圖是詳細(xì)邏輯圖:
鑒權(quán)中心客戶端代碼 入口作者寫了個(gè)注解的入口,使用@EnableAceAuthClient即自動開啟微服務(wù)(客戶端)的鑒權(quán)管理
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(AutoConfiguration.class) @Documented @Inherited public @interface EnableAceAuthClient { }配置
接著沿著注解的入口看
@Configuration @ComponentScan({"com.github.wxiaoqi.security.auth.client","com.github.wxiaoqi.security.auth.common.event"}) public class AutoConfiguration { @Bean ServiceAuthConfig getServiceAuthConfig(){ return new ServiceAuthConfig(); } @Bean UserAuthConfig getUserAuthConfig(){ return new UserAuthConfig(); } }
注解會自動的將客戶端的用戶token和服務(wù)token的關(guān)鍵信息加載到bean中
feigin攔截器作者重寫了okhttp3攔截器的方法,每一次微服務(wù)客戶端請求的token都會被攔截下來,驗(yàn)證服務(wù)調(diào)用服務(wù)的token和用戶調(diào)用服務(wù)的token是否過期,過期則返回新的token
@Override public Response intercept(Chain chain) throws IOException { Request newRequest = null; if (chain.request().url().toString().contains("client/token")) { newRequest = chain.request() .newBuilder() .header(userAuthConfig.getTokenHeader(), BaseContextHandler.getToken()) .build(); } else { newRequest = chain.request() .newBuilder() .header(userAuthConfig.getTokenHeader(), BaseContextHandler.getToken()) .header(serviceAuthConfig.getTokenHeader(), serviceAuthUtil.getClientToken()) .build(); } Response response = chain.proceed(newRequest); if (HttpStatus.FORBIDDEN.value() == response.code()) { if (response.body().string().contains(String.valueOf(CommonConstants.EX_CLIENT_INVALID_CODE))) { log.info("Client Token Expire,Retry to request..."); serviceAuthUtil.refreshClientToken(); newRequest = chain.request() .newBuilder() .header(userAuthConfig.getTokenHeader(), BaseContextHandler.getToken()) .header(serviceAuthConfig.getTokenHeader(), serviceAuthUtil.getClientToken()) .build(); response = chain.proceed(newRequest); } } return response; }spring容器的攔截器
第二道攔截器是來自spring容器的,第一道feign攔截器只是驗(yàn)證了兩個(gè)token是否過期,但token真實(shí)的權(quán)限卻沒驗(yàn)證。接下來就要驗(yàn)證兩個(gè)token的權(quán)限問題了。
服務(wù)調(diào)用權(quán)限代碼如下:
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; // 配置該注解,說明不進(jìn)行服務(wù)攔截 IgnoreClientToken annotation = handlerMethod.getBeanType().getAnnotation(IgnoreClientToken.class); if (annotation == null) { annotation = handlerMethod.getMethodAnnotation(IgnoreClientToken.class); } if(annotation!=null) { return super.preHandle(request, response, handler); } String token = request.getHeader(serviceAuthConfig.getTokenHeader()); IJWTInfo infoFromToken = serviceAuthUtil.getInfoFromToken(token); String uniqueName = infoFromToken.getUniqueName(); for(String client:serviceAuthUtil.getAllowedClient()){ if(client.equals(uniqueName)){ return super.preHandle(request, response, handler); } } throw new ClientForbiddenException("Client is Forbidden!"); }
用戶權(quán)限:
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; // 配置該注解,說明不進(jìn)行用戶攔截 IgnoreUserToken annotation = handlerMethod.getBeanType().getAnnotation(IgnoreUserToken.class); if (annotation == null) { annotation = handlerMethod.getMethodAnnotation(IgnoreUserToken.class); } if (annotation != null) { return super.preHandle(request, response, handler); } String token = request.getHeader(userAuthConfig.getTokenHeader()); if (StringUtils.isEmpty(token)) { if (request.getCookies() != null) { for (Cookie cookie : request.getCookies()) { if (cookie.getName().equals(userAuthConfig.getTokenHeader())) { token = cookie.getValue(); } } } } IJWTInfo infoFromToken = userAuthUtil.getInfoFromToken(token); BaseContextHandler.setUsername(infoFromToken.getUniqueName()); BaseContextHandler.setName(infoFromToken.getName()); BaseContextHandler.setUserID(infoFromToken.getId()); return super.preHandle(request, response, handler); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { BaseContextHandler.remove(); super.afterCompletion(request, response, handler, ex); }spring cloud gateway網(wǎng)關(guān)代碼
該框架中所有的請求都會走網(wǎng)關(guān)服務(wù)(ace-gatev2),通過網(wǎng)關(guān),來驗(yàn)證token是否過期異常,驗(yàn)證token是否不存在,驗(yàn)證token是否有權(quán)限進(jìn)行服務(wù)。
下面是核心代碼:
@Override public Monocloud admin總結(jié)filter(ServerWebExchange serverWebExchange, GatewayFilterChain gatewayFilterChain) { log.info("check token and user permission...."); LinkedHashSet requiredAttribute = serverWebExchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR); ServerHttpRequest request = serverWebExchange.getRequest(); String requestUri = request.getPath().pathWithinApplication().value(); if (requiredAttribute != null) { Iterator iterator = requiredAttribute.iterator(); while (iterator.hasNext()){ URI next = iterator.next(); if(next.getPath().startsWith(GATE_WAY_PREFIX)){ requestUri = next.getPath().substring(GATE_WAY_PREFIX.length()); } } } final String method = request.getMethod().toString(); BaseContextHandler.setToken(null); ServerHttpRequest.Builder mutate = request.mutate(); // 不進(jìn)行攔截的地址 if (isStartWith(requestUri)) { ServerHttpRequest build = mutate.build(); return gatewayFilterChain.filter(serverWebExchange.mutate().request(build).build()); } IJWTInfo user = null; try { user = getJWTUser(request, mutate); } catch (Exception e) { log.error("用戶Token過期異常", e); return getVoidMono(serverWebExchange, new TokenForbiddenResponse("User Token Forbidden or Expired!")); } List permissionIfs = userService.getAllPermissionInfo(); // 判斷資源是否啟用權(quán)限約束 Stream stream = getPermissionIfs(requestUri, method, permissionIfs); List result = stream.collect(Collectors.toList()); PermissionInfo[] permissions = result.toArray(new PermissionInfo[]{}); if (permissions.length > 0) { if (checkUserPermission(permissions, serverWebExchange, user)) { return getVoidMono(serverWebExchange, new TokenForbiddenResponse("User Forbidden!Does not has Permission!")); } } // 申請客戶端密鑰頭 mutate.header(serviceAuthConfig.getTokenHeader(), serviceAuthUtil.getClientToken()); ServerHttpRequest build = mutate.build(); return gatewayFilterChain.filter(serverWebExchange.mutate().request(build).build()); }
總的來說,鑒權(quán)和網(wǎng)關(guān)模塊就說完了。作者代碼構(gòu)思極其精妙,使用在大型的權(quán)限系統(tǒng)中,可以巧妙的減少耦合性,讓服務(wù)鑒權(quán)粒度細(xì)化,方便管理。
結(jié)束此片完了~ 想要了解更多精彩新姿勢?
請?jiān)L問我的個(gè)人博客
本篇為原創(chuàng)內(nèi)容,已在個(gè)人博客率先發(fā)表,隨后看心情可能會在CSDN,segmentfault,掘金,簡書,開源中國同步發(fā)出。如有雷同,緣分呢兄弟。趕快加個(gè)好友,咱們兩個(gè)想個(gè)號碼, 買個(gè)彩票,先掙他個(gè)幾百萬
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/72564.html
摘要:認(rèn)證服務(wù)器,即服務(wù)提供商專門用來處理認(rèn)證的服務(wù)器。它與認(rèn)證服務(wù)器,可以是同一臺服務(wù)器,也可以是不同的服務(wù)器。客戶端使用上一步獲得的授權(quán),向認(rèn)證服務(wù)器申請令牌。認(rèn)證服務(wù)器對客戶端進(jìn)行認(rèn)證以后,確認(rèn)無誤,同意發(fā)放令牌。 最近想做個(gè)小程序,需要用到授權(quán)認(rèn)證流程。以前項(xiàng)目都是用的 OAuth2 認(rèn)證,但是Sanic 使用OAuth2 不太方便,就想試一下 JWT 的認(rèn)證方式。這一篇主要內(nèi)容是 ...
摘要:前幾天寫一篇,一種新思路實(shí)現(xiàn)分布式事務(wù)的文章。寫個(gè)分布式事務(wù)就有人開始噴了事務(wù)提交了,怎么回滾都知道怎么回滾。 前幾天寫一篇 , 一種新思路實(shí)現(xiàn)分布式事務(wù)的文章。https://segmentfault.com/a/11... 部分死腦筋就開始,各種不解。看反饋 確實(shí)有點(diǎn)搞笑。 不要一聽到 session 就覺得是 $_SEESION不要別人換個(gè)名字 token 或者 jwt 就不認(rèn)識...
摘要:最近讀了幾篇大牛的博客才對認(rèn)證機(jī)制方面有了進(jìn)一步了解。盡管在服務(wù)器端可以優(yōu)雅地使用技術(shù)如攔截器或動態(tài)代理對所有進(jìn)行前置的登錄驗(yàn)證。認(rèn)證方式比較支持問題和其實(shí)是緊密相聯(lián)的。第三方授權(quán)問題采用傳統(tǒng)認(rèn)證方式,若要訪問業(yè)務(wù),一定要先登錄。 引言 以前對認(rèn)證這方面的認(rèn)識一直不太深刻,不清楚為什么需要token這種認(rèn)證,為什么不簡單使用session存儲用戶登錄信息等。最近讀了幾篇大牛的博客才對認(rèn)...
摘要:的聲明一般被用來在身份提供者和服務(wù)提供者間傳遞被認(rèn)證的用戶身份信息,以便于從資源服務(wù)器獲取資源,也可以增加一些額外的其它業(yè)務(wù)邏輯所必須的聲明信息,該也可直接被用于認(rèn)證,也可被加密。 JWT認(rèn)證登錄 最近在做一個(gè)審核系統(tǒng),后臺登錄用到JWT登錄認(rèn)證,在此主要做個(gè)總結(jié) JWT是什么 Json web token (JWT), 根據(jù)官網(wǎng)的定義,是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于J...
閱讀 2698·2021-11-08 13:16
閱讀 2376·2021-10-18 13:30
閱讀 2247·2021-09-27 13:35
閱讀 2002·2019-08-30 15:55
閱讀 2451·2019-08-30 13:22
閱讀 592·2019-08-30 11:24
閱讀 2084·2019-08-29 12:33
閱讀 1820·2019-08-26 12:10