Spring cloud zuul在生產的應用(一)

Zuul是Netflix出品的一個基於JVM路由和服務端的負載均衡器.

Zuul功能:

  • 認證
  • 壓力測試
  • 金絲雀測試
  • 動態路由
  • 負載削減
  • 安全
  • 靜態響應處理
  • 主動/主動交換管理

那麼實現這些的核心是什麼的呢?那這邊就要介紹到Zuul的filter

在zuul中定義了四種不同生命週期的過濾器類型,具體如下:

  • pre:可以在請求被路由之前調用
  • route:在路由請求時候被調用
  • post:在route和error過濾器之後被調用
  • error:處理請求時發生錯誤時被調用
Spring cloud zuul在生產的應用(一)

過濾器組成結構

前面說了API網關的作為下游接入上游的層,最基礎的能力就是API代理,安全控制,流量控制等業務前置相關校驗。

Spring cloud zuul在生產的應用(一)

在上面的圖中我們的業務要對我們的客戶端提供支付相關的能力,如賬戶信息,支付請求,支付單查詢的能力。我們要求API接口這些都要具備安全的信息交互能力,這邊的重點也是這塊。

我們這邊的安全需求及實現:

  1. 協議基於https, 這邊的對外SLB(阿里雲負載均衡服務)安裝證書
  2. 消息體保障不可偽造
  3. 對下游的調用者身份的識別

上面提到第一點好實現,通過阿里雲的購買的證書,並通過嚮導安裝在SLB即可,將後端服務映射到API網關即可;

那麼2,3我們這邊就要自行開發了。

我們這邊的模型先簡單介紹下:

Spring cloud zuul在生產的應用(一)

API模型

AppInfo: 應用信息信息:包含應用名稱,應用appKey,secret, 對應的API列表

Api: API服務的定義包含服務名稱,協議,描述,文檔信息,授權方式等

那麼要實現這個我們的PRE 這個類型的過濾器就能應用上了

