導讀:在微服務的架構下,系統會根據業務拆分為多個服務,各自負責單一的職責,在這樣的架構下,我們需要確保各api的安全性,也就是說服務不是開放的,而是需要授權才可訪問的,避免介面被不合法的請求所訪問,
但是在在微服務集群中服務之間暴力的介面,或者對于第三方開放的介面如果不做及安全和認證,后果可想而知,
閱讀下文之前思考幾個問題:
- 如何在restTemplate遠程呼叫請求增加添加統一認證?
- 服務認證如何規范加密和解密?
- 遠程呼叫統一什么協議比較合適?
如下圖,三個服務注冊到同一個注冊中心集群,服務A、B、C之間如果不做任何限制,服務之間的介面基本是互通的,

但是如果A、B、C之間要做服務認證該如何設計?如果外部定制集成服務D接入怎么保證服務的安全性?
怎么加認證?
假設服務A是組織架構服務,服務B是規則引擎服務,服務C是公式引擎服務,如果B、C請求A服務呼叫用戶資訊除了開放介面規定引數,我們如何加入權限認證資訊,
目前市面上主要兩種方案處理
- 請求體Body中加入引數校驗 => SDK集成場景較多
- 請求頭Header中加入認證資訊 token
請求頭Header中加入認證資訊 token,如下圖結構所示,

確定服務認證統一在request header 加X-SERVICE-NAME,在服務服務中我們不可能每個服務都會主動去管理基礎認證資訊,仔細閱讀RestTemplate原始碼不難發現,RestTemplate實作介面InterceptingHttpAccessor,因此我們可以再定義自己的攔截器來統一處理,

攔截@FeignClient 標識攔截
feign遠程呼叫請求增加頭部資訊處理,重新定義 RequestInterceptor
public class FeignApiInterceptor implements RequestInterceptor {
/**
* 統一處理feign的遠程呼叫攔截
*/
@Override
public void apply(RequestTemplate requestTemplate) {
// 遠程呼叫請求增加頭部資訊處理(簡寫代碼如下)
requestTemplate.header("X-SERVICE-NAME", "...");
}
}
攔截RestTemplate遠程呼叫請求
RestTemplate 提供高度封裝的介面,可以讓我們非常方便地進行 Rest API 呼叫,常見的方法如下:

