好久沒寫部落格了,因為最近公司要求我學spring cloud
,早點將以前軟體遷移到新的架構上。所以我那個拼命的學吶,總是圖快,很多關鍵的筆記沒有做好記錄,現在又遺忘了很多關鍵的技術點,極其罪惡!
現在想一想,還是踏踏實實的走比較好。這不,今天我冒了個泡,來補一補前面我所學所忘的知識點。
想要解鎖更多新姿勢?請訪問我的部落格。
常見的認證機制
今天我麼聊一聊JWT。
關於JWT,相信很多人都已經看過用過,他是基於json
資料結構的認證規範,簡單的說就是驗證使用者登沒登陸的玩意。這時候你可能回想,哎喲,不是又那個session麼,分散式系統用redis
做分散式session,那這個jwt有什麼好處呢?
請聽我慢慢訴說這歷史!
最原始的辦法--HTTP BASIC AUTH
HTTP BASIC auth,別看它名字那麼長那麼生,你就認為這個玩意很高大上。其實原理很簡單,簡單的說就是每次請求API的時候,都會把使用者名稱和密碼通過restful API
傳給服務端。這樣就可以實現一個無狀態思想,即每次HTTP請求和以前都沒有啥關係,只是獲取目標URI,得到目標內容之後,這次連線就被殺死,沒有任何痕跡。你可別一聽無狀態,正是現在的熱門思想,就覺得很厲害。其實他的缺點還是又的,我們通過http請求傳送給服務端的時候,很有可能將我們的使用者名稱密碼直接暴漏給第三方客戶端,風險特別大,因此生產環境下用這個方法很少。
Session和cookie
session和cookie老生常談了。開始時,都會在服務端全域性建立session物件,session物件儲存著各種關鍵資訊,同時向客戶端傳送一組sessionId
,成為一個cookie物件儲存在瀏覽器中。
當認證時,cookie的資料會傳入服務端與session進行匹配,進而進行資料認證。
此時,實現的是一個有狀態的思想,即該服務的例項可以將一部分資料隨時進行備份,並且在建立一個新的有狀態服務時,可以通過備份恢復這些資料,以達到資料持久化的目的。
缺點
這種認證方法基本是現在軟體最常用的方法了,它有一些自己的缺點:
- 安全性。cookies的安全性不好,攻擊者可以通過獲取本地cookies進行欺騙或者利用cookies進行CSRF攻擊。
- 跨域問題。使用cookies時,在多個域名下,會存在跨域問題。
- 有狀態。session在一定的時間裡,需要存放在服務端,因此當擁有大量使用者時,也會大幅度降低服務端的效能。
- 狀態問題。當有多臺機器時,如何共享session也會是一個問題,也就是說,使用者第一個訪問的時候是伺服器A,而第二個請求被轉發給了伺服器B,那伺服器B如何得知其狀態。
- 移動手機問題。現在的智慧手機,包括安卓,原生不支援cookie,要使用cookie挺麻煩。
Token認證(使用jwt規範)
token 即使是在計算機領域中也有不同的定義,這裡我們說的token,是指 訪問資源的憑據 。使用基於 Token 的身份驗證方法,在服務端不需要儲存使用者的登入記錄。大概的流程是 這樣的:
- 客戶端使用使用者名稱跟密碼請求登入
- 服務端收到請求,去驗證使用者名稱與密碼
- 驗證成功後,服務端會簽發一個 Token,再把這個 Token 傳送給客戶端
- 客戶端收到 Token 以後可以把它儲存起來,比如放在 Cookie 裡
- 客戶端每次向服務端請求資源的時候需要帶著服務端簽發的 Token
- 服務端收到請求,然後去驗證客戶端請求裡面帶著的 Token,如果驗證成功,就向客戶端返回請求的資料
Token機制,我認為其本質思想就是將session中的資訊簡化很多,當作cookie用,也就是客戶端的“session”。
好處
那Token機制相對於Cookie機制又有什麼好處呢?
- 支援跨域訪問: Cookie是不允許垮域訪問的,這一點對Token機制是不存在的,前提 是傳輸的使用者認證資訊通過HTTP頭傳輸.
- 無狀態:Token機制本質是校驗, 他得到的會話狀態完全來自於客戶端, Token機制在服務端不需要儲存session資訊,因為 Token 自身包含了所有登入使用者的資訊,只需要在客戶端的cookie或本地介質儲存狀態資訊.
- 更適用CDN: 可以通過內容分發網路請求你服務端的所有資料(如:javascript, HTML,圖片等),而你的服務端只要提供API即可.
- 去耦: 不需要繫結到一個特定的身份驗證方案。Token可以在任何地方生成,只要在 你的API被呼叫的時候,你可以進行Token生成呼叫即可.
- 更適用於移動應用: 當你的客戶端是一個原生平臺(iOS, Android,Windows 8等) 時,Cookie是不被支援的(你需要通過Cookie容器進行處理),這時採用Token認 證機制就會簡單得多。 CSRF:因為不再依賴於Cookie,所以你就不需要考慮對CSRF(跨站請求偽造)的防 範。
- 效能: 一次網路往返時間(通過資料庫查詢session資訊)總比做一次HMACSHA256 計算 的Token驗證和解析要費時得多. 不需要為登入頁面做特殊處理: 如果你使用Protractor 做功能測試的時候,不再需要 為登入頁面做特殊處理.
- 基於標準化:你的API可以採用標準化的 JSON Web Token (JWT). 這個標準已經存在 多個後端庫(.NET, Ruby, Java,Python, PHP)和多家公司的支援(如: Firebase,Google, Microsoft)
缺陷在哪?
說了那麼多token認證的好處,但他其實並沒有想象的那麼神,token 也並不是沒有問題。
-
佔頻寬
正常情況下要比 session_id 更大,需要消耗更多流量,擠佔更多頻寬,假如你的網站每月有 10 萬次的瀏覽器,就意味著要多開銷幾十兆的流量。聽起來並不多,但日積月累也是不小一筆開銷。實際上,許多人會在 JWT 中儲存的資訊會更多。
-
無論如何你需要運算元據庫
在網站上使用 JWT,對於使用者載入的幾乎所有頁面,都需要從快取/資料庫中載入使用者資訊,如果對於高流量的服務,你確定這個操作合適麼?如果使用redis進行快取,那麼效率上也並不能比 session 更高效
-
無法在服務端登出,那麼久很難解決劫持問題
-
效能問題
JWT 的賣點之一就是加密簽名,由於這個特性,接收方得以驗證 JWT 是否有效且被信任。但是大多數 Web 身份認證應用中,JWT 都會被儲存到 Cookie 中,這就是說你有了兩個層面的簽名。聽著似乎很牛逼,但是沒有任何優勢,為此,你需要花費兩倍的 CPU 開銷來驗證簽名。對於有著嚴格效能要求的 Web 應用,這並不理想,尤其對於單執行緒環境。
JWT
現在我們來說說今天的主角,JWT
JSON Web Token(JWT)是一個非常輕巧的規範。這個規範允許我們使用JWT在用 戶和伺服器之間傳遞安全可靠的資訊
組成
一個JWT實際上就是一個字串,它由三部分組成,頭部、載荷與簽名。
頭部(header)
頭部用於描述關於該JWT的最基本的資訊,例如其型別以及簽名所用的演算法等。這也可以 被表示成一個JSON物件。
{
"typ":"JWT",
"alg":"HS256"
}
複製程式碼
這就是頭部的明文內容,第一部分說明他是一個jwt,第二部分則指出簽名演算法用的是HS256演算法。
然後將這個頭部進行BASE64編碼,編碼後形成頭部:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
複製程式碼
載荷(payload)
載荷就是存放有效資訊的地方,有效資訊包含三個部分:
(1)標準中註冊的宣告(建議但不強制使用)
- iss: jwt簽發者
- sub: jwt所面向的使用者
- aud: 接收jwt的一方
- exp: jwt的過期時間,這個過期時間必須要大於簽發時間
- nbf: 定義在什麼時間之前,該jwt都是不可用的.
- iat: jwt的簽發時間
- jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。
(2)公共的宣告 公共的宣告可以新增任何的資訊,一般新增使用者的相關資訊或其他業務需要的必要資訊. 但不建議新增敏感資訊,因為該部分在客戶端可解密.
(3)私有的宣告
私有宣告是提供者和消費者所共同定義的宣告,一般不建議存放敏感資訊,因為base64 是對稱解密的,意味著該部分資訊可以歸類為明文資訊。
{
"sub":"1234567890",
"name":"tengshe789",
"admin": true
}
複製程式碼
上面就是一個簡單的載荷的明文,接下來使用base64加密:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
複製程式碼
簽證(signature)
jwt的第三部分是一個簽證資訊,這個簽證資訊由三部分組成:
- header (base64後的)
- payload (base64後的)
- secret
這個部分需要base64加密後的header和base64加密後的payload使用.連線組成的字串,然後通過header中宣告的加密方式進行加鹽secret組合加密,然後就構成了jwt的第 三部分。
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
複製程式碼
合成
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I kpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7Hg Q
複製程式碼
實現JWT
現在一般實現jwt,都使用Apache 的開源專案JJWT(一個提供端到端的JWT建立和驗證的Java庫)。
依賴
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
複製程式碼
建立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
}
}
複製程式碼
結果如圖:
(注意,jjwt不支援jdk11,0.9.1以後的jjwt必須實現signWith()方法才能實現)
解析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());
}
}
複製程式碼
結果如圖:
生產中的JWT
在企業級系統中,通常內部會有非常多的工具平臺供大家使用,比如人力資源,程式碼管理,日誌監控,預算申請等等。如果每一個平臺都實現自己的使用者體系的話無疑是巨大的浪費,所以公司內部會有一套公用的使用者體系,使用者只要登陸之後,就能夠訪問所有的系統。
這就是 單點登入(SSO: Single Sign-On)
SSO 是一類解決方案的統稱,而在具體的實施方面,一般有兩種策略可供選擇:
- SAML 2.0
- OAuth 2.0
欲揚先抑,先說說幾個重要的知識點。
Authentication VS Authorisation
-
Authentication: 身份鑑別,鑑權,以下簡稱認證
認證 的作用在於認可你有許可權訪問系統,用於鑑別訪問者是否是合法使用者。負責認證的服務通常稱為 Authorization Server 或者 Identity Provider,以下簡稱 IdP
-
Authorisation: 授權
授權 用於決定你有訪問哪些資源的許可權。大多數人不會區分這兩者的區別,因為站在使用者的立場上。而作為系統的設計者來說,這兩者是有差別的,這是不同的兩個工作職責,我們可以只需要認證功能,而不需要授權功能,甚至不需要自己實現認證功能,而藉助 Google 的認證系統,即使用者可以用 Google 的賬號進行登陸。負責提供資源(API呼叫)的服務稱為 Resource Server 或者 Service Provider,以下簡稱 SP
SMAL 2.0
OAuth(JWT)
OAuth(開放授權)是一個開放的授權標準,允許使用者讓第三方應用訪問該使用者在 某一web服務上儲存的私密的資源(如照片,視訊,聯絡人列表),而無需將使用者名稱和密碼提供給第三方應用。
流程可以參考如下:
簡單的來說,就是你要訪問一個應用服務,先找它要一個request token
(請求令牌),再把這個request token
發到第三方認證伺服器,此時第三方認證伺服器會給你一個aceess token
(通行令牌), 有了aceess token
你就可以使用你的應用服務了。
注意圖中第4步兌換 access token
的過程中,很多第三方系統,如Google ,並不會僅僅返回 access token
,還會返回額外的資訊,這其中和之後更新相關的就是 refresh token
。一旦 access token
過期,你就可以通過 refresh token
再次請求 access token
。
當然了,流程是根據你的請求方式和訪問的資源型別而定的,業務很多也是不一樣的,我這是簡單的聊聊。
現在這種方法比較常見,常見的譬如使用QQ快速登陸,用的基本的都是這種方法。
開源專案
我們用一個很火的開源專案Cloud-Admin為栗子,來分析一下jwt的應用。
Cloud-Admin是基於Spring Cloud微服務化開發平臺,具有統一授權、認證後臺管理系統,其中包含具備使用者管理、資源許可權管理、閘道器API管理等多個模組,支援多業務系統並行開發。
目錄結構
鑑權中心功能在ace-auth
與ace-gate
下。
模型
下面是官方提供的架構模型。
可以看到,AuthServer
在架構的中心環節,要訪問服務,必須需要鑑權中心的JWT鑑權。
鑑權中心服務端程式碼解讀
實體類
先看實體類,這裡鑑權中心定義了一組客戶端實體,如下:
@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;
複製程式碼
對應資料庫:
CREATE TABLE `auth_client` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` varchar(255) DEFAULT NULL COMMENT '服務編碼',
`secret` varchar(255) DEFAULT NULL COMMENT '服務金鑰',
`name` varchar(255) DEFAULT NULL COMMENT '服務名',
`locked` char(1) DEFAULT NULL COMMENT '是否鎖定',
`description` varchar(255) DEFAULT NULL COMMENT '描述',
`crt_time` datetime DEFAULT NULL COMMENT '建立時間',
`crt_user` varchar(255) DEFAULT NULL COMMENT '建立人',
`crt_name` varchar(255) DEFAULT NULL COMMENT '建立人姓名',
`crt_host` varchar(255) DEFAULT NULL COMMENT '建立主機',
`upd_time` datetime DEFAULT NULL COMMENT '更新時間',
`upd_user` varchar(255) DEFAULT NULL COMMENT '更新人',
`upd_name` varchar(255) DEFAULT NULL COMMENT '更新姓名',
`upd_host` varchar(255) DEFAULT NULL COMMENT '更新主機',
`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;
複製程式碼
這些是每組微服務客戶端的資訊
第二個實體類,就是客戶端_服務的實體,也就是對應著那些微服務客戶端能呼叫哪些微服務客戶端:
大概對應的就是微服務間呼叫許可權關係。
@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<>();
}
}
複製程式碼
這裡放出了三個介面
先說第一個介面,建立token
。
具體邏輯如下:
每一個使用者登陸進來時,都會進入這個環節。根據request中使用者的使用者名稱和密碼,利用feign
客戶端的攔截器攔截request,然後使用作者寫的JwtTokenUtil
裡面的各種方法取出token中的key和金鑰,驗證token是否正確,正確則用authService.login(authenticationRequest);
的方法返回出去一個新的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("使用者不存在或賬戶密碼錯誤!");
}
複製程式碼
下圖是詳細邏輯圖:
鑑權中心客戶端程式碼
入口
作者寫了個註解的入口,使用@EnableAceAuthClient
即自動開啟微服務(客戶端)的鑑權管理
@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和服務token的關鍵資訊載入到bean中
feigin攔截器
作者重寫了okhttp3
攔截器的方法,每一次微服務客戶端請求的token都會被攔截下來,驗證服務呼叫服務的token和使用者呼叫服務的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攔截器只是驗證了兩個token是否過期,但token真實的許可權卻沒驗證。接下來就要驗證兩個token的許可權問題了。
服務呼叫許可權程式碼如下:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 配置該註解,說明不進行服務攔截
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!");
}
複製程式碼
使用者許可權:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 配置該註解,說明不進行使用者攔截
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閘道器程式碼
該框架中所有的請求都會走閘道器服務(ace-gatev2),通過閘道器,來驗證token是否過期異常,驗證token是否不存在,驗證token是否有許可權進行服務。
下面是核心程式碼:
@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();
// 不進行攔截的地址
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<PermissionInfo> permissionIfs = userService.getAllPermissionInfo();
// 判斷資源是否啟用許可權約束
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!"));
}
}
// 申請客戶端金鑰頭
mutate.header(serviceAuthConfig.getTokenHeader(), serviceAuthUtil.getClientToken());
ServerHttpRequest build = mutate.build();
return gatewayFilterChain.filter(serverWebExchange.mutate().request(build).build());
}
複製程式碼
cloud admin總結
總的來說,鑑權和閘道器模組就說完了。作者程式碼構思極其精妙,使用在大型的許可權系統中,可以巧妙的減少耦合性,讓服務鑑權粒度細化,方便管理。
結束
此片完了~ 想要了解更多精彩新姿勢?
請訪問我的個人部落格
本篇為原創內容,已在個人部落格率先發表,隨後看心情可能會在CSDN,segmentfault,掘金,簡書,開源中國同步發出。如有雷同,緣分呢兄弟。趕快加個好友,我們們兩個想個號碼, 買個彩票,先掙他個幾百萬?