分布式系統(tǒng)--感性認(rèn)識(shí)JWT

好久沒(méi)寫博客了氓鄙,因?yàn)樽罱疽笪覍W(xué)spring cloud诚隙,早點(diǎn)將以前軟件遷移到新的架構(gòu)上。所以我那個(gè)拼命的學(xué)吶赎懦,總是圖快雀鹃,很多關(guān)鍵的筆記沒(méi)有做好記錄,現(xiàn)在又遺忘了很多關(guān)鍵的技術(shù)點(diǎn)励两,極其罪惡黎茎!

現(xiàn)在想一想,還是踏踏實(shí)實(shí)的走比較好当悔。這不傅瞻,今天我冒了個(gè)泡,來(lái)補(bǔ)一補(bǔ)前面我所學(xué)所忘的知識(shí)點(diǎn)盲憎。

想要解鎖更多新姿勢(shì)嗅骄?請(qǐng)?jiān)L問(wèn)我的博客

常見(jiàn)的認(rèn)證機(jī)制

今天我么聊一聊JWT饼疙。

關(guān)于JWT溺森,相信很多人都已經(jīng)看過(guò)用過(guò),他是基于json數(shù)據(jù)結(jié)構(gòu)的認(rèn)證規(guī)范,簡(jiǎn)單的說(shuō)就是驗(yàn)證用戶登沒(méi)登陸的玩意儿惫。這時(shí)候你可能回想澡罚,哎喲,不是又那個(gè)session么肾请,分布式系統(tǒng)用redis做分布式session留搔,那這個(gè)jwt有什么好處呢?

請(qǐng)聽我慢慢訴說(shuō)這歷史铛铁!

最原始的辦法--HTTP BASIC AUTH

HTTP BASIC auth隔显,別看它名字那么長(zhǎng)那么生,你就認(rèn)為這個(gè)玩意很高大上饵逐。其實(shí)原理很簡(jiǎn)單括眠,簡(jiǎn)單的說(shuō)就是每次請(qǐng)求API的時(shí)候,都會(huì)把用戶名和密碼通過(guò)restful API傳給服務(wù)端倍权。這樣就可以實(shí)現(xiàn)一個(gè)無(wú)狀態(tài)思想掷豺,即每次HTTP請(qǐng)求和以前都沒(méi)有啥關(guān)系,只是獲取目標(biāo)URI薄声,得到目標(biāo)內(nèi)容之后当船,這次連接就被殺死,沒(méi)有任何痕跡默辨。你可別一聽無(wú)狀態(tài)德频,正是現(xiàn)在的熱門思想,就覺(jué)得很厲害缩幸。其實(shí)他的缺點(diǎn)還是又的壹置,我們通過(guò)http請(qǐng)求發(fā)送給服務(wù)端的時(shí)候,很有可能將我們的用戶名密碼直接暴漏給第三方客戶端表谊,風(fēng)險(xiǎn)特別大钞护,因此生產(chǎn)環(huán)境下用這個(gè)方法很少。

Session和cookie

session和cookie老生常談了爆办。開始時(shí)难咕,都會(huì)在服務(wù)端全局創(chuàng)建session對(duì)象,session對(duì)象保存著各種關(guān)鍵信息押逼,同時(shí)向客戶端發(fā)送一組sessionId步藕,成為一個(gè)cookie對(duì)象保存在瀏覽器中惦界。

當(dāng)認(rèn)證時(shí)挑格,cookie的數(shù)據(jù)會(huì)傳入服務(wù)端與session進(jìn)行匹配,進(jìn)而進(jìn)行數(shù)據(jù)認(rèn)證沾歪。

how session work

此時(shí)漂彤,實(shí)現(xiàn)的是一個(gè)有狀態(tài)的思想,即該服務(wù)的實(shí)例可以將一部分?jǐn)?shù)據(jù)隨時(shí)進(jìn)行備份,并且在創(chuàng)建一個(gè)新的有狀態(tài)服務(wù)時(shí)挫望,可以通過(guò)備份恢復(fù)這些數(shù)據(jù)立润,以達(dá)到數(shù)據(jù)持久化的目的。

缺點(diǎn)