package com.hc.api.route;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import com.hc.api.dto.ApiCallExtParam;
import com.hc.api.model.Api;
import com.hc.api.service.ApiAuthService;
import com.hc.api.service.ApiService;
import com.hc.api.util.KeyGenerateUtils;
import com.hc.common.ResultDto;
import com.hc.common.http.HttpUtils;
import com.hc.common.json.JacksonUtils;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
@Component
public class AuthenZuulFilter extends ZuulFilter{
\tprivate final static Logger LOG = LoggerFactory.getLogger(AuthenZuulFilter.class);
\t
\tpublic final static Integer CONST_API_AUTH_TYPE_NO_NEED = 0;
\t@Autowired
\tprivate ApiAuthService apiAuthService;
\t

\t@Autowired
\tprivate ApiService apiService;
\t
\t@Value("${zuul.prefix:'/api'}")
\tprivate String apiPrefix;
\t
\t@Override
\tpublic boolean shouldFilter() {
\t\tHttpServletRequest request = ctx.getRequest();
\t\tString uri = request.getRequestURI(); //後續需要結合參數method做匹配校驗
\t\tString apiPath = uri.substring(apiPrefix.length()).replace("//", "/");
\t\tboolean isCanApiFilter = excludeUriHandler(apiPath);
\t\treturn isCanApiFilter;
\t}
\t@Override
\tpublic Object run() {
\t\tRequestContext ctx = RequestContext.getCurrentContext();
\t\tHttpServletRequest request = ctx.getRequest();
\t\tString queryStr = request.getQueryString();
\t\tString srcIp = HttpUtils.getIpAddress(request);
\t\tString uri = request.getRequestURI(); //後續需要結合參數method做匹配校驗
\t\tString flushCache = request.getParameter("flushCache"); //是否要清楚api信息的緩存
\t\tLOG.info("access_log: user_ip={},uri={},querystr={},headInfo={}",new Object[]{srcIp,uri,queryStr,HttpUtils.getHeadersInfo(request)});
\t\tString apiPath = uri.substring(apiPrefix.length()).replace("//", "/");
\t\t//清空緩存指令
\t\tif("true".equalsIgnoreCase(flushCache)){
\t\t\tapiService.cacheRemove();
\t\t}
\t\tString appKey = request.getParameter("appKey");
\t\tString method = request.getParameter("method");
\t\tString timestamp = request.getParameter("timestamp");
\t\tString sign = request.getParameter("sign");
\t\t
\t\tMap<string> urlParam = null;
\t\tif(StringUtils.hasText(request.getQueryString())){
\t\t\turlParam = HttpUtils.getUrlParams(queryStr);
\t\t}else{
\t\t\tResultDto<object> ret = ResultDto.builder().retCode(1).retMsg("參數缺少").reqId(KeyGenerateUtils.getKey("api")).build();
\t\t\tctx.setResponseStatusCode(HttpStatus.SC_BAD_REQUEST);// 返回錯誤碼
\t\t\tctx.setResponseBody(JacksonUtils.obj2Str(ret));// 返回錯誤內容
\t\t\treturn null;
\t\t}
\t\t//校驗不通過返回
\t\tif(!checkParam(urlParam,ctx)){

\t\t\treturn null;
\t\t}

Map<string> param = new HashMap<string>();
//傳遞擴展參數
ApiCallExtParam extParam = new ApiCallExtParam();

param.put("appKey", appKey);
param.put("method", method);
param.put("timestamp", timestamp);
\t\t
\t\textParam.setSrcIp(srcIp);
\t\textParam.setActionMethod(request.getMethod());
\t\textParam.setHttpHeader(HttpUtils.getHeadersInfo(request));
\t\t
\t\textParam.setUrlParam(urlParam);
\t\tparam.putAll(urlParam);
\t\tparam.remove("sign");
\t\t//獲取playload
\t\ttry {
\t\t\tString playload = HttpUtils.decodeUrl(IOUtils.toString(request.getInputStream(), "UTF-8"));
\t\t\tif(StringUtils.hasText(playload)){
\t\t\t\tparam.put("playload", playload);
\t\t\t}
\t\t} catch (IOException e) {
\t\t\t// TODO Auto-generated catch block
\t\t\tLOG.warn("輸入流讀取異常",e);
\t\t}
\t\t//轉發客戶端的頭信息
ResultDto<object> chkRet = apiAuthService.restApiHandle(param, sign, extParam);
if(!chkRet.getRetCode().equals(Integer.valueOf(0))){
\tctx.setSendZuulResponse(false);
\t\t\tctx.setResponseStatusCode(401);// 返回錯誤碼
\t\t\tctx.setResponseBody(JacksonUtils.obj2Str(chkRet));// 返回錯誤內容
}else{
\t\t\tctx.setSendZuulResponse(true);// 對該請求進行路由
\t\t\tctx.setResponseStatusCode(200);
\t\t\tctx.set("isSuccess", true);// 設值,讓下一個Filter看到上一個Filter的狀態
}
\t\treturn null;
\t}
\tprivate boolean excludeUriHandler(String apiPath) {
\t\tList apiList = apiService.getAllNoAuthApi();
\t\tOptional
flag = apiList.stream().filter(api -> api.getApiUri().equals(apiPath)).findAny();
\t\treturn flag.isPresent();
\t}
\tprivate void forwardHeader(RequestContext ctx, HttpServletRequest request) {
\t\tEnumeration headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = (String) headerNames.nextElement();
String value = request.getHeader(key);
// System.out.println("key:"+key+",value:"+value);
//默認zuul不會攜帶,需要自己在加入頭信息
ctx.addZuulRequestHeader(key, value);
}
\t}
\tprivate boolean checkParam(Map<string> urlParam, RequestContext ctx) {
\t\tString appKey = urlParam.get("appKey");
\t\tString method = urlParam.get("method");
\t\tString timestamp = urlParam.get("timestamp");
\t\tString sign = urlParam.get("sign");
\t\tString version = urlParam.get("version");
\t\tif(!StringUtils.hasText(appKey)){
\t\t\tctx.setResponseStatusCode(401);
\t\t\tctx.setResponseBody("{\\"result\\":\\"appKey is not correct/missing!\\"}");
\t\t\treturn false;
\t\t}
\t\tif(!StringUtils.hasText(method)){
\t\t\tctx.setResponseStatusCode(401);
\t\t\tctx.setResponseBody("{\\"result\\":\\"method is not correct/missing!\\"}");
\t\t\treturn false;
\t\t}
\t\tif(!StringUtils.hasText(timestamp)){
\t\t\tctx.setResponseStatusCode(401);
\t\t\tctx.setResponseBody("{\\"result\\":\\"timestamp is not correct/missing!\\"}");
\t\t\treturn false;
\t\t}
\t\tif(!StringUtils.hasText(sign)){
\t\t\tctx.setResponseStatusCode(401);
\t\t\tctx.setResponseBody("{\\"result\\":\\"sign is not correct/missing!\\"}");
\t\t\treturn false;
\t\t}
\t\tif(!StringUtils.hasText(version)){
\t\t\tctx.setResponseStatusCode(401);
\t\t\tctx.setResponseBody("{\\"result\\":\\"version is not correct/missing!\\"}");
\t\t\treturn false;
\t\t}
\t\treturn true;
\t}
\t@Override
\tpublic String filterType() {
\t\t// TODO Auto-generated method stub

\t\treturn "pre";
\t}
\t@Override
\tpublic int filterOrder() {
\t\t// TODO Auto-generated method stub
\t\treturn 1;
\t}
}
/<string>
/<object>/<string>/<string>/<object>/<string>

