签名生成及验签实在开放平台中应用,暂时不清楚调用流程及原理
一,生成签名
签名规则
签名sign是通过应用在平台的私钥和请求参数根据一定算法生成的签名值,主要是防止参数在传输过程中被篡改, 同时对调用方的身份进行校验
签名生成步骤:
第1步:
将除签名sign外的所有请求参数按参数名首字母进行升序排序,其中参数包括系统参数和业务参数
第2步:
将第1步得到的排序结果,依次按照"keyvalue"的形式拼接成待加密的串
第3步
对第2步得到的新串进行HMAC加密并转化为大写的十六进制签名串
签名示例如下:
假设请求为:/anji-open/open-api/request?appId=testApp&method=testApp&sign=8D45C66B6E1E773614E5866541EAF78D×tamp=111&name=hello&password=123456&age=12
1、关键信息: 应用私钥:29bca37bf0174ea287a770cd0d4ff83c 请求参数:appId=testApp&method=testApp&sign=8D45C66B6E1E773614E5866541EAF78D×tamp=111&name=hello&password=123456&age=12 2、将除"sign"外所有的参数进行排序:age、appId、method、name、password、timestamp; 依次按照"keyvalue"形式拼接的结果:age12appIdtestAppmethodtestAppnamehellopassword123456timestamp111 3、将得到的新串进行HMAC加密并转化为大写的十六进制签名串:8D45C66B6E1E773614E5866541EAF78D
二,生成签名代码
参数+秘钥(生成随机数)
/** * 验证签名,防止数据被篡改 * @author lr * @date 2019-05-29 17:02 */ public class SignUtils { private final static String SIGN_PARAM="sign"; /** * 生产签名 * @param params * @param secret * @return * @throws IOException */ public static String generateSign(Map<String, String> params, String secret) throws IOException { // 第一步:检查参数是否已经排序 String[] keys = params.keySet().toArray(new String[0]); Arrays.sort(keys); // 第二步:把所有参数名和参数值串在一起 StringBuilder query = new StringBuilder(); for (String key : keys) { //跳过参数"sign",sign不参与签名 if(StringUtils.equals(SIGN_PARAM,key)) { continue; } String value = params.get(key); if (StringUtils.isNotBlank(value)) { query.append(key).append(value); } } // 第三步:使用HMAC加密 byte[] bytes = encryptHMAC(query.toString(), secret); // 第四步:把二进制转化为大写的十六进制(正确签名应该为32大写字符串,此方法需要时使用) return byte2hex(bytes); } /** * 加密 * @param data * @param secret * @return * @throws IOException */ public static byte[] encryptHMAC(String data, String secret) throws IOException { byte[] bytes = null; try { SecretKey secretKey = new SecretKeySpec(secret.getBytes(ApiConstants.CHARSET_UTF8), "HmacMD5"); Mac mac = Mac.getInstance(secretKey.getAlgorithm()); mac.init(secretKey); bytes = mac.doFinal(data.getBytes(ApiConstants.CHARSET_UTF8)); } catch (GeneralSecurityException gse) { throw new IOException(gse.toString()); } return bytes; } /** * 转16进制 * @param bytes * @return */ public static String byte2hex(byte[] bytes) { StringBuilder sign = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(bytes[i] & 0xFF); if (hex.length() == 1) { sign.append("0"); } sign.append(hex.toUpperCase()); } return sign.toString(); } public static void main(String[] args) { Map<String,String> map = new HashMap<>(16); map.put("appId","testApp"); map.put("method","testPost"); map.put("timestamp","111"); try { String s = generateSign(map, "e113e43bb76a437d94848a2bbbb45b0b"); System.out.println(s); } catch (IOException e) { e.printStackTrace(); } } }
3,验签
在网关中进行验签
流程:从请求参数中获取参数+应用中秘钥,再次生成签名,比较入参签名和新生成签名是否一致,是验签通过
###疑问
1,在项目中参数生成签名,与接口接收的参数生成签名中间,参数何时会改变?接口调用流程是怎么样的?
2,如何进行身份验证的?
/** * 校验appId及秘钥 * @author lr * @date 2019-07-26 09:54 */ public class OpenAppInfoGatewayFilterFactory extends AbstractGatewayFilterFactory<OpenAppInfoGatewayFilterFactory.Config> implements ParentGatewayFilterFactory{ private Logger logger = LoggerFactory.getLogger(OpenAppInfoGatewayFilterFactory.class); public final static int OPEN_APP_INFO_FILTER_ORDER = 2; @Autowired private CacheHelper cacheHelper; public OpenAppInfoGatewayFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return new OpenAppInfoGatewayFilter(); } /** * 内部内的作用,排序,AbstractGatewayFilterFactory实现ordered没效果 * 原因:获取过滤器调用apply()返回的GatewayFilter,没有实现Ordered接口,无法排序 * 参考org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#loadGatewayFilters(java.lang.String, java.util.List):186行 */ private class OpenAppInfoGatewayFilter implements GatewayFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); MultiValueMap<String, String> params = request.getQueryParams(); //1、校验应用appId String appId = params.getFirst(SystemParamEnum.APPID.getValue()); //获取缓存中的应用对象 String appSecret = cacheHelper.getOpenAppSecret(appId); if(appSecret == null) { return getFilterResult(exchange,getResponseBean(ErrorCode.OPEN_NO_APP,appId)); } //2、校验method是否合法 String method = params.getFirst(SystemParamEnum.METHOD.getValue()); //获取缓存中的接口对象 ApiDocument apiDocument = cacheHelper.getApiDocument(method); if(apiDocument == null) { return getFilterResult(exchange,getResponseBean(ErrorCode.OPEN_NO_METHOD,method)); } //校验接口对应的项目编码 String projectCode = apiDocument.getProjectCode(); if(StringUtils.isBlank(projectCode)) { return getFilterResult(exchange,getResponseBean(ErrorCode.METHOD_NO_PROJECT_CODE,method)); } //3、校验接口对应的项目 String apiProjectUrl = cacheHelper.getApiProjectUrl(projectCode); if(apiProjectUrl == null) { return getFilterResult(exchange,getResponseBean(ErrorCode.OPEN_NO_PROJECT,projectCode)); } ***************************验签************************************************************************ try { String openSecret = SignUtils.generateSign(params.toSingleValueMap(), appSecret); //判断秘钥 if(!StringUtils.equals(openSecret,params.getFirst(SystemParamEnum.SIGN.getValue()))) { logger.error("秘钥验证错误,{}",params); return getFilterResult(exchange,getResponseBean(ErrorCode.APP_ERROR_SIGN,appId,openSecret)); } } catch (IOException e) { return getFilterResult(exchange,getResponseBean(ErrorCode.OPEN_APP_ERROR_SIGN,appId)); } //校验参数包括请求头、请求参数必输项 ServerHttpRequest.Builder mutate = request.mutate(); //4、校验请求头 String requestHeaders = apiDocument.getRequestHeaders(); List<JSONObject> requestHeaderList = new ArrayList<>(); if(StringUtils.isNotBlank(requestHeaders)) { requestHeaderList.addAll(OpenUtils.getListJson(requestHeaders)); //构造新请求 for(JSONObject header : requestHeaderList) { if(StringUtils.equals("1",header.getString("fixed"))) { mutate.header(header.getString("name"),new String[]{header.getString("value")}); }else{ //请求头缺少 if(StringUtils.isBlank(request.getHeaders().getFirst(header.getString("name")))) { return getFilterResult(exchange, getResponseBean(ErrorCode.API_NO_HEADER,method,header.getString("name"))); } } } } //5、校验请求参数 List<JSONObject> requestParamsJson = OpenUtils.getListJson(apiDocument.getQueryParams()); //验证必填参数 for(JSONObject param : requestParamsJson) { if(param.getBoolean("require")) { if(StringUtils.isBlank(params.getFirst(param.getString("name")))) { return getFilterResult(exchange, getResponseBean(ErrorCode.METHOD_NO_REQUIRE_PARAM,method,param.getString("name"))); } } } //新请求 ServerHttpRequest newRequest = mutate.build(); return chain.filter(exchange.mutate().request(newRequest).build()); } @Override public int getOrder() { return OPEN_APP_INFO_FILTER_ORDER; } } public static class Config{ } }