這種認(rèn)證方法基本是現(xiàn)在軟件最常用的方法了媳板,它有一些自己的缺點(diǎn):

  • 安全性桑腮。cookies的安全性不好,攻擊者可以通過(guò)獲取本地cookies進(jìn)行欺騙或者利用cookies進(jìn)行CSRF攻擊蛉幸。
  • 跨域問(wèn)題破讨。使用cookies時(shí),在多個(gè)域名下奕纫,會(huì)存在跨域問(wèn)題提陶。
  • 有狀態(tài)。session在一定的時(shí)間里匹层,需要存放在服務(wù)端隙笆,因此當(dāng)擁有大量用戶時(shí),也會(huì)大幅度降低服務(wù)端的性能升筏。
  • 狀態(tài)問(wèn)題撑柔。當(dāng)有多臺(tái)機(jī)器時(shí),如何共享session也會(huì)是一個(gè)問(wèn)題仰冠,也就是說(shuō)乏冀,用戶第一個(gè)訪問(wèn)的時(shí)候是服務(wù)器A,而第二個(gè)請(qǐng)求被轉(zhuǎn)發(fā)給了服務(wù)器B洋只,那服務(wù)器B如何得知其狀態(tài)辆沦。
  • 移動(dòng)手機(jī)問(wèn)題。現(xiàn)在的智能手機(jī)识虚,包括安卓肢扯,原生不支持cookie,要使用cookie挺麻煩担锤。

Token認(rèn)證(使用jwt規(guī)范)

token 即使是在計(jì)算機(jī)領(lǐng)域中也有不同的定義蔚晨,這里我們說(shuō)的token,是指 訪問(wèn)資源的憑據(jù) 肛循。使用基于 Token 的身份驗(yàn)證方法铭腕,在服務(wù)端不需要存儲(chǔ)用戶的登錄記錄。大概的流程是 這樣的:

  1. 客戶端使用用戶名跟密碼請(qǐng)求登錄
  2. 服務(wù)端收到請(qǐng)求多糠,去驗(yàn)證用戶名與密碼
  3. 驗(yàn)證成功后累舷,服務(wù)端會(huì)簽發(fā)一個(gè) Token,再把這個(gè) Token 發(fā)送給客戶端
  4. 客戶端收到 Token 以后可以把它存儲(chǔ)起來(lái)夹孔,比如放在 Cookie 里
  5. 客戶端每次向服務(wù)端請(qǐng)求資源的時(shí)候需要帶著服務(wù)端簽發(fā)的 Token
  6. 服務(wù)端收到請(qǐng)求被盈,然后去驗(yàn)證客戶端請(qǐng)求里面帶著的 Token析孽,如果驗(yàn)證成功,就向客戶端返回請(qǐng)求的數(shù)據(jù)

Token機(jī)制只怎,我認(rèn)為其本質(zhì)思想就是將session中的信息簡(jiǎn)化很多袜瞬,當(dāng)作cookie用,也就是客戶端的“session”身堡。

好處

那Token機(jī)制相對(duì)于Cookie機(jī)制又有什么好處呢邓尤?

  • 支持跨域訪問(wèn): Cookie是不允許垮域訪問(wèn)的,這一點(diǎn)對(duì)Token機(jī)制是不存在的贴谎,前提 是傳輸?shù)挠脩粽J(rèn)證信息通過(guò)HTTP頭傳輸.
  • 無(wú)狀態(tài):Token機(jī)制本質(zhì)是校驗(yàn), 他得到的會(huì)話狀態(tài)完全來(lái)自于客戶端, Token機(jī)制在服務(wù)端不需要存儲(chǔ)session信息裁赠,因?yàn)?Token 自身包含了所有登錄用戶的信息,只需要在客戶端的cookie或本地介質(zhì)存儲(chǔ)狀態(tài)信息.
  • 更適用CDN: 可以通過(guò)內(nèi)容分發(fā)網(wǎng)絡(luò)請(qǐng)求你服務(wù)端的所有資料(如:javascript赴精, HTML,圖片等)佩捞,而你的服務(wù)端只要提供API即可.
  • 去耦: 不需要綁定到一個(gè)特定的身份驗(yàn)證方案。Token可以在任何地方生成蕾哟,只要在 你的API被調(diào)用的時(shí)候一忱,你可以進(jìn)行Token生成調(diào)用即可.
  • 更適用于移動(dòng)應(yīng)用: 當(dāng)你的客戶端是一個(gè)原生平臺(tái)(iOS, Android,Windows 8等) 時(shí)谭确,Cookie是不被支持的(你需要通過(guò)Cookie容器進(jìn)行處理)帘营,這時(shí)采用Token認(rèn) 證機(jī)制就會(huì)簡(jiǎn)單得多。 CSRF:因?yàn)椴辉僖蕾囉贑ookie逐哈,所以你就不需要考慮對(duì)CSRF(跨站請(qǐng)求偽造)的防 范芬迄。
  • 性能: 一次網(wǎng)絡(luò)往返時(shí)間(通過(guò)數(shù)據(jù)庫(kù)查詢session信息)總比做一次HMACSHA256 計(jì)算 的Token驗(yàn)證和解析要費(fèi)時(shí)得多. 不需要為登錄頁(yè)面做特殊處理: 如果你使用Protractor 做功能測(cè)試的時(shí)候,不再需要 為登錄頁(yè)面做特殊處理.
  • 基于標(biāo)準(zhǔn)化:你的API可以采用標(biāo)準(zhǔn)化的 JSON Web Token (JWT). 這個(gè)標(biāo)準(zhǔn)已經(jīng)存在 多個(gè)后端庫(kù)(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如: Firebase,Google, Microsoft)