Api認證邏輯

package com.hc.api.service;
import java.util.Date;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.hc.api.dto.ApiCallExtParam;
import com.hc.api.model.App2Api;
import com.hc.api.model.AppInfo;
import com.hc.api.util.ApiUtil;
import com.hc.api.util.KeyGenerateUtils;
import com.hc.common.ResultDto;
import com.hc.common.date.DateUtils;
@Service
public class ApiAuthService {
\tprivate final Logger LOG = LoggerFactory.getLogger(ApiAuthService.class);
\t
\t@Autowired
\tprivate AppInfoService appInfoService;
\t
\t@Autowired
\tprivate App2ApiService app2ApiService;
\t
\t/**
\t * api認證處理
\t * @param param 參數:url參數+消息體(playload)
\t * @param playload 消息體
\t * @param sign 簽名
\t * @param extParam 擴展參數
\t * @return
\t */
\tpublic ResultDto<object> restApiHandle(Map<string> param,String sign,ApiCallExtParam extParam) {
\t\tResultDto<object> ret = new ResultDto<object>();

\t\tret.setRetCode(0);
\t\tString reqIdSubFix = param.get("appKey")+param.get("method")+param.get("timestamp");
\t\tret.setReqId(KeyGenerateUtils.getKey(reqIdSubFix));
\t\t//timestamp 先判斷時間是否符合與服務端的誤差範圍內
\t\tlong correctionTime = 1000*60*10; //十分鐘
\t\tDate clientReqTime = DateUtils.getDateByStr(param.get("timestamp"));
\t\tif(clientReqTime==null){
\t\t\tret.setRetCode(ResultDto.RET_CODE_SYS_TIME_INCORRECT);
\t\t\tret.setRetMsg("timestamp 時間格式必須符合yyyy-MM-dd HH:mm:ss");
\t\t\tif(LOG.isDebugEnabled()){
\t\t\t\tLOG.debug("API AUTH timestamp check... ,timestamp:{}",new Object[]{clientReqTime});
\t\t\t}
\t\t\treturn ret;
\t\t}
\t\tif(Math.abs(System.currentTimeMillis()-clientReqTime.getTime())>correctionTime){
\t\t\tret.setRetCode(ResultDto.RET_CODE_SYS_TIME_INCORRECT);
\t\t\tret.setRetMsg("timestamp 不在允許的時間範圍內");
\t\t\tif(LOG.isDebugEnabled()){
\t\t\t\tLOG.debug("timestamp 不在允許的時間範圍內,timestamp:{}",new Object[]{clientReqTime});
\t\t\t}
\t\t\treturn ret;
\t\t}
\t\t//判斷APP Key是否存在
\t\ttry{
\t\t\tString appKey = param.get("appKey");
\t\t\tString method = param.get("method");
\t\t\tAppInfo appInfo = appInfoService.getAppByKey(appKey);
\t\t\t
\t\t\tif(appInfo==null){
\t\t\t\tret.setRetCode(ResultDto.RET_CODE_APP_NO_EXISTS);
\t\t\t\tret.setRetMsg("APP KEY不存在");
\t\t\t\tif(LOG.isDebugEnabled()){
\t\t\t\t\tLOG.debug("APP KEY不存在 ,appKey:{}",new Object[]{appKey});
\t\t\t\t}
\t\t\t\treturn ret;
\t\t\t}
\t\t\t//判斷是否有對應的API調用權限
\t\t\tApp2Api app2Api = app2ApiService.checkApp2Api(appKey, method);
\t\t\tif(app2Api==null){
\t\t\t\tret.setRetCode(ResultDto.RET_CODE_APP_METHOD_UN_AUTHORIZE);
\t\t\t\tret.setRetMsg("方法未授權");
\t\t\t\tif(LOG.isDebugEnabled()){

\t\t\t\t\tLOG.debug("方法未授權 ,appKey:{},method:{}",new Object[]{appKey,method});
\t\t\t\t}
\t\t\t\treturn ret;\t\t\t\t\t\t
\t\t\t}
\t\t\t//再判斷API http method是否正確
\t\t\tif(!extParam.getActionMethod().equalsIgnoreCase(app2Api.getActionType())){
\t\t\t\tret.setRetCode(ResultDto.RET_CODE_APP_METHOD_UN_AUTHORIZE);
\t\t\t\tret.setRetMsg("方法"+app2Api.getMethod()+"必須使用"+app2Api.getActionType());
\t\t\t\treturn ret;\t\t\t\t\t
\t\t\t}
\t\t\t//冗餘
\t\t\textParam.setAppName(appInfo.getAppName());
\t\t\textParam.setLogEnable(Integer.valueOf(1).equals(appInfo.getEnableLog()));
\t\t\tString gensign = ApiUtil.signTopRequest(param,appInfo.getSecret());
\t\t\tif(LOG.isDebugEnabled()){
\t\t\t\tLOG.debug("gensign:"+gensign);
\t\t\t}
\t\t\tif(!sign.equals(gensign)){
\t\t\t\tret.setRetCode(ResultDto.RET_CODE_APP_SIGN_INCORRECT);
\t\t\t\tret.setRetMsg("簽名錯誤");
\t\t\t\treturn ret;\t\t
\t\t\t}
\t\t\tparam.put("srcIp", extParam.getSrcIp());
\t\t\t
\t\t}catch(Exception e){
\t\t\tLOG.error("API-ServiceException ",e);
\t\t\tret.setRetCode(-1);
\t\t\tif(e.getCause()!=null){
\t\t\t\tret.setRetMsg(e.getCause().getMessage());
\t\t\t}else
\t\t\t\tret.setRetMsg("異常信息"+e.getMessage());
\t\t}
\t\t
\t\treturn ret;
\t}
}
/<object>/<object>/<string>/<object>

通過上面我們是不是很熟悉,這邊的重點就是簽名的邏輯,提取報文中需要簽名的信息,然後排序的報文串在進行散列算法(MD5,RSA)生成固定的32位簽名串,消費者生成簽名生產者驗籤。

這次就講了API安全認證實踐案例,那麼接下來我後續也會針對一些公共API網關的基礎能力的擴展再補充一些實用的案例。

Spring cloud zuul在生產的應用(一)


分享到:


相關文章: