網關的角色是作為一個 API 架構,用來保護、增強和控制對于 API 服務的訪問
API 網關是一個處于應用程式或服務(提供 REST API 介面服務)之前的系統,用來管理授權、訪問控制和流量限制等,這樣 REST API 介面服務就被 API 網關保護起來,對所有的呼叫者透明,因此,隱藏在 API 網關后面的業務系統就可以專注于創建和管理服務,而不用去處理這些策略性的基礎設施.,主要的功能大概下面這幾點
- 性能:API高可用,負載均衡,容錯機制,
- 安全:權限身份認證、脫敏,流量清洗,后端簽名
- 黑名單,
- 日志:日志記錄一旦涉及分布式,全鏈路跟蹤必不可少,
- 快取:資料快取,監控:記錄請求回應資料,api耗時分析,性能監控,
- 限流:流量控制,錯峰流控,可以定義多種限流規則,
- 灰度:線上灰度部署,可以減小風險,
- 路由:動態路由規則,
這篇文章主要的內容是spring cloud gateway 對請求引數解密,回傳引數加密處理以及密鑰下發處理
一,搭建基本gateway專案
1.1 maven
<!--gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
1.2本地appcation.yml配置
server:
port: 8087
spring:
profiles:
active: dev
application:
name: nacos-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
1.3本地bootstrap.properties配置
因為專案是整合的nacos,所以這里網關的配置也從nacos取
#專案名
spring.application.name=nacos-gateway
#配置中心地址
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
#組態檔尾綴
spring.cloud.nacos.config.file-extension=yml
#配置組
spring.cloud.nacos.config.group=DEV
#命名空間
#spring.cloud.nacos.config.namespace: 7a36049b-ce40-4a3a-97d0-c86858fdcc0f
spring.cloud.nacos.config.refresh-enabled=true
spring.cloud.nacos.config.extension-configs[0].data-id=nacos-gateway-commons.yml
spring.cloud.nacos.config.extension-configs[0].group=DEV
spring.cloud.nacos.config.extension-configs[0].refresh=true
spring.cloud.nacos.config.extension-configs[1].data-id=nacos-gateway-route.yml
spring.cloud.nacos.config.extension-configs[1].group=DEV
spring.cloud.nacos.config.extension-configs[1].refresh=true
這里引入nacos-gateway-route.yml配置如下
nacos-gateway-route.yml
這個是網關路由專案的配置
spring:
cloud:
gateway:
discovery:
locator:
enabled: false
#lower-case-service-id: true
routes: #路由陣列
- id: nacos-user #路由
uri: lb://nacos-user #lb 指的是負載均衡 后面是nacos的服務名
order: 1 #路由優先級 數字越小越大
predicates: #斷言陣列(條件判斷,回傳值boolean ,轉發請求滿足條件的)
- Path=/nacos-user/**
filters: #過濾器 陣列 (在請求傳遞程序中,對請求做修改)
- StripPrefix=1 #在請求轉發之前去掉 gateway的一層路徑
- id: nacos-order #路由
uri: lb://nacos-order #lb 指的是負載均衡 后面是nacos的服務名
order: 1 #路由優先級 數字越小越大
predicates: #斷言陣列(條件判斷,回傳值boolean ,轉發請求滿足條件的)
- Path=/nacos-order/**
filters: #過濾器 陣列 (在請求傳遞程序中,對請求做修改)
- StripPrefix=1 #在請求轉發之前去掉 gateway的一層路徑
這個時候訪問
localhost:8083/nacos-user/test會轉發到nacos-user應用下的/test介面
localhost:8083/nacos-order/test會轉發到nacos-order應用下的/test介面
二,請求回傳加解密

先給大家看一下這樣設計的結果
//請求引數
{
"data": "q9fGLvECb5vDcVh1EKa9Rhf5R56mz6dmRV3G2onljALO7ReKEjqykwBIlL/Jz7wdle7jd03PQhczx0KvrU4JUfMh11YWq5MGkQsid1sENkg=",
"sign": "AD6A2E46B56FF164476B8A441F45AD77"
}
//回傳引數
{
"data": "S73h3QYbroWrDnnPfRGKKE/lCOWdpOqVe1hQg2vNuDvdLZPe2uHZ94UNGNIm+tjpJ+caeWoToD53cLi0rwEjCF0jPnOQ4h0lvgAeOP6Ov9StU/yTKDZ+YV3490j8XO5wb367fI5TNyp5cpgWFrw+Y2OAbIDKVCjmUHOpkh3m9zv8frhOEtLyw7bavOsrgKeCh6PS5ND8Mk4pxdjDoD1MFKapIxGFv0X+VktRgYmaSfUR/n/zj3KKxHAC6Ear0MRuDvpVZZEe01OepB9wnkGBdNcL3rqnAQspu5zwUJ7WZS0+g3elsqEgn7Ar7yYK75TgWmlzmCNQv9GKN+V4oG4UW1l7SzLRsD+grHRwaGdFLkHqIYPdAlo1XKXPgHWI/nwS+ANVfliZo/76dAru4d889werCOCyuuxHR1f1ML/7b3TfvQ8NffH2y2ELU9/FvTr0L4pAch20UfNtLosDWkqbrmA7PZewWOf7nwmuBPDoXig0+wSI0s7FFHl+eYuDGEoi3NAAakjYggGFoBmg39KsIZcmIwvZWIP6wVyvkO5PKuN5upfDGgQQtcwlzdryQ+12h74aQXpEnIwotESuBNrZbfi3UNnyEyWE2KguHhhZNUGCKx87Sbb0fFOSbrSOXHr0QGtAQxog4EMQvsqzz8NE494ocjjkdhRSGlwxMLqbgBfap0tHeTqCdFkga7WED3YmOiGRXtjmYL/zBXKSl7ScRyVNLT8ETleVrjOJR7WUQEE=",
"respCode": "0000",
"respDate": "20201226230745",
"respMsg": "請求成功"
}
第一步:把請求引數快取到exchange中
public class ConstantFilter {
// 設定到 exchange.getAttributes()中的key
public final static String CACHED_REQUEST_BODY_OBJECT_KEY = "CACHED_REQUEST_BODY_OBJECT_KEY";
// 設定到 exchange.getAttributes()中的key
public final static String BG_DEBUG_KEY = "BG_DEBUG";
// 請求引數,回傳引數 加密
public final static String REQ_RES_ENCRYPT = "0";
// 請求參出,回傳結果 無需加密
public final static String REQ_RES_NALMORE = "1";
}
GlobalCacheRequestBodyFilter:把請求中的body復制到exchange,便于在之后使用,也可以理解為同一個執行緒中,多個filter之間共享的域
/**
* @calssName AppCacheRequestBodyFilter
* @Description 將 request body 中的內容 copy 一份,記錄到 exchange 的一個自定義屬性中
* @Author hl
* @DATE 2020/9/27 14:42
*/
public class GlobalCacheRequestBodyFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(GlobalCacheRequestBodyFilter.class);
private int order;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info("GlobalCacheRequestBodyFilter ...");
// 將 request body 中的內容 copy 一份,記錄到 exchange 的一個自定義屬性中
Object cachedRequestBodyObject = exchange.getAttributeOrDefault(ConstantFilter.CACHED_REQUEST_BODY_OBJECT_KEY, null);
// 如果已經快取過,略過
if (cachedRequestBodyObject != null) {
return chain.filter(exchange);
}
// 如果沒有快取過,獲取位元組陣列存入 exchange 的自定義屬性中
return DataBufferUtils.join(exchange.getRequest().getBody())
.map(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
return bytes;
}).defaultIfEmpty(new byte[0])
.doOnNext(bytes -> exchange.getAttributes().put(ConstantFilter.CACHED_REQUEST_BODY_OBJECT_KEY, bytes))
.then(chain.filter(exchange));
}
@Override
public int getOrder() {
return this.order;
}
public GlobalCacheRequestBodyFilter(int order){
this.order = order;
}
}
第二步:請求引數解密處理
- gateway中我們并不能直接對請求引數進行修改,那么如何才能將加密的請求一解密后的方式路由到下游的服務中去呢?這里我們需要在filter中根據原來的請求對引數解密后創建新的請求,需要注意的是:解密后對請求頭CONTENT_LENGTH需要重置,都在會出現讀取的引數不完整的情況,
- 如果需要對引數進行解密處理,所以對于客戶端的請求統一使用POST的請求方式,
- 加密解密的方法根據你的需要處理,(此處我使用的是RSA,AES結合的方式-之后會繼續分享此種加密方式)
public class AppReqDecryptFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(AppReqDecryptFilter.class);
@Autowired
private CheckService checkService;
private int order;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info("請求引數系解密...");
// 設定是否加密標識
List<String> bgDebugHeaders = exchange.getRequest().getHeaders().get("bg-debug");
String bgDebug = bgDebugHeaders != null ? bgDebugHeaders.get(0) : ConstantFilter.REQ_RES_ENCRYPT;
exchange.getAttributes().put(ConstantFilter.BG_DEBUG_KEY, bgDebug);
// 獲取請求的方法
ServerHttpRequest oldRequest = exchange.getRequest();
String method = oldRequest.getMethodValue();
URI uri = oldRequest.getURI();
if ("POST".equals(method)){
// 嘗試從 exchange 的自定義屬性中取出快取到的 body
Object cachedRequestBodyObject = exchange.getAttributeOrDefault(ConstantFilter.CACHED_REQUEST_BODY_OBJECT_KEY, null);
if (cachedRequestBodyObject != null) {
byte[] decrypBytes;
try {
byte[] body = (byte[]) cachedRequestBodyObject;
String rootData = new String(body); // 客戶端傳過來的資料
decrypBytes = body;
if(ConstantFilter.REQ_RES_ENCRYPT.equals(bgDebug)) {
//獲取密鑰
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(Params.AUTHORIZE_TOKEN);
//此處獲取自己配置的
SecretIv secretIv = checkService.getKey(exchange, ResType.REQ, token);
logger.info("獲取密鑰:{}", secretIv);
//報文處理解密
JSONObject data = JSONObject.parseObject(new String(decrypBytes));
Map<String, Object> map = AESUtils.decryptAESRequest(data.getString(Params.AUTHORIZE_DATA), secretIv.getReqSecret(), secretIv.getReqIv());
//驗簽
checkService.checkSign(map, secretIv.getReqSecret(), data.getString(Params.AUTHORIZE_SIGN));
//下放報文
decrypBytes = JSONHelper.map2json(map).getBytes();
}
}catch (RuntimeException e){
logger.info("客戶端資料決議例外:{}", e);
throw new RuntimeException(e.getMessage());
}catch (Exception e){
logger.info("客戶端資料決議例外:{}", e);
throw new RuntimeException("系統例外");
}
// 根據解密后的引數重新構建請求
DataBufferFactory dataBufferFactory = exchange.getResponse().bufferFactory();
Flux<DataBuffer> bodyFlux = Flux.just(dataBufferFactory.wrap(decrypBytes));
ServerHttpRequest newRequest = oldRequest.mutate().uri(uri).build();
newRequest = new ServerHttpRequestDecorator(newRequest) {
@Override
public Flux<DataBuffer> getBody() {
return bodyFlux;
}
};
// 構建新的請求頭
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
// 由于修改了傳遞引數,需要重新設定CONTENT_LENGTH,長度是位元組長度,不是字串長度
int length = decrypBytes.length;
headers.remove(HttpHeaders.CONTENT_LENGTH);
headers.setContentLength(length);
// headers.set(HttpHeaders.CONTENT_TYPE, "application/json");
newRequest = new ServerHttpRequestDecorator(newRequest) {
@Override
public HttpHeaders getHeaders() {
return headers;
}
};
// 把解密后的資料重置到exchange自定義屬性中,在之后的日志GlobalLogFilter從此處獲取請求引數列印日志
exchange.getAttributes().put(ConstantFilter.CACHED_REQUEST_BODY_OBJECT_KEY, decrypBytes);
return chain.filter(exchange.mutate().request(newRequest).build());
}
}else if("GET".equals(method)){ // todo 暫不處理
Map requestQueryParams = oldRequest.getQueryParams();
return chain.filter(exchange);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return this.order;
}
public AppReqDecryptFilter(int order){
this.order = order;
}
public AppReqDecryptFilter(){
}
第三部:回傳結果加密操作
public class AppRespEncryptFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(AppReqDecryptFilter.class);
@Autowired
private CheckService checkService;
private int order;
@Autowired
CommonKeys commonKeys;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info("回傳引數加密...");
String bgDebug = exchange.getAttributeOrDefault(ConstantFilter.BG_DEBUG_KEY, ConstantFilter.REQ_RES_ENCRYPT);
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.buffer().map(dataBuffer -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffer);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
//釋放掉記憶體
DataBufferUtils.release(join);
// 正常回傳的資料
String rootData = new String(content, Charset.forName("UTF-8"));
byte[] respData = rootData.getBytes();
if(ConstantFilter.REQ_RES_ENCRYPT.equals(bgDebug)){
try {
//登錄成功的下發密鑰
ResultData<Object> res = checkService.getNewSecretIv(exchange, commonKeys, rootData);
rootData = res.getData() + "";
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(Params.AUTHORIZE_TOKEN);
SecretIv secretIv = checkService.getKey(exchange, ResType.RSP, token);
//報文加密
respData = JSON.toJSONString(ResultData.result(CodeMsg.GATEWAY_SUCCESS,AESUtils.encryptAESRequest(rootData, secretIv.getRspSecret(), secretIv.getRspIv(), false))).getBytes();
} catch (Exception e) {
e.printStackTrace();
respData = JSON.toJSONString(ResultData.result(CodeMsg.GATEWAY_ERROR)).getBytes();
}
}
// 加密后的資料回傳給客戶端
byte[] uppedContent = new String(respData, Charset.forName("UTF-8")).getBytes();
return bufferFactory.wrap(uppedContent);
}));
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
@Override
public int getOrder() {
return this.order;
}
public AppRespEncryptFilter(int order){
this.order = order;
}
public AppRespEncryptFilter(){
}
第四步:在配置到指定的路由中去
如果所有的路由都需要,則自定義過濾器中可以直接實作全域過濾器,就無需配置到指定路由中去了,如:GlobalCacheRequestBodyFilter
@Configuration
public class GatewayConfig {
/**
* 全域過濾器:快取請求body
*/
@Bean
public GlobalCacheRequestBodyFilter globalCacheRequestBodyFilter(){
return new GlobalCacheRequestBodyFilter(-30);
}
@Bean
public AppRespEncryptFilter globalAppRespEncryptFilter(){
return new AppRespEncryptFilter(-22);
}
@Bean
public AppReqDecryptFilter globalAppReqDecryptFilter(){
return new AppReqDecryptFilter(-25);
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/241970.html
標籤:其他
上一篇:【資料庫視頻】安全機制