缺陷在哪昂秃?

說(shuō)了那么多token認(rèn)證的好處禀梳,但他其實(shí)并沒(méi)有想象的那么神,token 也并不是沒(méi)有問(wèn)題肠骆。

  1. 占帶寬

    正常情況下要比 session_id 更大算途,需要消耗更多流量,擠占更多帶寬蚀腿,假如你的網(wǎng)站每月有 10 萬(wàn)次的瀏覽器嘴瓤,就意味著要多開銷幾十兆的流量。聽起來(lái)并不多莉钙,但日積月累也是不小一筆開銷廓脆。實(shí)際上,許多人會(huì)在 JWT 中存儲(chǔ)的信息會(huì)更多磁玉。

  2. 無(wú)論如何你需要操作數(shù)據(jù)庫(kù)

    在網(wǎng)站上使用 JWT停忿,對(duì)于用戶加載的幾乎所有頁(yè)面,都需要從緩存/數(shù)據(jù)庫(kù)中加載用戶信息蜀涨,如果對(duì)于高流量的服務(wù)瞎嬉,你確定這個(gè)操作合適么?如果使用redis進(jìn)行緩存厚柳,那么效率上也并不能比 session 更高效

  3. 無(wú)法在服務(wù)端注銷氧枣,那么久很難解決劫持問(wèn)題

  4. 性能問(wèn)題

    JWT 的賣點(diǎn)之一就是加密簽名,由于這個(gè)特性别垮,接收方得以驗(yàn)證 JWT 是否有效且被信任便监。但是大多數(shù) Web 身份認(rèn)證應(yīng)用中,JWT 都會(huì)被存儲(chǔ)到 Cookie 中碳想,這就是說(shuō)你有了兩個(gè)層面的簽名烧董。聽著似乎很牛逼,但是沒(méi)有任何優(yōu)勢(shì)胧奔,為此逊移,你需要花費(fèi)兩倍的 CPU 開銷來(lái)驗(yàn)證簽名。對(duì)于有著嚴(yán)格性能要求的 Web 應(yīng)用龙填,這并不理想胳泉,尤其對(duì)于單線程環(huán)境。

JWT

現(xiàn)在我們來(lái)說(shuō)說(shuō)今天的主角岩遗,JWT

JSON Web Token(JWT)是一個(gè)非常輕巧的規(guī)范扇商。這個(gè)規(guī)范允許我們使用JWT在用 戶和服務(wù)器之間傳遞安全可靠的信息

1543760350545

組成

一個(gè)JWT實(shí)際上就是一個(gè)字符串,它由三部分組成宿礁,頭部案铺、載荷簽名怪嫌。

頭部(header)

頭部用于描述關(guān)于該JWT的最基本的信息货抄,例如其類型以及簽名所用的算法等。這也可以 被表示成一個(gè)JSON對(duì)象钟病。

{
    "typ":"JWT",
    "alg":"HS256"
}

這就是頭部的明文內(nèi)容返吻,第一部分說(shuō)明他是一個(gè)jwt暇番,第二部分則指出簽名算法用的是HS256算法

然后將這個(gè)頭部進(jìn)行BASE64編碼思喊,編碼后形成頭部:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

載荷(payload)

載荷就是存放有效信息的地方,有效信息包含三個(gè)部分:

(1)標(biāo)準(zhǔn)中注冊(cè)的聲明(建議但不強(qiáng)制使用)

  • iss: jwt簽發(fā)者
  • sub: jwt所面向的用戶
  • aud: 接收jwt的一方
  • exp: jwt的過(guò)期時(shí)間壁酬,這個(gè)過(guò)期時(shí)間必須要大于簽發(fā)時(shí)間
  • nbf: 定義在什么時(shí)間之前,該jwt都是不可用的.
  • iat: jwt的簽發(fā)時(shí)間
  • jti: jwt的唯一身份標(biāo)識(shí)恨课,主要用來(lái)作為一次性token,從而回避重放攻擊舆乔。

(2)公共的聲明
公共的聲明可以添加任何的信息,一般添加用戶的相關(guān)信息或其他業(yè)務(wù)需要的必要信息. 但不建議添加敏感信息剂公,因?yàn)樵摬糠衷诳蛻舳丝山饷?

(3)私有的聲明

私有聲明是提供者和消費(fèi)者所共同定義的聲明希俩,一般不建議存放敏感信息,因?yàn)閎ase64 是對(duì)稱解密的纲辽,意味著該部分信息可以歸類為明文信息颜武。

{
    "sub":"1234567890",
    "name":"tengshe789",
    "admin": true
}

上面就是一個(gè)簡(jiǎn)單的載荷的明文璃搜,接下來(lái)使用base64加密:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

簽證(signature)

jwt的第三部分是一個(gè)簽證信息,這個(gè)簽證信息由三部分組成:

  1. header (base64后的)
  2. payload (base64后的)
  3. secret

這個(gè)部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串鳞上,然后通過(guò)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庫(kù))唾糯。

依賴

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>

創(chuàng)建token的demo

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é)果如圖:

1543759471279

(注意,jjwt不支持jdk11鬼贱,0.9.1以后的jjwt必須實(shí)現(xiàn)signWith()方法才能實(shí)現(xiàn))

解析Token的demo

public 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é)果如圖:

1543759769057

生產(chǎn)中的JWT

在企業(yè)級(jí)系統(tǒng)中移怯,通常內(nèi)部會(huì)有非常多的工具平臺(tái)供大家使用,比如人力資源这难,代碼管理舟误,日志監(jiān)控,預(yù)算申請(qǐng)等等姻乓。如果每一個(gè)平臺(tái)都實(shí)現(xiàn)自己的用戶體系的話無(wú)疑是巨大的浪費(fèi)脐帝,所以公司內(nèi)部會(huì)有一套公用的用戶體系,用戶只要登陸之后糖权,就能夠訪問(wèn)所有的系統(tǒng)堵腹。

這就是 單點(diǎn)登錄(SSO: Single Sign-On)

SSO 是一類解決方案的統(tǒng)稱,而在具體的實(shí)施方面星澳,一般有兩種策略可供選擇:

  1. SAML 2.0
  2. OAuth 2.0

欲揚(yáng)先抑疚顷,先說(shuō)說(shuō)幾個(gè)重要的知識(shí)點(diǎn)。

Authentication VS Authorisation

  • Authentication: 身份鑒別禁偎,鑒權(quán)腿堤,以下簡(jiǎn)稱認(rèn)證

    認(rèn)證 的作用在于認(rèn)可你有權(quán)限訪問(wèn)系統(tǒng),用于鑒別訪問(wèn)者是否是合法用戶如暖。負(fù)責(zé)認(rèn)證的服務(wù)通常稱為 Authorization Server 或者 Identity Provider笆檀,以下簡(jiǎn)稱 IdP

  • Authorisation: 授權(quán)

    授權(quán) 用于決定你有訪問(wèn)哪些資源的權(quán)限。大多數(shù)人不會(huì)區(qū)分這兩者的區(qū)別盒至,因?yàn)檎驹谟脩舻牧?chǎng)上酗洒。而作為系統(tǒng)的設(shè)計(jì)者來(lái)說(shuō),這兩者是有差別的枷遂,這是不同的兩個(gè)工作職責(zé)樱衷,我們可以只需要認(rèn)證功能,而不需要授權(quán)功能酒唉,甚至不需要自己實(shí)現(xiàn)認(rèn)證功能矩桂,而借助 Google 的認(rèn)證系統(tǒng),即用戶可以用 Google 的賬號(hào)進(jìn)行登陸痪伦。負(fù)責(zé)提供資源(API調(diào)用)的服務(wù)稱為 Resource Server 或者 Service Provider侄榴,以下簡(jiǎn)稱 SP

