SpringCloud微服務(wù)中如何實(shí)現(xiàn)多端認(rèn)證?
概述
DailyMart是一個ToC的在線購物商城,目前僅支持通過瀏覽器訪問。在商城中的所有操作都需要用戶先登錄。為了實(shí)現(xiàn)這一需求,我們可以采用以下技術(shù)方案:
- 用戶通過SpringCloud Gateway訪問CustomerService進(jìn)行登錄認(rèn)證。認(rèn)證成功后,服務(wù)器返回一個JWT(JSON Web Token)。在接下來的操作中,用戶需要在請求頭中攜帶此Token。
- 在網(wǎng)關(guān)服務(wù)中,我們創(chuàng)建了一個名為ApiAuthenticatorFilter的過濾器。該過濾器用于驗(yàn)證請求頭中是否包含Token,并檢查Token的有效性。如果請求頭中沒有攜帶Token,或者Token失效,則不允許訪問后端接口。
詳細(xì)交互流程如下圖1所示:
圖1:PC認(rèn)證流程
多端認(rèn)證需求
這種架構(gòu)在初期可以滿足業(yè)務(wù)的發(fā)展需求。然而,隨著業(yè)務(wù)的擴(kuò)展,我們需要考慮到現(xiàn)在大部分用戶使用手機(jī)進(jìn)行購物的情況。因此,DailyMart也需要支持手機(jī)端訪問。但與瀏覽器不同,手機(jī)端的認(rèn)證機(jī)制可能會有所不同。
例如,瀏覽器端的Token有效期通常設(shè)定為1小時,而手機(jī)端的Token有效期通常設(shè)置為7天或更長。此外,瀏覽器端的Token采用JWT這種去中心化的認(rèn)證機(jī)制,而手機(jī)端的Token采用中心化的認(rèn)證機(jī)制,需要調(diào)用手機(jī)端服務(wù)進(jìn)行登錄認(rèn)證。
同時,為了擴(kuò)展業(yè)務(wù),其他一些第三方應(yīng)用可能也需要調(diào)用DailyMart的后端服務(wù)來獲取數(shù)據(jù),對于第三方的應(yīng)用一般采用appId + appSecret的方式進(jìn)行認(rèn)證,同時需要對接口參數(shù)進(jìn)行簽名防止出現(xiàn)篡改和重放。(此方案在前文中有詳細(xì)說明,可以通過鏈接跳轉(zhuǎn)訪問查看。)
現(xiàn)在的問題是,如何在原有架構(gòu)的基礎(chǔ)上滿足這三種不同形式的認(rèn)證需求呢?
圖片
解決方案
要解決這個問題,最關(guān)鍵在于如何判斷請求的來源,是來自瀏覽器端的請求、手機(jī)端的請求還是第三方的請求?
我們可以通過請求路徑進(jìn)行區(qū)分,對于不同端的請求使用不同的路徑進(jìn)行標(biāo)識,可以做如下約定:
- 手機(jī)端請求,需要在請求路徑上帶有/ph/
- 瀏覽器請求,需要在請求路徑上帶有/pd/
- 第三方請求,需要在路徑請求上帶有/pt/
- ...
最終規(guī)定接口的完整請求路徑為:/服務(wù)名/api/來源標(biāo)識/接口路徑/,如:http://localhost:9090/customer-service/api/pd/customer/info
這樣在SpringCloud Gateway網(wǎng)關(guān)先獲取請求的路徑,再根據(jù)請求的路徑判斷請求來源,最后根據(jù)請求來源實(shí)現(xiàn)不同的認(rèn)證方案。
解決這個問題的關(guān)鍵在于如何判斷請求的來源,即是來自瀏覽器端、手機(jī)端還是第三方應(yīng)用?
我們可以通過請求路徑進(jìn)行區(qū)分,對于不同端的請求使用不同的路徑進(jìn)行標(biāo)識。例如:
- 手機(jī)端請求,在請求路徑上帶有 /ph/
- 瀏覽器端請求,在請求路徑上帶有 /pd/
- 第三方請求,在請求路徑上帶有 /pt/
- ...
最終,我們規(guī)定接口的完整請求路徑為:/服務(wù)名/api/來源標(biāo)識/接口路徑/,例如:http://localhost:9090/customer-service/api/pd/customer/info
這樣,在SpringCloud Gateway網(wǎng)關(guān)中,我們需要創(chuàng)建一個過濾器,首先獲取請求的路徑,然后根據(jù)請求的路徑判斷請求來源,最后根據(jù)請求來源實(shí)現(xiàn)不同的認(rèn)證方案。
代碼實(shí)現(xiàn)
有了解決方案,我們就很容易完成代碼實(shí)現(xiàn)了。
為了滿足多端認(rèn)證的需求,在網(wǎng)關(guān)服務(wù)中我們可以抽取一個公共的認(rèn)證接口ApiAuthenticator,具體的認(rèn)證邏輯由具體實(shí)現(xiàn)類實(shí)現(xiàn)。
圖片
在上面的類圖中,ProtectedApiAuthenticator用于實(shí)現(xiàn)第三方的認(rèn)證邏輯,DefaultApiAuthenticator用于實(shí)現(xiàn)瀏覽器端的認(rèn)證邏輯。
在網(wǎng)關(guān)過濾器ApiAuthenticatorFilter中,我們首先根據(jù)請求路徑獲取請求來源,然后根據(jù)請求來源找到對應(yīng)的實(shí)現(xiàn)類。
@Component
@Slf4j
public class ApiAuthenticatorFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI uri = exchange.getRequest().getURI();
String rawPath = uri.getRawPath();
// 靜態(tài)接口直接過濾
if (handleExcludeUrl(rawPath)) {
return chain.filter(exchange);
}
// 獲取認(rèn)證邏輯
ApiAuthenticator apiAuthenticator = getApiAuthenticator(rawPath);
AuthenticatorResult authenticatorResult = apiAuthenticator.auth(exchange);
if (!authenticatorResult.isResult()) {
return Mono.error(new HttpServerErrorException(
HttpStatus.METHOD_NOT_ALLOWED, authenticatorResult.getMessage()));
}
return chain.filter(exchange);
}
/**
* 確定認(rèn)證策略
* @param rawPath 請求路徑
*/
private ApiAuthenticator getApiAuthenticator(String rawPath) {
String[] parts = rawPath.split("/");
if (parts.length >= 4) {
String parameter = parts[3];
return switch (parameter) {
case PROTECT_PATH -> new ProtectedApiAuthenticator();
case PRIVATE_PATH -> new PrivateApiAuthenticator();
case PUBLIC_PATH -> new PublicApiAuthenticator();
case DEFAULT_PATH -> new DefaultApiAuthenticator();
default -> throw new IllegalStateException("Unexpected value: " + parameter);
};
}
return new DefaultApiAuthenticator();
}
}
以下是瀏覽器端的認(rèn)證邏輯,它會驗(yàn)證JWT token的有效性。如果token失效,則直接返回錯誤提示給用戶,引導(dǎo)其重新登錄。
@Component
@Slf4j
public class DefaultApiAuthenticator implements ApiAuthenticator {
@Override
public AuthenticatorResult auth(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
HttpHeaders httpHeaders = request.getHeaders();
// 獲取JWT請求頭 Authorization
String token = httpHeaders.getFirst(HttpHeaders.AUTHORIZATION);
if (Objects.nonNull(token)) {
try {
String subjectFromJWT = JwtUtil.getSubjectFromJWT(token);
log.info("用戶請求token: {} , 身份Subject:{}", token, subjectFromJWT);
//重新設(shè)置請求頭
mutateNewHeader(exchange, subjectFromJWT);
return new AuthenticatorResult(true, "認(rèn)證通過");
} catch (ParseException | JOSEException e) {
log.error("token解析失敗");
return new AuthenticatorResult(false, "Token錯誤,請重新登錄!");
}
}
return new AuthenticatorResult(false, "Token為空,請重新登錄!");
}
}
小結(jié)
本文提出了一種靈活、可擴(kuò)展的方案,以滿足 DailyMart 在業(yè)務(wù)發(fā)展過程中的多端認(rèn)證需求。通過使用請求路徑區(qū)分不同端的請求來源,并在 SpringCloud Gateway 網(wǎng)關(guān)中實(shí)現(xiàn)相應(yīng)的過濾器進(jìn)行認(rèn)證,方案具有靈活性、可擴(kuò)展性和可維護(hù)性。