RestTemplate遠程呼叫請求增加頭部資訊處理,統一處理處理restTemplate的請求攔截,
/**
* restTemplate遠程呼叫請求增加頭部資訊處理
*/
@Slf4j
public class RestApiHeaderInterceptor implements ClientHttpRequestInterceptor {
/**
* 處理restTemplate的請求攔截
*/
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
long startTime = System.currentTimeMillis();
try {
HttpHeaders headers = request.getHeaders();
// 遠程呼叫請求增加頭部資訊處理(簡寫代碼如下)
requestTemplate.header("X-SERVICE-NAME", "...");
ClientHttpResponse response = execution.execute(request, body);
// 加入鏈路深度Deep
// ....
return response;
} finally {
// do something
}
}
}
服務注冊客戶端配置
@Configuration
@EnableFeignClients(basePackages = NamingConstant.BASE_PACKAGE)
public class DiscoveryClientConfig {
//......
/**
* feign請求攔截器
*/
@Bean
public RequestInterceptor feignInterceptor() {
return new FeignApiInterceptor();
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(HttpClient httpClient,
Environment environment) {
RestTemplate restTemplate = new RestTemplate();
restTemplateBuilder.configure(restTemplate);
restTemplate.setRequestFactory(buildFactory(httpClient, environment));
// 加入自定義攔截器
restTemplate.getInterceptors().add(new RestApiHeaderInterceptor());
return restTemplate;
}
}
拓展補充:
在處理RestTemplate的請求攔截的時候我們也可以追加鏈路追蹤日志,具體對于鏈路日志的拓展可查閱《微服務分布式架構中,如何實作日志鏈路跟蹤?》
簡寫代碼
@Slf4j
public class RestApiHeaderInterceptor implements ClientHttpRequestInterceptor {
/**
* 處理restTemplate的請求攔截
*/
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
long startTime = System.currentTimeMillis();
try {
// 遠程呼叫請求增加頭部資訊處理
ClientHttpResponse response = execution.execute(request, body);
// 獲取傳遞執行緒變數
Collection<String> values = response.getHeaders()
.get(TraceUtil.TRACE_RPCCOUNT);
if (!CollectionUtils.isEmpty(values)) {
int value = Integer.valueOf(values.iterator().next());
// 鏈路深度追加
TraceUtil.getRpcCounter().addAndGet(value + 1);
}
return response;
} finally {
// 是否開啟鏈路追蹤
if (TraceUtil.isTraceLoggerOn()) {
// 輸出鏈路日志(介面耗時)
TraceUtil.log(StringHelper.join("dt:", System.currentTimeMillis() - startTime,
", TRACE-RPC-", request.getMethod(),
", URI:", request.getURI()));
}
}
}
}
怎么接收認證資訊?
讀過Spring-Web原始碼應該知道OncePerRequestFilter過濾器基類,旨在保證在任何 servlet 容器上每個請求分派一次執行, 它提供了一個帶有 HttpServletRequest 和 HttpServletResponse 引數的doFilterInternal方法,詳細用法可閱讀原始碼,
另一種也出現在它自己的執行緒中的調度型別是ERROR , 如果子類希望靜態宣告是否應該在錯誤調度期間呼叫一次,它們可以覆寫shouldNotFilterErrorDispatch() ,
getAlreadyFilteredAttributeName方法確定如何識別請求已被過濾, 默認實作基于具體過濾器實體的配置名稱,
定義基礎過濾器
public abstract class BaseWebFilter extends OncePerRequestFilter {
/**
* 回傳類名,避免filter不被執行
*/
@Override
protected String getFilterName() {
return null;
}
}
過濾器定義虛擬類, 繼承自介面的過濾器,如果宣告為springbean,他自動加載到請求過濾鏈(因為繼承了GenericFilterBean介面,spring默認把繼承此類的過濾器bean加到web過濾鏈)中,通過注解@order定義其優先級如果不申明為springbean,又要加入到過濾鏈中,可以通過FilterRegistrationBean定義,并指定優先級
封裝攔截 ApiValidationFilter
@Override
public BaseWebFilter getFilterInstance() {
return new BaseWebFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 攔截校驗
if (validateToken(request)) {
// 校驗通過執行下一個lan'j
chain.doFilter(request, response);
} else {
// 封裝統一認證失敗回傳資訊
response.setStatus(401);
response.setCharacterEncoding(
Charset.defaultCharset().displayName());
response.setContentType(
MimeTypeUtils.APPLICATION_JSON.toString());
response.getWriter().println(JsonUtil.toJsonString(Response
.err("status.401")));
}
}
};
}
針對與validateToken方法這里就不做展開,對于加密方式大同小異,根據自己專案情況來定,
private boolean validateToken(HttpServletRequest request) {
if (!needValidate(request)) {
return true;
}
String security = resolveToken(request);
if (security == null) {
log.info("驗證資訊缺失,請求地址:{}", request.getRequestURI());
return false;
}
try {
// 決議security加密規則
return true;
} catch (Exception e) {
log.info("驗證資訊無效:{},請求地址:{}", security, request.getRequestURI());
return false;
}
}
總結
回顧整個方案設計與實作,大致可分為以下幾個步驟
- 引數加入位置:請求頭Header還是請求體Body
- 確定加入范圍:@FeignClient客戶端、構建RestTemplate以及集成服務遠程介面呼叫
- 確定攔截位置:Web執行緒攔截器,用于統一處理執行緒變數,該過濾器執行順序早于springsecurity的過濾器
- 確定加密/解密方式:加密字串和解密
原文地址:Spring Cloud中如何保證各個微服務之間呼叫的安全性?
???????
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/295467.html
標籤:其他