SMAL 2.0

smal flow

OAuth(JWT)

OAuth(開放授權(quán))是一個(gè)開放的授權(quán)標(biāo)準(zhǔn)雹锣,允許用戶讓第三方應(yīng)用訪問(wèn)該用戶在 某一web服務(wù)上存儲(chǔ)的私密的資源(如照片,視頻癞蚕,聯(lián)系人列表)蕊爵,而無(wú)需將用戶名和密碼提供給第三方應(yīng)用。

流程可以參考如下:

oauth

簡(jiǎn)單的來(lái)說(shuō)涣达,就是你要訪問(wèn)一個(gè)應(yīng)用服務(wù),先找它要一個(gè)request token(請(qǐng)求令牌)证薇,再把這個(gè)request token發(fā)到第三方認(rèn)證服務(wù)器度苔,此時(shí)第三方認(rèn)證服務(wù)器會(huì)給你一個(gè)aceess token(通行令牌), 有了aceess token你就可以使用你的應(yīng)用服務(wù)了。

注意圖中第4步兌換 access token 的過(guò)程中浑度,很多第三方系統(tǒng)寇窑,如Google ,并不會(huì)僅僅返回 access token箩张,還會(huì)返回額外的信息甩骏,這其中和之后更新相關(guān)的就是 refresh token。一旦 access token過(guò)期先慷,你就可以通過(guò) refresh token 再次請(qǐng)求 access token饮笛。

refresh token

當(dāng)然了,流程是根據(jù)你的請(qǐng)求方式和訪問(wèn)的資源類型而定的论熙,業(yè)務(wù)很多也是不一樣的福青,我這是簡(jiǎn)單的聊聊。

現(xiàn)在這種方法比較常見(jiàn)脓诡,常見(jiàn)的譬如使用QQ快速登陸无午,用的基本的都是這種方法。

開源項(xiàng)目

我們用一個(gè)很火的開源項(xiàng)目Cloud-Admin為栗子祝谚,來(lái)分析一下jwt的應(yīng)用宪迟。

Cloud-Admin是基于Spring Cloud微服務(wù)化開發(fā)平臺(tái),具有統(tǒng)一授權(quán)交惯、認(rèn)證后臺(tái)管理系統(tǒng)次泽,其中包含具備用戶管理、資源權(quán)限管理席爽、網(wǎng)關(guān)API管理等多個(gè)模塊箕憾,支持多業(yè)務(wù)系統(tǒng)并行開發(fā)。

目錄結(jié)構(gòu)

1543763543823

鑒權(quán)中心功能在ace-authace-gate下拳昌。

模型

下面是官方提供的架構(gòu)模型袭异。

image.png

可以看到,AuthServer在架構(gòu)的中心環(huán)節(jié)炬藤,要訪問(wèn)服務(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;

對(duì)應(yīng)數(shù)據(jù)庫(kù):

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í)體,也就是對(duì)應(yīng)著那些微服務(wù)客戶端能調(diào)用哪些微服務(wù)客戶端:

大概對(duì)應(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 ObjectRestResponse<String> createAuthenticationToken(
            @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<String> 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è)接口

先說(shuō)第一個(gè)接口,創(chuàng)建token就珠。

具體邏輯如下:
每一個(gè)用戶登陸進(jìn)來(lái)時(shí)寇壳,都會(huì)進(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("用戶不存在或賬戶密碼錯(cuò)誤!");
    }

下圖是詳細(xì)邏輯圖:

[圖片上傳失敗...(image-153ab0-1543986254420)]

鑒權(quán)中心客戶端代碼

入口

作者寫了個(gè)注解的入口,使用@EnableAceAuthClient即自動(dòng)開啟微服務(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();
    }
}

注解會(huì)自動(dòng)的將客戶端的用戶token和服務(wù)token的關(guān)鍵信息加載到bean中

feigin攔截器

作者重寫了okhttp3攔截器的方法榛丢,每一次微服務(wù)客戶端請(qǐng)求的token都會(huì)被攔截下來(lái)铲球,驗(yàn)證服務(wù)調(diào)用服務(wù)的token和用戶調(diào)用服務(wù)的token是否過(guò)期,過(guò)期則返回新的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容器的攔截器

