背景
在日常开发中,有时候需要开放接口给第三方合作伙伴使用,就像微信、支付宝的开发者平台一样,开放指定功能的接口给到具备开发能力的人员使用;为了保证对应的接口安全性,我们在网关自然是要做拦截校验的,下面我们就来看看在Spring Cloud Zuul中如何实现。
解决方案
1.平台方给到用户生成的appKey和appSecurity,该appKey绑定开放的接口;
2.调用方在请求中携带appKey以及通过相关签名算法计算出来的结果sign;
3.请求经过网关时,需要平台方解开请求校验对应的appKey以及签名结果sign;
4.如果校验通过,那么请求下发给目标服务;否则告知调用方没有调用权限;
相关对接文档如下:
- 准备工作
先在开放平台创建应用程序,并勾选相关功能,最后由开放平台生成appKey和appSecret给到调用方;
- 请求发送
【请求头】 | 必选 | 类型 | 说明 |
app-key | 是 | String | 由开放平台分配的appKey |
app-sign | 是 | String | 请求参数加密后的结果 |
- 计算app-sign
【请求参数加密前需要先进行排序,针对排序后拼接出来的字符串做HmacSHA256加密】
【排序示例】 :
appKey=具体参数值 //排序前请求参数 name=jack age=11 gender=男 //排序后等待加密字符串: age=11&gender=男&name=jack&appKey=具体参数值 复制代码
提示:如果Java代码,可使用Collections.sort针对List排序;如果是Map容器,可以使用TreeMap排序。
【加密方式】
private static final String MACSHA256 = "HmacSHA256"; public static String macSha2Base64(String message, String secret) { byte[] digestBytes = signBySha256(message, secret); try { return bytes2Base64(digestBytes, UTF8); } catch (Exception e) { return null; } } public static byte[] signBySha256(String message, String secret) { Mac hmacSha256; try { hmacSha256 = Mac.getInstance(MACSHA256); byte[] keyBytes = secret.getBytes(UTF8); byte[] messageBytes = message.getBytes(UTF8); hmacSha256.init(new SecretKeySpec(keyBytes, 0, keyBytes.length, MACSHA256)); // 使用HmacSHA256对二进制数据消息Bytes计算摘要 return hmacSha256.doFinal(messageBytes); } catch (Exception e) { return null; } } private static String bytes2Base64(byte[] bytes, String charset) throws UnsupportedEncodingException { return new String(Base64.getEncoder().encode(bytes), charset); } 复制代码
示例:
//待加密字符串 age=11&gender=男&name=zouwei&appKey=123456 //测试appSecurity appSecurity=plokmijnuhb //加密结果 lHw8EijUbCXnSzAOplMQE2Kwfu8ckTXHy5gITtOtlhw= 复制代码
以上便是调用方的接口对接方案。
开放平台如何实现接口安全性校验
- 创建数据表
-- ---------------------------- -- Table structure for app_security_tbl -- ---------------------------- DROP TABLE IF EXISTS `app_security_tbl`; CREATE TABLE `app_security_tbl` ( `id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, `user_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户ID', `app_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'app key', `app_secret` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '加密secret', `create_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `modified_date` timestamp NULL DEFAULT NULL COMMENT '修改时间', `merchant_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '商户号', `remark` varchar(256) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; -- ---------------------------- -- Table structure for app_api_permission_tbl -- ---------------------------- DROP TABLE IF EXISTS `app_api_permission_tbl`; CREATE TABLE `app_api_permission_tbl` ( `id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, `app_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'app key', `path` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '接口路径', `method` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '接口请求方法', `description` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '接口描述', `create_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `modified_date` timestamp NULL DEFAULT NULL COMMENT '修改时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; 复制代码
1.
app_security_tbl
表用来管理用户与app_key
的绑定关系;2.
app_api_permission_tbl
表用来管理app_key
与接口api的绑定关系;
- 网关实现
import com.google.common.collect.Maps; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import com.zx.silverfox.api.auth.dto.AppSecurityResponse; import com.zx.silverfox.api.auth.dto.HasPermissionRequest; import com.zx.silverfox.common.config.api.ApiSecurityConst; import com.zx.silverfox.common.exception.GlobalException; import com.zx.silverfox.common.util.JsonUtil; import com.zx.silverfox.common.vo.CommonResponse; import com.zx.silverfox.gateway.filter.strategy.MethodSecurityStrategy; import com.zx.silverfox.gateway.rpc.IAuthServiceAPI; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.util.List; import java.util.Map; import java.util.Objects; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE; /** * @author zouwei * @className ApiSecurityFilter * @date: 2020/9/26 上午11:40 * @description: */ @Component @Slf4j public class ApiSecurityFilter extends ZuulFilter { @Autowired(required = false) private List<MethodSecurityStrategy> strategies; @Autowired private IAuthServiceAPI authServiceAPI; @Override public String filterType() { return PRE_TYPE; } @Override public int filterOrder() { return -9; } @Override public boolean shouldFilter() { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); // 判断请求头中是否有指定的appKey,如果有指定的appKey就要准备拦截 return !StringUtils.equalsIgnoreCase(request.getRequestURI(), "/error") && StringUtils.isNotBlank(request.getHeader(ApiSecurityConst.API_KEY)); } @Override public Object run() throws ZuulException { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); // 验证接口访问权限 AppSecurityResponse securityResponse = verifyPermission(request); // 返回值为空,或者鉴权失败,直接结束此次请求 if (Objects.isNull(securityResponse) || !appSecurityAuthentication(request, securityResponse)) { noPermissionResponse(context); return null; } // 鉴权成功,添加头部信息 addAppSecurityToHeader(context, context.getRequest(), securityResponse); return null; } /** * 没有权限 * * @param context */ private void noPermissionResponse(RequestContext context) { Map<String, String> map = Maps.newHashMap(); map.put("data", "暂无权限!"); context.setSendZuulResponse(Boolean.FALSE); context.setResponseStatusCode(401); context.getResponse().setCharacterEncoding("UTF-8"); context.getResponse().setContentType("application/json;charset=UTF-8"); context.setResponseBody(JsonUtil.obj2String(map)); } /** * 验证appKey * * @param request * @return */ private AppSecurityResponse verifyPermission(HttpServletRequest request) { HasPermissionRequest permissionRequest = new HasPermissionRequest(); String apiKey = request.getHeader(ApiSecurityConst.API_KEY); permissionRequest.setAppKey(apiKey); permissionRequest.setPath(request.getRequestURI()); permissionRequest.setMethod(request.getMethod()); try { CommonResponse<AppSecurityResponse> response = authServiceAPI.hasPermission(permissionRequest); if (response.isSuccess()) { return response.getData(); } } catch (GlobalException e) { return null; } return null; } /** * 添加请求头 * * @param context * @param request * @param securityResponse */ private void addAppSecurityToHeader( RequestContext context, HttpServletRequest request, AppSecurityResponse securityResponse) { CustomHeaderServletRequest customHeaderServletRequest = new CustomHeaderServletRequest(request); customHeaderServletRequest.setHeaders( ApiSecurityConst.API_USER_ID, securityResponse.getUserId()); customHeaderServletRequest.setHeaders( ApiSecurityConst.API_SECURITY_KEY, securityResponse.getSecurityKey()); customHeaderServletRequest.setHeaders( ApiSecurityConst.API_MERCHANT_CODE, securityResponse.getMerchantCode()); context.setRequest(customHeaderServletRequest); } /** * 鉴权 * * @return */ private boolean appSecurityAuthentication( HttpServletRequest request, AppSecurityResponse securityResponse) { String method = request.getMethod(); for (MethodSecurityStrategy strategy : strategies) { if (strategy.isTest(method)) { return strategy.test(request, securityResponse.getSecurityKey()); } } return false; } } 复制代码
1.网关需要判断请求头中是否存在指定的
appKey
字段,如果包含,那么就符合拦截条件,否则直接跳过;2.拦截到请求后,从请求头中获取
appKey
,并从数据库中查询出对应的appSecret
;如果数据库中找不到appKey
,那么说明没有调用权限;3.解析出请求参数,通过对请求参数进行排序后再与查询出来的
appSecret
进行加密得到sign结果;4.对比请求头中获取的sign与加密得到的sign结果,如果结果一致,那么说明允许接口被调用,否则加密结果不一致没有访问权限;
5.所有校验通过后,我们需要将当前请求重新组装继续向后传递;