上一篇中,我們構(gòu)建了一個簡單的Spring Cloud Demo項目茎辐,涵蓋了服務(wù)注冊/發(fā)現(xiàn)缕允,服務(wù)間的相互調(diào)用峡扩,以及熔斷降級等內(nèi)容。但如果服務(wù)需要暴露給外部進行使用障本,比如移動端教届,或者web端响鹃,則還需要考慮更多的事情。整個服務(wù)端的部署情況對于外部調(diào)用方應(yīng)該是一個黑盒案训,外部調(diào)用方無法了解到每個服務(wù)具體是部署到哪一個IP或者域名下面买置,為了安全性也不太可能允許外部調(diào)用方直接連接到Consul去查詢服務(wù)注冊的情況,這樣我們就需要一個服務(wù)網(wǎng)關(guān)來集中對外部請求進行路由和負載均衡强霎,同時驗證調(diào)用方的權(quán)限和身份堕义。如下圖所示:
基礎(chǔ)介紹
服務(wù)網(wǎng)關(guān)的概念有點類似于傳統(tǒng)的反向代理服務(wù)器(如nginx),但反向代理一般都只是做業(yè)務(wù)無關(guān)的轉(zhuǎn)發(fā)請求,而服務(wù)網(wǎng)關(guān)與服務(wù)的整合程度更高脆栋,可以看作也是整個服務(wù)體系的組成部分,通過過濾器等組件可以在網(wǎng)關(guān)中集成一些業(yè)務(wù)處理的操作(比如權(quán)限認證等)洒擦。Spring Cloud Gateway正是Spring官方推出的服務(wù)網(wǎng)關(guān)的實現(xiàn)框架椿争,它主要包含三個核心的概念:
- Route: 負責將某個外部請求路由到一個合適的地址,包含一個ID,一個目標地址熟嫩,一系列的Predicate和Filter秦踪;
- Predicate: 基于Java 8 Function Predicate的斷言機制,用于將請求匹配到某一個Route
- Filter: 類似于Servlet filter掸茅,可以在請求傳遞給下一級處理器之前對請求或響應(yīng)進行修改椅邓,用于實現(xiàn)權(quán)限驗證,日志記錄昧狮,限流等功能
整個工作流程如下圖所示:
網(wǎng)關(guān)集成
我們現(xiàn)在來為我們的demo項目加入一個服務(wù)網(wǎng)關(guān)景馁。首先需要創(chuàng)建一個新的模塊,名字叫Gateway逗鸣,在pom.xml中加入如下依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
在application.yml中加入如下內(nèi)容:
server:
port: 9000
spring:
application:
name: gateway
cloud:
consul:
host: 192.168.1.220
port: 8500
discovery:
prefer-ip-address: true
gateway:
routes:
- id: order-service
#lb協(xié)議會激活LoadBalancerClient來解析后續(xù)的地址合住,自動根據(jù)注冊的服務(wù)實例進行負載均衡
uri: lb://order-service
filters:
- Log
# 轉(zhuǎn)發(fā)時去掉請求地址的服務(wù)名前綴
- StripPrefix=1
predicates:
- Path=/order-service/**
從以上配置可以很容易看出來,gateway模塊其實也會注冊到consul中成為一個服務(wù)撒璧,并通過consul獲取其它服務(wù)的相關(guān)信息透葛。上面的配置中我們加入了一個名為order-service的路由,其中predicates定義了這個路由的匹配規(guī)則卿樱,也就是訪問路徑以/order-service/開頭的請求僚害,就會被路由到 lb://order-service的地址 (地址代表的含義參見注釋)。
斷言
predicates用于定義route的匹配規(guī)則繁调,可以針對請求的幾乎所有內(nèi)容進行匹配萨蚕,例如針對特定的header進行匹配:
predicates:
- Header=X-Request-Id, \d+**
針對Cookie進行匹配:
predicates:
- Cookie=mycookie,mycookievalue
匹配特定域名的請求
predicates:
- Host=**.somehost.org,**.anotherhost.org
更多predicates種類的介紹可以查看 這里
過濾器
剛才的路由配置中,我們定義了兩個過濾器: Log涉馁,StripPrefix门岔,這些都屬于GatewayFilter,每個Route可以定義多個GatewayFilter烤送。Spring Cloud Gateway已經(jīng)內(nèi)置了多個很有用的GatewayFilter實現(xiàn)寒随,例如StripPrefix就是內(nèi)置的用于轉(zhuǎn)發(fā)時修改請求地址的過濾器。其它內(nèi)置過濾器的作用可以查看 這里。如果內(nèi)置過濾器不能滿足我們的需求妻往,那就需要自行實現(xiàn)新的過濾器了互艾。
我們現(xiàn)在來添加一個簡單的過濾器日志過濾器,用于打印出每次請求所花費的時間:
@Slf4j
public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
private static final String REQUEST_START_TIME = "request_start_time";
public LogGatewayFilterFactory() {
// 這里需要將自定義的config傳過去讯泣,否則會報告ClassCastException
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
exchange.getAttributes().put(REQUEST_START_TIME, System.currentTimeMillis());
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(REQUEST_START_TIME);
if (startTime != null) {
log.info("請求地址:{},消耗時間:{}ms", exchange.getRequest().getURI(), System.currentTimeMillis() - startTime);
}
})
);
};
}
public static class Config {
}
}
自定義過濾器需要實現(xiàn)一個新的GatewayFilterFactory纫普,其類名也需要遵循XXXGatewayFilterFactory的規(guī)則,這樣的話在配置中只需要配置“XXX”的部分就可以正常被識別了好渠,例如 LogGatewayFilterFactory就只需要配置成“Log”就行了昨稼。代碼中的內(nèi)部類Config是用于接收配置時傳遞的參數(shù)(類似于Log=true),這里不需要參數(shù)所以只是一個空類拳锚。需要注意的是Spring Cloud Gateway是使用 Spring WebFlux 來構(gòu)建的假栓,所以filter這里的寫法是基于Reactor異步模式的,和傳統(tǒng)的同步請求模式(如Spring MVC)不太一樣霍掺。
定義了新的過濾器之后需要將其注冊到容器:
@Bean
public LogGatewayFilterFactory logGatewayFilterFactory() {
return new LogGatewayFilterFactory();
}
GatewayFilter都是基于Route進行配置的匾荆,Spring Cloud Filter還定義了一種GlobalFilter,不需要在配置文件中配置杆烁,作用在所有的路由上牙丽。GlobalFilter同樣支持自定義新的過濾器,只需要實現(xiàn)GlobalFilter和Ordered接口即可兔魂,詳細情況我們后面在講到權(quán)限的時候再介紹烤芦。
權(quán)限管理
服務(wù)網(wǎng)關(guān)的一大作用就是可以對外部的請求進行集中權(quán)限認證,這樣每個具體的服務(wù)就不用操心權(quán)限管理的問題了析校,可以專心于業(yè)務(wù)的實現(xiàn)拍棕。基本的思路是外部客戶端首先需要獲取一個由系統(tǒng)中獨立的認證中心負責簽發(fā)的accessToken勺良,然后每次請求服務(wù)時在http header中攜帶該Token绰播,服務(wù)網(wǎng)關(guān)負責校驗accessToken的有效性以及是否具備訪問該服務(wù)的權(quán)限,具體的思路和我之前介紹單系統(tǒng)權(quán)限管理的思路比較類似尚困,可以查看 Spring Boot整合Shiro和JWT的無狀態(tài)權(quán)限管理方案 這篇文章蠢箩。
我們首先需要在服務(wù)網(wǎng)關(guān)中定義一個GlobalFilter對所有的外部請求進行過濾,代碼如下:
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private AuthService authService;
private AuthConfigProperties authConfig;
public AuthGlobalFilter(AuthConfigProperties authConfig, AuthService authService) {
this.authConfig = authConfig;
this.authService = authService;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String reqPath = exchange.getRequest().getURI().getPath();
String token = exchange.getRequest().getHeaders().getFirst(authConfig.getHeaderKeyOfToken());
if (!authService.verifyToken(reqPath, token)) {
log.warn("沒有授權(quán)的訪問事甜,{}", reqPath);
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
//獲取token中存儲的用戶唯一標識谬泌,并放入request header中,供后端業(yè)務(wù)服務(wù)使用
String account = authService.getAccountByToken(token);
ServerHttpRequest request = exchange.getRequest().mutate()
.header(authConfig.getHeaderKeyOfAccount(), account).build();
return chain.filter(exchange.mutate().request(request).build());
}
/**
* 過濾器的優(yōu)先級逻谦,越低越高
*/
@Override
public int getOrder() {
return 1;
}
}
功能很簡單掌实,就是對請求頭部的token進行校驗,如果成功就將從token中解析出來的用戶賬戶信息放入轉(zhuǎn)發(fā)的請求頭中供后端的業(yè)務(wù)服務(wù)使用邦马,否則返回UNAUTHORIZED贱鼻。這個Filter也需要注冊到容器中:
@Bean
public AuthGlobalFilter authGlobalFilter(AuthService authService) {
return new AuthGlobalFilter(authConfig, authService);
}
對token進行校驗的核心邏輯在authService.verifyToken方法中宴卖,代碼如下:
/**
* 驗證token的有效性及是否具備對該url的訪問權(quán)限,
* 判定規(guī)則參考了shiro的一些設(shè)定
*/
public boolean verifyToken(String url, String token) {
if (Strings.isNullOrEmpty(token)) {
return false;
}
//獲取每個Url所對應(yīng)的權(quán)限控制符
String urlPermission = getUrlPermission(url);
if ("anno".equals(urlPermission)) {
return true;
} else {
//獲取token中包含的用戶唯一標識
String account = jwtHelper.getAccount(token);
if (Strings.isNullOrEmpty(account)) {
return false;
}
//獲取token的加密密鑰
String secret = getUserSecret(account);
//校驗accessToken
if (jwtHelper.verify(token, secret) == null) {
return false;
}
// 如果url僅要求驗證用戶有效性邻悬,則直接通過
if (Strings.isNullOrEmpty(urlPermission) ||
"authc".equals(urlPermission)) {
return true;
}
// 進一步判斷用戶權(quán)限
if (urlPermission.startsWith("perms")) {
Set<String> userPerms = this.getUserPermissions(account);
String perms = urlPermission.substring(urlPermission.indexOf("[") + 1, urlPermission.lastIndexOf("]"));
return userPerms.containsAll(Arrays.asList(perms.split(",")));
}
}
return false;
}
服務(wù)網(wǎng)關(guān)首先需要知道不同的服務(wù)地址需要什么樣的權(quán)限才允許訪問症昏,這里采用了類似Shiro配置的格式,類似這樣如下的格式父丰,實際環(huán)境中可能是從數(shù)據(jù)庫或配置文件中讀雀翁贰:
/**
* 獲取所有的接口url與用戶權(quán)限的映射關(guān)系,格式仿造了shiro的權(quán)限配置格式
*/
public Map<String, String> getAllUrlPermissionsMap() {
Map<String, String> urlPermissionsMap = Maps.newHashMap();
urlPermissionsMap.put("/api/order/orders", "authc");
urlPermissionsMap.put("/api/order/create-order", "perms[order]");
urlPermissionsMap.put("/api/storage/**", "perms[storage]");
return urlPermissionsMap;
}
通過Spring 提供的工具類AntPathMatcher,就可以查詢到每個請求url所需要的權(quán)限標識符蛾扇,再根據(jù)權(quán)限標識符去檢查token對應(yīng)的用戶是否具備相應(yīng)的權(quán)限攘烛。對這部分感興趣的同學可以去查看源碼。
本文的相關(guān)代碼可以查看這里 spring-cloud-demo