這是悟空的第 162 篇原創(chuàng)文章 官網(wǎng):www.passjava.cn 你好,我是悟空。 前言上篇我已經(jīng)講解了 Spring Cloud 的原理和實(shí)戰(zhàn),這次就要結(jié)合 JWT 來實(shí)現(xiàn)登錄認(rèn)證的功能了。 本文已收錄至《深入剖析 Spring Cloud 底層架構(gòu)原理》,已更新 17 講。 通過本文你會掌握以下知識點(diǎn):
本篇還是基于我的開源項(xiàng)目 PassJava 作為講解。
前置知識點(diǎn): 在講解之前有必要澄清下什么是認(rèn)證、授權(quán)、憑證,這三個方面是一個系統(tǒng)中最基礎(chǔ)的安全設(shè)計(jì)。 認(rèn)證、授權(quán)、憑證1.1 認(rèn)證(Authentication)認(rèn)證表示你是誰。系統(tǒng)如何正確分辨出操作用戶的真實(shí)身份,比如通過輸入用戶名和密碼來辨別身份。 1.2 授權(quán)(Authorization)授權(quán)表示你能干什么。系統(tǒng)如何控制一個用戶能看到哪些數(shù)據(jù)和操作哪些功能,也就是具有哪些權(quán)限。 1.3 憑證(Credential)表示你如何證明你的身份。系統(tǒng)如何保證它與用戶之間的承諾是雙方當(dāng)時真實(shí)意圖的體現(xiàn),是準(zhǔn)確、完整和不可抵賴的。 接下來我們看下使用 JWT 作為憑證完成認(rèn)證的原理。 認(rèn)證的原理在如下的認(rèn)證時序圖中,有以下幾種角色:
認(rèn)證和校驗(yàn)身份的流程如下所示: ① 用戶登錄:客戶端在登錄頁面輸入用戶名和密碼,提交表單,調(diào)用登錄接口。 ② 轉(zhuǎn)發(fā)請求:這里會先將登錄請求發(fā)送到網(wǎng)關(guān)服務(wù) passjava-gateway,網(wǎng)關(guān)對于登錄請求會直接轉(zhuǎn)發(fā)到認(rèn)證服務(wù) passjava-auth。(網(wǎng)關(guān)對登錄請求不做 token 校驗(yàn),這個可以配置不校驗(yàn)?zāi)男┱埱?URL) ③ 認(rèn)證:認(rèn)證服務(wù)會將請求參數(shù)中的用戶名+密碼和數(shù)據(jù)庫中的用戶進(jìn)行比對,如果完全匹配,則認(rèn)證通過。 ④ 生成令牌:生成兩個令牌: ⑤ 客戶端緩存 token:客戶端拿到兩個 token 緩存到 cookie 中或者 LocalStorage 中。 ⑥ 攜帶 token 發(fā)起請求:客戶端下次想調(diào)用業(yè)務(wù)服務(wù)時,將 access_token 放到請求的 header 中。 ⑦ 網(wǎng)關(guān)校驗(yàn) token:請求還是先到網(wǎng)關(guān)服務(wù),然后由它校驗(yàn) access_token 是否合法。如果 access_token 未過期,且能正確解析出來,就說明是合法的 access_token。 ⑧ 攜帶用戶身份信息轉(zhuǎn)發(fā)請求:網(wǎng)關(guān)將 access_token 中攜帶的用戶的 user_id 放到請求的 header 中,轉(zhuǎn)發(fā)給真正的業(yè)務(wù)服務(wù)。 ⑨ 處理業(yè)務(wù)邏輯:業(yè)務(wù)服務(wù)從 header 中拿到用戶的 user_id,然后處理業(yè)務(wù)邏輯,處理完后將結(jié)果原路返回給客戶端。 接下來我們看下項(xiàng)目的整體架構(gòu)。 項(xiàng)目整體結(jié)構(gòu)
認(rèn)證服務(wù):passjava-auth核心類就是 JwtAuthController 類,里面有登錄接口和刷新令牌的接口。 網(wǎng)關(guān)服務(wù):passjava-gateway核心類就是 JwtAuthCheckFilter 全局過濾器。 如果不需要在服務(wù)端保存刷新令牌,可以不需要 redis 配置。 JWT 公共項(xiàng)目核心類就是 PassJavaJWTTokenUtil 工具類。認(rèn)證服務(wù)引入 JWT 項(xiàng)目后用來生成 token,網(wǎng)關(guān)服務(wù)引入 JWT 項(xiàng)目后用來校驗(yàn) token 合法性。 業(yè)務(wù)服務(wù)這里我選擇了會員微服務(wù)作為本次演示的業(yè)務(wù)微服務(wù)。 它從網(wǎng)關(guān)轉(zhuǎn)發(fā)的請求 Header 中拿到 userId, 根據(jù) userId 查詢 member 信息。 核心文件是 MemberController 類、MemberEntity實(shí)體類、MemberService服務(wù)類、MemberDao 類和 mapper 文件。 啟動的服務(wù)Nacos 注冊配置中心首先啟動 Nacos 服務(wù)。和 PassJava 項(xiàng)目配套使用的 Nacos 工具已經(jīng)上傳到網(wǎng)盤,下載后直接運(yùn)行啟動腳本就可以將 Nacos 在本地啟動。 啟動教程:
網(wǎng)關(guān)、會員、認(rèn)證服務(wù)啟動以下三個微服務(wù),分別為網(wǎng)關(guān)、會員、認(rèn)證服務(wù)。 檢查下 nacos 注冊中心上是否注冊了這三個服務(wù):可以看到確實(shí)有上面的三個微服務(wù)。 如何做登錄認(rèn)證登錄認(rèn)證就是校驗(yàn)下用戶提交的賬戶名和密碼與本地?cái)?shù)據(jù)庫中的是否完全匹配,如果匹配,就認(rèn)證通過。就是下方這個流程的 1、2、3 步。 第一步:提交用戶名和密碼這里用 Postman 工具模擬前端發(fā)起登錄請求,請求的 URL 如下:
請求是向網(wǎng)關(guān)服務(wù) passjava-gateway 發(fā)起的,所以可以看到上面的 URL 中 localhost 和 8060 是網(wǎng)關(guān)的 host 和 port。 然后 API 地址為 /api/auth/login,這個地址經(jīng)過網(wǎng)關(guān)的路由匹配后會轉(zhuǎn)發(fā)到 passjava-auth 服務(wù)的登錄 API。
關(guān)于網(wǎng)關(guān)轉(zhuǎn)發(fā)的原理可以參考這篇:深入理解 Spring Cloud Gateway 的原理 請求參數(shù)如下:
賬號和密碼都是密文的,轉(zhuǎn)發(fā)到認(rèn)證服務(wù)后,會根據(jù) userId 查詢出系統(tǒng)用戶,然后將 password 參數(shù)加密后對比系統(tǒng)用戶的密碼。 所以為了讓用戶登錄成功,還需要在數(shù)據(jù)庫插入一條系統(tǒng)用戶,用戶 id 為 wukong,密碼是對 123456 加密后的密碼。 在線加密工具地址:
第二步:轉(zhuǎn)發(fā)登錄請求轉(zhuǎn)發(fā)登錄請求是網(wǎng)關(guān)服務(wù)做的,所以我們來看下做了哪些事情。 在 Gateway 項(xiàng)目的 application-routers.yml 中配置路由規(guī)則:
在 application.properties 引入 application-routers.yml
第三步:驗(yàn)證用戶賬號和密碼這一步是認(rèn)證服務(wù)的登錄 API 里面做的。在 AuthController 中定義 login 接口,核心步驟就是查找系統(tǒng)用戶和比對密碼。 用戶名和密碼匹配成功后,就會生成 JWT 令牌。 如何生成令牌生成令牌就是通過工具類 PassJavaJwtTokenUtil 生成 JWT Token,也就是流程圖中的第四步。 生成令牌的核心代碼如下: 使用這個工具類的前提是我們需要先引入 jjwt 依賴。這個在 passjava-jwt 項(xiàng)目的 pom 文件中引入。 用 Postman 工具調(diào)用后,可以看到生成的令牌如下: 用 base64 解碼后,可以看到 token 中的 PAYLOAD 里面包含了用戶 id 和用戶名。 生成 JWT 的加密密鑰一般都是寫到配置文件中。這里我是配置在 passjava-jwt 項(xiàng)目的 application-jwt.yml 配置文件中的。 然后認(rèn)證服務(wù)就會將 JWT 令牌返回給客戶端了。當(dāng)客戶端想要查詢這個 userId 對應(yīng)的會員信息時,就可以在請求的 Header 中帶上 JWT 令牌。 如何攜帶 JWT 發(fā)送請求客戶端(瀏覽器或 APP)拿到 JWT 后,可以將 JWT 存放在瀏覽器的 Cookie 或 LocalStorage(本地存儲) 或者內(nèi)存中。 發(fā)送請求時在請求 Header 的 Authorization 字段中設(shè)置 JWT,這個字段其實(shí)可以自定義,但是我建議用 Authorization,因?yàn)檫@是一種業(yè)界標(biāo)準(zhǔn)。 另外告訴大家一個小技巧,在 Postman 工具中有個地方專門配置 Authorization,然后自動加到 Header 中,不用自己手動加 Header。 還有一個點(diǎn)需要注意,這里配置的 Authorization 的認(rèn)證類型為 Bearer Token。它表示令牌可以是任意字符串格式的令牌。然后會在 Authorization 字段中加上一個前綴 Bearer。所以我們在網(wǎng)關(guān)服務(wù)解析 Header 中的 Authorization 時,需要去掉這個前綴 Bearer,代碼如下所示: 網(wǎng)關(guān)如何驗(yàn)證 JWT 和轉(zhuǎn)發(fā)請求網(wǎng)關(guān)接收到前端發(fā)起的業(yè)務(wù)請求后,會先驗(yàn)證請求的 Header 中是否攜帶 Authorization 字段,以及里面的 Token 是否合法。然后解析 Token 中的 userId 和 username,放到 header 中再進(jìn)行轉(zhuǎn)發(fā),也就是流程圖中的第七步和第八步。 網(wǎng)關(guān)是通過多個 網(wǎng)關(guān)的全局過濾器 JwtAuthCheckFilter 的核心代碼如下所示: 會員服務(wù)處理業(yè)務(wù)邏輯會員服務(wù)接收到網(wǎng)關(guān)轉(zhuǎn)發(fā)的請求后,就從 Header 中拿到用戶身份信息,然后通過 userId 獲取會員信息。
獲取 userId 的方式其實(shí)可以通過加一個 獲取 userId 的方式:
Request 中獲取 userId 方式代碼示例如下: 下面介紹如何使用攔截器方式將 userId 存入線程變量的方式。 攔截器方式在 passjava-common 模塊中新增一個攔截器,獲取請求頭中的身份信息,加入到線程變量中。文件名為 HeaderInterceptor。 將攔截器注冊到 WebMvcConfigurer。文件名為 WebMvcConfig.java。 配置文件中需要定義一個配置項(xiàng):
然后 passjava-member 服務(wù)引入這個攔截器配置。
通過上面兩種方式中的任意一種拿到 userId 后,通過 userId 查詢會員的詳情。這里需要注意的是這個 user 既是系統(tǒng)用戶也是系統(tǒng)中的會員。關(guān)于查詢會員的數(shù)據(jù)庫操作就不在此展開了。 執(zhí)行結(jié)果如下圖所示: 如何刷新令牌還有一個內(nèi)容是關(guān)于如何刷新令牌的。當(dāng)認(rèn)證服務(wù)返回給客戶端的 JWT 也就是 access_token 過期后,客戶端是通過發(fā)送登錄請求重新拿到 access_token 嗎? 這種重新登錄的操作如果很頻繁(因 JWT 過期時間較短),對于用戶來說體驗(yàn)就很差了??蛻舳诵枰D(zhuǎn)到登錄頁面,讓用戶重新提交用戶名和密碼,即使客戶端有記住用戶名和密碼,但是這種跳轉(zhuǎn)到登錄頁的操作會大幅度降低用戶的體驗(yàn),甚至導(dǎo)致用戶不想再用第二次。
我們知道 JWT 生成后是不能篡改里面的內(nèi)容,即使是 JWT 的有效期也不行。所以延長 access_token 有效期的做法并不適合,而且如果長期保持一個 access_token 有效,也是不安全的。 那就只能重新生成 access_token 了。方案其實(shí)挺簡單,客戶端拿之前生成的 JWT 調(diào)用后端一個接口,然后后端校驗(yàn)這個 JWT 是否合法,如果是合法的就重新生成一個新的返回給客戶端??蛻舳俗孕刑鎿Q掉之前本地保存的 access_token 就可以了。 這里有一個巧妙的設(shè)計(jì),就是生成 JWT 時,返回了兩個 JWT token,一個 access_token,一個 refresh_token,這兩個 token 其實(shí)都可以用來刷新 token,但是我們把 refresh_token 設(shè)置的過期時間稍微長一點(diǎn),比如兩倍于 access_token,當(dāng) access_token 過期后,refresh_token 如果還沒有過期,就可以利用兩者的過期時間差進(jìn)行重新生成令牌的操作,也就是 饑餓模式和懶模式當(dāng)然,在 access_token 過期之前,客戶端提前刷新令牌也是可以的,我稱這種提前刷新的模式為 刷新令牌的操作完全是通過客戶端自己控制的,而且客戶端也不僅限于瀏覽器,還有可能是第三方服務(wù)。 一次性通常情況下,我們會將刷新令牌 refresh_token 設(shè)置為只能用一次,來保證刷新令牌的安全性。而這種就需要服務(wù)端來緩存刷新令牌了,當(dāng)用過一次后,就從緩存里面主動剔除掉。但這樣就違背了 JWT 無狀態(tài)的特性,這個完全看業(yè)務(wù)需求來決定是否使用這種緩存方式。 如下圖所示,生成令牌時我將刷新令牌緩存到了 Redis 里面。當(dāng)我用 refresh_token 調(diào)用刷新 API 時,會主動剔除掉這個 key,下次再用相同的 refresh_token 刷新令牌時,因 Redis 中不存在這個 key,就會提示刷新刷新失敗了。 留兩個小問題:
總結(jié)雖然本篇是講實(shí)戰(zhàn)內(nèi)容的,但是里面又涉及了很多原理性內(nèi)容,比如網(wǎng)關(guān)、JWT 的原理。 結(jié)合實(shí)戰(zhàn)講解,相信大家對如何使用 Spring Cloud Gateway + JWT 實(shí)現(xiàn)登錄認(rèn)證有了充分的理解。 本篇只講解了認(rèn)證和憑證,授權(quán)部分還沒有觸及,所以這也是下篇要講解的內(nèi)容,來追更吧~ |
|