第二道攔截器是來(lái)自spring容器的晰赞,第一道feign攔截器只是驗(yàn)證了兩個(gè)token是否過(guò)期睬辐,但token真實(shí)的權(quán)限卻沒(méi)驗(yàn)證。接下來(lái)就要驗(yàn)證兩個(gè)token的權(quán)限問(wèn)題了宾肺。

服務(wù)調(diào)用權(quán)限代碼如下:

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        // 配置該注解溯饵,說(shuō)明不進(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;
        // 配置該注解,說(shuō)明不進(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)代碼

該框架中所有的請(qǐng)求都會(huì)走網(wǎng)關(guān)服務(wù)(ace-gatev2)锨用,通過(guò)網(wǎng)關(guān)丰刊,來(lái)驗(yàn)證token是否過(guò)期異常,驗(yàn)證token是否不存在增拥,驗(yàn)證token是否有權(quán)限進(jìn)行服務(wù)啄巧。

下面是核心代碼:

@Override
    public Mono<Void> 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<URI> 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過(guò)期異常", e);
            return getVoidMono(serverWebExchange, new TokenForbiddenResponse("User Token Forbidden or Expired!"));
        }
        List<PermissionInfo> permissionIfs = userService.getAllPermissionInfo();
        // 判斷資源是否啟用權(quán)限約束
        Stream<PermissionInfo> stream = getPermissionIfs(requestUri, method, permissionIfs);
        List<PermissionInfo> 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!"));
            }
        }
        // 申請(qǐng)客戶端密鑰頭
        mutate.header(serviceAuthConfig.getTokenHeader(), serviceAuthUtil.getClientToken());
        ServerHttpRequest build = mutate.build();
        return gatewayFilterChain.filter(serverWebExchange.mutate().request(build).build());

    }
1543848104059

cloud admin總結(jié)

總的來(lái)說(shuō),鑒權(quán)和網(wǎng)關(guān)模塊就說(shuō)完了掌栅。作者代碼構(gòu)思極其精妙秩仆,使用在大型的權(quán)限系統(tǒng)中,可以巧妙的減少耦合性猾封,讓服務(wù)鑒權(quán)粒度細(xì)化澄耍,方便管理。

結(jié)束

此片完了~ 想要了解更多精彩新姿勢(shì)?
請(qǐng)?jiān)L問(wèn)我的個(gè)人博客

本篇為原創(chuàng)內(nèi)容齐莲,已在個(gè)人博客率先發(fā)表痢站,隨后看心情可能會(huì)在CSDN,segmentfault选酗,掘金阵难,簡(jiǎn)書,開源中國(guó)同步發(fā)出芒填。如有雷同呜叫,緣分呢兄弟。趕快加個(gè)好友殿衰,咱們兩個(gè)想個(gè)號(hào)碼朱庆, 買個(gè)彩票,先掙他個(gè)幾百萬(wàn)??

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末播玖,一起剝皮案震驚了整個(gè)濱河市椎工,隨后出現(xiàn)的幾起案子饭于,更是在濱河造成了極大的恐慌蜀踏,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掰吕,死亡現(xiàn)場(chǎng)離奇詭異果覆,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)殖熟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門局待,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人菱属,你說(shuō)我怎么就攤上這事钳榨。” “怎么了纽门?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵薛耻,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我赏陵,道長(zhǎng)饼齿,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任蝙搔,我火速辦了婚禮缕溉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吃型。我一直安慰自己证鸥,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著敌土,像睡著了一般镜硕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上返干,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天兴枯,我揣著相機(jī)與錄音,去河邊找鬼矩欠。 笑死财剖,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的癌淮。 我是一名探鬼主播躺坟,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼乳蓄!你這毒婦竟也來(lái)了咪橙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤虚倒,失蹤者是張志新(化名)和其女友劉穎美侦,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體魂奥,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡菠剩,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了耻煤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片具壮。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖哈蝇,靈堂內(nèi)的尸體忽然破棺而出棺妓,到底是詐尸還是另有隱情,我是刑警寧澤炮赦,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布怜跑,位于F島的核電站,受9級(jí)特大地震影響眼五,放射性物質(zhì)發(fā)生泄漏妆艘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一看幼、第九天 我趴在偏房一處隱蔽的房頂上張望批旺。 院中可真熱鬧,春花似錦诵姜、人聲如沸汽煮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)暇赤。三九已至心例,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鞋囊,已是汗流浹背止后。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留溜腐,地道東北人译株。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像挺益,于是被迫代替她去往敵國(guó)和親歉糜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容