Zuul是Netflix出品的一个基于JVM路由和服务端的负载均衡器.
Zuul功能:
认证压力测试金丝雀测试动态路由负载削减安全静态响应处理主动/主动交换管理
那么实现这些的核心是什么的呢?那这边就要介绍到Zuul的filter
在zuul中定义了四种不同生命周期的过滤器类型,具体如下:
pre:可以在请求被路由之前调用route:在路由请求时候被调用post:在route和error过滤器之后被调用error:处理请求时发生错误时被调用
过滤器组成结构
前面说了API**的作为下游接入上游的层,最基础的能力就是API代理,安全控制,流量控制等业务前置相关校验。
在上面的图中我们的业务要对我们的客户端提供支付相关的能力,如账户信息,支付请求,支付单查询的能力。我们要求API接口这些都要具备安全的信息交互能力,这边的重点也是这块。
我们这边的安全需求及实现:
协议基于https, 这边的对外SLB(阿里云负载均衡服务)安装证书消息体保障不可**对下游的调用者身份的识别
上面提到第一点好实现,通过阿里云的购买的证书,并通过向导安装在SLB即可,将后端服务映射到API**即可;
那么2,3我们这边就要自行**了。
我们这边的模型先简单介绍下:
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;@Componentpublic class AuthenZuulFilter extends ZuulFilter{ private final static Logger LOG = LoggerFactory.getLogger(AuthenZuulFilter.class); public final static Integer CONST_API_AUTH_TYPE_NO_NEED = 0; @Autowired private ApiAuthService apiAuthService; @Autowired private ApiService apiService; @Value("${zuul.prefix:'/api'}") private String apiPrefix; @Override public boolean shouldFilter() { HttpServletRequest request = ctx.getRequest(); String uri = request.getRequestURI(); //后续需要结合参数method做匹配校验 String apiPath = uri.substring(apiPrefix.length()).replace("//", "/"); boolean isCanApiFilter = excludeUriHandler(apiPath); return isCanApiFilter; } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); String queryStr = request.getQueryString(); String srcIp = HttpUtils.getIpAddress(request); String uri = request.getRequestURI(); //后续需要结合参数method做匹配校验 String flushCache = request.getParameter("flushCache"); //是否要清楚api信息的缓存 LOG.info("access_log: user_ip={},uri={},querystr={},headInfo={}",new Object[]{srcIp,uri,queryStr,HttpUtils.getHeadersInfo(request)}); String apiPath = uri.substring(apiPrefix.length()).replace("//", "/"); //清空缓存指令 if("true".equalsIgnoreCase(flushCache)){ apiService.cacheRemove(); } String appKey = request.getParameter("appKey"); String method = request.getParameter("method"); String timestamp = request.getParameter("timestamp"); String sign = request.getParameter("sign"); Map<String,String> urlParam = null; if(StringUtils.hasText(request.getQueryString())){ urlParam = HttpUtils.getUrlParams(queryStr); }else{ ResultDto<Object> ret = ResultDto.builder().retCode(1).retMsg("参数缺少").reqId(KeyGenerateUtils.getKey("api")).build(); ctx.setResponseStatusCode(HttpStatus.SC_BAD_REQUEST);// 返回错误码 ctx.setResponseBody(JacksonUtils.obj2Str(ret));// 返回错误内容 return null; } //校验不通过返回 if(!checkParam(urlParam,ctx)){ return null; } Map<String,String> param = new HashMap<String,String>(); //传递扩展参数 ApiCallExtParam extParam = new ApiCallExtParam(); param.put("appKey", appKey); param.put("method", method); param.put("timestamp", timestamp); extParam.setSrcIp(srcIp); extParam.setActionMethod(request.getMethod()); extParam.setHttpHeader(HttpUtils.getHeadersInfo(request)); extParam.setUrlParam(urlParam); param.putAll(urlParam); param.remove("sign"); //获取playload try { String playload = HttpUtils.decodeUrl(IOUtils.toString(request.getInputStream(), "UTF-8")); if(StringUtils.hasText(playload)){ param.put("playload", playload); } } catch (IOException e) { // TODO Auto-generated catch block LOG.warn("输入流读取异常",e); } //转发客户端的头信息 ResultDto<Object> chkRet = apiAuthService.restApiHandle(param, sign, extParam); if(!chkRet.getRetCode().equals(Integer.valueOf(0))){ ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(401);// 返回错误码 ctx.setResponseBody(JacksonUtils.obj2Str(chkRet));// 返回错误内容 }else{ ctx.setSendZuulResponse(true);// 对该请求进行路由 ctx.setResponseStatusCode(200); ctx.set("isSuccess", true);// 设值,让下一个Filter看到上一个Filter的状态 } return null; } private boolean excludeUriHandler(String apiPath) { List<Api> apiList = apiService.getAllNoAuthApi(); Optional<Api> flag = apiList.stream().filter(api -> api.getApiUri().equals(apiPath)).findAny(); return flag.isPresent(); } private void forwardHeader(RequestContext ctx, HttpServletRequest request) { Enumeration 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); } } private boolean checkParam(Map<String, String> urlParam, RequestContext ctx) { String appKey = urlParam.get("appKey"); String method = urlParam.get("method"); String timestamp = urlParam.get("timestamp"); String sign = urlParam.get("sign"); String version = urlParam.get("version"); if(!StringUtils.hasText(appKey)){ ctx.setResponseStatusCode(401); ctx.setResponseBody("{\"result\":\"appKey is not correct/missing!\"}"); return false; } if(!StringUtils.hasText(method)){ ctx.setResponseStatusCode(401); ctx.setResponseBody("{\"result\":\"method is not correct/missing!\"}"); return false; } if(!StringUtils.hasText(timestamp)){ ctx.setResponseStatusCode(401); ctx.setResponseBody("{\"result\":\"timestamp is not correct/missing!\"}"); return false; } if(!StringUtils.hasText(sign)){ ctx.setResponseStatusCode(401); ctx.setResponseBody("{\"result\":\"sign is not correct/missing!\"}"); return false; } if(!StringUtils.hasText(version)){ ctx.setResponseStatusCode(401); ctx.setResponseBody("{\"result\":\"version is not correct/missing!\"}"); return false; } return true; } @Override public String filterType() { // TODO Auto-generated method stub return "pre"; } @Override public int filterOrder() { // TODO Auto-generated method stub return 1; }}
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;@Servicepublic class ApiAuthService { private final Logger LOG = LoggerFactory.getLogger(ApiAuthService.class); @Autowired private AppInfoService appInfoService; @Autowired private App2ApiService app2ApiService; /** * api认证处理 * @param param 参数:url参数+消息体(playload) * @param playload 消息体 * @param sign 签名 * @param extParam 扩展参数 * @return */ public ResultDto<Object> restApiHandle(Map<String, String> param,String sign,ApiCallExtParam extParam) { ResultDto<Object> ret = new ResultDto<Object>(); ret.setRetCode(0); String reqIdSubFix = param.get("appKey")+param.get("method")+param.get("timestamp"); ret.setReqId(KeyGenerateUtils.getKey(reqIdSubFix)); //timestamp 先判断时间是否符合与服务端的误差范围内 long correctionTime = 1000*60*10; //十分钟 Date clientReqTime = DateUtils.getDateByStr(param.get("timestamp")); if(clientReqTime==null){ ret.setRetCode(ResultDto.RET_CODE_SYS_TIME_INCORRECT); ret.setRetMsg("timestamp 时间格式必须符合yyyy-MM-dd HH:mm:ss"); if(LOG.isDebugEnabled()){ LOG.debug("API AUTH timestamp check... ,timestamp:{}",new Object[]{clientReqTime}); } return ret; } if(Math.abs(System.currentTimeMillis()-clientReqTime.getTime())>correctionTime){ ret.setRetCode(ResultDto.RET_CODE_SYS_TIME_INCORRECT); ret.setRetMsg("timestamp 不在允许的时间范围内"); if(LOG.isDebugEnabled()){ LOG.debug("timestamp 不在允许的时间范围内,timestamp:{}",new Object[]{clientReqTime}); } return ret; } //判断APP Key是否存在 try{ String appKey = param.get("appKey"); String method = param.get("method"); AppInfo appInfo = appInfoService.getAppByKey(appKey); if(appInfo==null){ ret.setRetCode(ResultDto.RET_CODE_APP_NO_EXISTS); ret.setRetMsg("APP KEY不存在"); if(LOG.isDebugEnabled()){ LOG.debug("APP KEY不存在 ,appKey:{}",new Object[]{appKey}); } return ret; } //判断是否有对应的API调用权限 App2Api app2Api = app2ApiService.checkApp2Api(appKey, method); if(app2Api==null){ ret.setRetCode(ResultDto.RET_CODE_APP_METHOD_UN_AUTHORIZE); ret.setRetMsg("方法未授权"); if(LOG.isDebugEnabled()){ LOG.debug("方法未授权 ,appKey:{},method:{}",new Object[]{appKey,method}); } return ret; } //再判断API http method是否正确 if(!extParam.getActionMethod().equalsIgnoreCase(app2Api.getActionType())){ ret.setRetCode(ResultDto.RET_CODE_APP_METHOD_UN_AUTHORIZE); ret.setRetMsg("方法"+app2Api.getMethod()+"必须使用"+app2Api.getActionType()); return ret; } //冗余 extParam.setAppName(appInfo.getAppName()); extParam.setLogEnable(Integer.valueOf(1).equals(appInfo.getEnableLog())); String gensign = ApiUtil.signTopRequest(param,appInfo.getSecret()); if(LOG.isDebugEnabled()){ LOG.debug("gensign:"+gensign); } if(!sign.equals(gensign)){ ret.setRetCode(ResultDto.RET_CODE_APP_SIGN_INCORRECT); ret.setRetMsg("签名错误"); return ret; } param.put("srcIp", extParam.getSrcIp()); }catch(Exception e){ LOG.error("API-ServiceException ",e); ret.setRetCode(-1); if(e.getCause()!=null){ ret.setRetMsg(e.getCause().getMessage()); }else ret.setRetMsg("异常信息"+e.getMessage()); } return ret; }}
通过上面我们是不是很熟悉,这边的重点就是签名的逻辑,提取报文中需要签名的信息,然后排序的报文串在进行散列算法(MD5,RSA)生成固定的32位签名串,消费者生成签名生产者验签。
这次就讲了API安全认证实践案例,那么接下来我后续也会针对一些公共API**的基础能力的扩展再补充一些实用的案例。