在本空间有一文章,描述了如何通过Postman工具自动添加请求报文的签名。其请求报文格式及签名位置参考《报文格式》
现同样,如果我们调用某平台,对方平台也要求我们调用时必须有签名,以防止报文中途被篡改。下面给出普通性做法。本例以SpringBoot为基础。
下面是拦截器的实现。
package com.ule.xxx.thrsp.comm;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import com.xxx.xxx.config.ThrPlatInfoConfig;
import com.xxx.xxx.config.ThrPlatInfoConfig.ThrPlatConfigProp;
import com.xxx.xxx.openapi.constant.CommResultCode;
import com.xxx.xxx.openapi.exception.BussinessException;
import com.xxx.xxx.utils.MDUtils;
/**
* 向三方平台请求时,在拦截器中添加签名属性。</br>
*/
@Component(value = "toThrPlatSignInterceptor")
public class ToThrPlatSignInterceptor implements ClientHttpRequestInterceptor {
/**
* 日志记录器
*/
private final Logger logger = LoggerFactory.getLogger(getClass());
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
String bodyStr = new String(body, StandardCharsets.UTF_8);
// 生成签名并添加到paramters中
try {
request = addSignInURI(request, bodyStr);
}
catch (URISyntaxException e) {
// Ingore
}
return execution.execute(request, body);
}
private HttpRequest addSignInURI(HttpRequest request, String bodyStr) throws IOException, URISyntaxException {
URI uri = request.getURI();
TreeMap<String, String> urlParamMap = handleUri(uri);
String sign = getSign(urlParamMap, bodyStr);
ClientHttpRequest clientRequest = (ClientHttpRequest) request;
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("sign", sign));
String url = URLEncodedUtils.format(params, "UTF-8");
URI src = new URI(request.getURI().toString().concat("&").concat(url));
clientRequest = new HttpComponentsClientHttpRequestFactory().createRequest(src, request.getMethod());
return clientRequest;
}
private TreeMap<String, String> handleUri(URI uri) {
TreeMap<String, String> urlMap = new TreeMap<>();
try {
String rawQuery = uri.getRawQuery();
if (StringUtils.isNotEmpty(rawQuery)) {
String queryParams = URLDecoder.decode(rawQuery, "utf-8");
String[] params = queryParams.split("&", -1);
for (int i = 0; i < params.length; i++) {
String[] paramPair = params[i].split("=");
if (paramPair.length == 2 && !"sign".equals(paramPair[0])) {
urlMap.put(paramPair[0], paramPair[1]);
}
}
}
}
catch (UnsupportedEncodingException e) {
logger.error("UnsupportedEncodingException", e);
}
return urlMap;
}
@Autowired
private ThrPlatInfoConfig thrPlatInfoConfig;
private String getSign(TreeMap<String, String> urlParamMap, String bodyStr) {
String thirdId = null;
// 签名原文PartA:
StringBuilder signSource = new StringBuilder();
// 除去sign的地址请求参数列表拼接而成的字符串
int i = 0;
for (Map.Entry<String, String> entry : urlParamMap.entrySet()) {
String paramKey = entry.getKey();
if ("thirdId".equals(paramKey)) {
thirdId = entry.getValue();
}
if (i > 0) {
signSource.append("&");
}
if (StringUtils.isNoneBlank(entry.getValue())) {
signSource.append(paramKey).append("=").append(entry.getValue());
}
i++;
}
if (StringUtils.isEmpty(thirdId)) {
throw new BussinessException(CommResultCode.SYSTEM_ERROR);
}
// 根据三方平台ID获取对应的三方平台发放与我们的密钥
ThrPlatConfigProp thrPlatConfigInfo = thrPlatInfoConfig.getConfigByCustId(thirdId);
if (thrPlatConfigInfo == null) {
throw new BussinessException(CommResultCode.SYSTEM_ERROR);
}
String meAppSecret = thrPlatConfigInfo.getMySecret();
if (StringUtils.isEmpty(meAppSecret)) {
throw new BussinessException(CommResultCode.SYSTEM_ERROR);
}
// 签名原文PartB:
signSource.append(uleAppSecret);
// 签名原文PartC:
if (StringUtils.isNotBlank(bodyStr)) {
signSource.append(bodyStr);
}
try {
return MDUtils.md5EncodeForHex(signSource.toString(), "utf-8");
}
catch (UnsupportedEncodingException e) {
logger.error("UTF-8 is unsupported", e);
throw new BussinessException(CommResultCode.SYSTEM_ERROR);
}
catch (NoSuchAlgorithmException e) {
logger.error("MessageDigest不支持MD5", e);
throw new BussinessException(CommResultCode.SYSTEM_ERROR);
}
}
}
拦截器在请求创建时加入。下面给出示例。
/**
* 创建访问三方服务的RestTemplate对象
*
* @param requestFactory HttpRequestFactory
* @return RestTemplate
*/
@Bean(name = "thrRestTemplate")
public RestTemplate thrRestTemplate(@Qualifier("requestFactory") ClientHttpRequestFactory requestFactory) {
RestTemplate restTemplate = new RestTemplate(requestFactory);
List<HttpMessageConverter<?>> converterList = restTemplate.getMessageConverters();
// 重新设置StringHttpMessageConverter字符集为UTF-8,解决中文乱码问题
HttpMessageConverter<?> converterTarget = null;
for (HttpMessageConverter<?> item : converterList) {
if (StringHttpMessageConverter.class == item.getClass()) {
converterTarget = item;
break;
}
}
if (null != converterTarget) {
converterList.remove(converterTarget);
}
// 都是JSON报文
converterList.add(1, fastJsonHttpMessageConverter);
// 设置错误处理器
restTemplate.setErrorHandler(new DefaultResponseErrorHandler());
// 设置拦截器,添加共通签名至URI参数中
ArrayList<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(httpRequestInterceptor);
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
@Resource(name = "toThrPlatSignInterceptor")
private ClientHttpRequestInterceptor httpRequestInterceptor;