加密验签流程
- 配置partnerId、key、url。
private static String partnerId = "test";
private static String key = "06f7aab08aa2431e6dae6a156fc9e0b4";
private static String url = "http://openapi.test.com:8810/gateway.html";
- 组装接口请求参数。
Map map = new HashMap();
map.put(YijipayConstants.VERSION, null);
map.put(YijipayConstants.REQUEST_NO, Ids.oid());
map.put(YijipayConstants.PARTNER_ID, partnerId);
map.put(YijipayConstants.CHARSET, "MD5");
map.put("service", "posTradeCallBackQuery");
map.put("userNo", "17080915060804000049");
- 进行doPost网络请求:
- 3.1 检查orderNo、requestNo是否为空,空值抛出异常。
- 3.2 检查version、signType、protocol,为空进行默认设置。
- 3.3 检查partnerId、service是否为空,空值抛出异常。
- 3.4 遍历参数表,参数为空时从参数表中移除。
- 3.5 使用参数(params)和秘钥(securityKey)进行签名生成字符串,并把该字符串组装进params-sign。
- 3.6 进行网络请求,得到响应responseStr。
- 3.7 响应体转JSON对象responseObj。
- 3.8 判断响应体responseObj是否含有sign,否则直接返回responseObj。
- 3.9 验签:没有通过抛出异常,通过直接返回responseObj。原理:根据参数和密钥按照参数中的加密方法进行加密生成字符串是否和参数中的sign字段是否一致。
protected void supplementDefaultAndConfig(Map request) {
if ((StringUtils.isBlank((CharSequence) request.get("orderNo")))
&& (StringUtils.isBlank((CharSequence) request.get("requestNo")))) {
throw new OpenApiClientException("请求号流水号不能为空");
}
if (StringUtils.isBlank((CharSequence) request.get("version"))) {
request.put("version", "1.0");
}
if (StringUtils.isBlank((CharSequence) request.get("signType"))) {
request.put("signType", SignTypeEnum.MD5.code());
}
if (StringUtils.isBlank((CharSequence) request.get("protocol"))) {
request.put("protocol", "HTTP_FORM_JSON");
}
if (Strings.isBlank((CharSequence) request.get("partnerId"))) {
throw new OpenApiClientException("商户ID(partnerId)不能为空");
}
if (Strings.isBlank("service")) {
throw new OpenApiClientException("服务码(service)不能为空");
}
Iterator it = request.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
if (StringUtils.isBlank((CharSequence) entry.getValue()))
it.remove();
}
}
public String doPost(String url, Map params, String securityKey, int connectTimeout,
int readTimeout) {
try {
supplementDefaultAndConfig(params);
String signStr = sign(params, securityKey);
params.put("sign", signStr);
String responseStr = WebUtils.doPost(url, params, connectTimeout * 1000, readTimeout * 1000);
JSONObject responseObj = JSON.parseObject(responseStr);
if (!(responseObj.containsKey("sign"))) {
logger.warn("服务执行失败,不验签响应报文!,响应报文:" + responseObj);
return responseStr;
}
boolean isPass = verificationSign(responseStr, securityKey);
if (!(isPass)) {
throw new OpenApiClientException("服务响应验签失败,响应报文:" + responseStr);
}
logger.info("易极付响应报文:" + responseStr);
return responseStr;
} catch (IOException e) {
logger.error("服务请求错误:" + e.getMessage());
throw new OpenApiClientException("服务请求错误:" + e.getMessage());
}
}
public static String doPost(String url, Map params, int connectTimeout, int readTimeout)
throws IOException {
return doPost(url, params, "UTF-8", connectTimeout, readTimeout);
}
public static String doPost(String url, Map params, String charset, int connectTimeout,
int readTimeout) throws IOException {
String ctype = new StringBuilder().append("application/x-www-form-urlencoded;charset=").append(charset)
.toString();
String query = buildQuery(params, charset);
byte[] content = new byte[0];
if (query != null) {
content = query.getBytes(charset);
}
return doPost(url, ctype, content, connectTimeout, readTimeout);
}
public static String doPost(String url, String ctype, byte[] content, int connectTimeout, int readTimeout)
throws IOException {
HttpURLConnection conn = null;
OutputStream out = null;
String rsp = null;
try {
try {
conn = getConnection(new URL(url), "POST", ctype);
conn.setConnectTimeout(connectTimeout);
conn.setReadTimeout(readTimeout);
} catch (IOException e) {
Map map = getParamsFromUrl(url);
logger.error(new StringBuilder().append("服务[").append((String) map.get("service")).append("]请求失败:")
.append(e.getMessage()).toString());
throw e;
}
try {
out = conn.getOutputStream();
out.write(content);
rsp = getResponseAsString(conn);
} catch (IOException e) {
Map map = getParamsFromUrl(url);
logger.error(new StringBuilder().append("服务[").append((String) map.get("service")).append("]请求失败:")
.append(e.getMessage()).toString());
throw e;
}
} finally {
if (out != null) {
out.close();
}
if (conn != null) {
conn.disconnect();
}
}
return rsp;
}
public boolean verificationSign(String responseStr, String securitykKey) {
Map dataMap;
try {
dataMap = (Map) JSON.parseObject(responseStr, Map.class, new Feature[]{Feature.OrderedField});
} catch (Exception e) {
logger.info("认证(签名)错误");
return false;
}
return executerDigest(dataMap, securitykKey);
}
protected boolean executerDigest(Map dataMap, String securityCheckKey) {
boolean isPass = false;
try {
String signType = (String) dataMap.get("signType");
String signature = (String) dataMap.get("sign");
dataMap.remove("sign");
String getSignStr = DigestUtil.digest(dataMap, securityCheckKey,
DigestUtil.DigestALGEnum.getByName(signType));
if (getSignStr.equals(signature)) {
isPass = true;
logger.info("服务[" + dataMap.get("service") + "]响应报文验签成功");
}
} catch (Exception e) {
logger.info("服务[" + dataMap.get("service") + "]响应报文验签失败" + e.getMessage());
}
return isPass;
}
public static String sign(Map formData, String secretyKey) {
if (formData == null) {
throw new IllegalArgumentException("数据不能为空");
}
if (formData.isEmpty()) {
return null;
}
if (secretyKey == null) {
throw new IllegalArgumentException("安全校验码数据不能为空");
}
Map sortedMap = new TreeMap(formData);
if (sortedMap.containsKey("sign")) {
sortedMap.remove("sign");
}
StringBuffer stringToSign = new StringBuffer();
if (sortedMap.size() > 0) {
for (Map.Entry entry : sortedMap.entrySet()) {
if ((StringUtils.isNotBlank((CharSequence) entry.getValue())) &&(entry.getValue() != null)) {
stringToSign.append((String)entry.getKey()).append("=").apped((String) entry.getValue())
.append("&");
}
}
stringToSign.deleteCharAt(stringToSign.length() - 1);
}
logger.info("代签字符串[" + stringToSign.toString() + "]");
stringToSign.append(secretyKey);
String signature = DigestUtils.md5Hex(stringToSign.toString());
logger.info("签名结果[" + signature + "]");
return signature;
}
交互流程
- 商户首先要对需要加密的参数进行加密,然后对请求报文做签名,最后请求支付公司网的网关。
- 商户收到网关响应和通知后,首先对参数进行验签,确保参数没有被篡改。验证通过后,对加密的参数进行解密。
一些注意
- 为保障异步通知能正确的通知到商户系统,请保证服务器异步通知URL链接(notifyUrl)上无任何特别字符,如空格、HTML标签、商户系统自带抛出的异常提示信息等,也不能后接queryString。
- 如果商户需要在notifyUrl加上queryString,从而在得到企账通响应时得到该参数值,这个时候可以使用公共请求报文的context字段,把对应的queryString赋值给context。具体格式请参照公共请求报文中的context。
- 同步通知采用http/redirect方式跳转returnUrl通知,采用http-get方式,异步通知采用http-post方式后台请求通知商户端。
安全
摘要方案:摘要签名(MD5)/对称加密(AES)
证书方案:证书签名(RSA)/非对称加密(RSA)
-
摘要安全模式
- 报文的签名、验签 ——> 使用MD5算法。
- 数据加解密 ——> AES(对称)。
- 密钥 ——> 用于报文的摘要签名、验签和对称加解密的密钥。
-
证书安全模式
- 报文的签名,验签和加解密都使用RSA算法。
- 商户会收到一对秘钥,包含商户证书(.pfx)和网关证书(server.cer)的两个文件。
- 商户证书(.pfx):==包含用户的私钥和公钥证书,主要用于请求报文签名和收到报文的数据解密。==
- 网关证书(server.cer):==网关的公钥证书,主要用于对收到的报文(响应,通知)验签和发送数据的加密。==
-
报文签名、验签流程
- 参数拼接:参数的键值通过key=value形式用&链接,生成待签名字符串。==(待签字符串中的特殊字符无需编码,根据HTTP协议,传递参数的值中如果存在特殊字符(如:&、@等)和中文字符,那么该值需要做UTF-8编码的URL-Encoding,这样请求接收方才能接收到正确的参数值。)==
- 复杂参数拼接:如果是复杂的报文签名,API服务某个数据项是复杂对象(Object)类型或数组类型(Array),则需要把该项的值转换为标准JSON字符串后作为签名的参数值。在线工具
- 签名明文:为代签字符串后接商户安全码,请注意待签字符串与secretKey间没有任何连接字符。eg:key1=val1&key2=val2 c9cef22553afujh64b04a012f9cb8ea9
- 签名计算。首先转换签名明文(P)为UTF-8的字节数组(byte[]),通过标准摘要算法计算出摘要值的字节数组形式(byte[]),然后转换摘要的字节数组为Hex(小写字母的16进制形式)的字符串格式(S)则为摘要签名。 伪代码公式为: S=Hex(MD5(P.getBytes(“UTF-8”)))
- 验证签名-摘要安全模式:验证签名是在商户端收到网站的同步响应报文或通知报文后,对这些报文进行签名验证。原则为,使用“4. 签名计算”的方法对报文计算的签名值与响应或通知报文中的sign参数值(服务器端计算的签名)进行字符串比较,如果相同则表示验证签名通过,报文在传输过程中未被篡改。
- 验证签名-证书安全模式:
- [x] 签名和验签的明文为代签字符串,里请注意与摘要方式的区别是无需后接任何秘钥。
- [x] 签名计算:使用标准RSA签名算法,使用商户证书的 商户私钥对请求报文进行签名计算,计算出的结果为256字节(byte数组)的签名数据,请对签名数据进行标准base64编码,最后得到344字节的字符串则为报文的签名。
- [x] 验证签名:使用RSA标准算法,使用网关证书公钥进行验签。
参考:API商户开发规范