# 美团企业版签名
# 0. 签名示例
/**
* Author: chenzhenfeng@meituan.com
* Date: 17/12/9 下午6:15
* Description:
* 示例
*/
public class WaimaiClient {
private static final String VERSION = "1.0";
private static final String FORM_URLENCODED = "application/x-www-form-urlencoded";
private static final String ACCEPT_JSON = "application/json";
protected String host = "https://sqt-api.test.meituan.com"; // 测试环境域名
protected String token;
protected String sign;
protected String aesKey;
public WaimaiClient(String token, String sign, String aesKey) {
this.token = token;
this.sign = sign;
this.aesKey = aesKey;
}
// setter & getter 省略
private void setBaseParam(BaseApiRequest apiRequest) {
apiRequest.setTs(System.currentTimeMillis()/1000);
apiRequest.setSign(getSign());
}
protected String commonPostInvoke(String url, WmBaseApiRequest request) {
setBaseParam(request);
try {
String rawContent = JsonUtil.object2Json(request, false);
String body = "token=" + getToken() + "&version=" + VERSION
+ "&content=" + EncryptUtil.aesEncrypt(rawContent, getAesKey());
String responseStr = HttpClientUtil.invokePost(url, body, FORM_URLENCODED, ACCEPT_JSON);
return responseStr;
} catch (Exception e) {
LOGGER.error("HTTP调用失败, url:{}", url, e);
}
return null;
}
public WmPoiListQueryResult poiListQuery(WmPoiListQueryRequest request) {
request.setMethod("waimai.poi.list");
String result = commonPostInvoke(host + "/waimai/v1/poi/list", request);
return JsonUtil.json2Object(result, WmPoiListQueryResult.class);
}
public static void main(String[] args) throws Exception {
// 从美团企业版获取
WaimaiClient client = new WaimaiClient("xxxx", "xxxx", "xxxx");
WmPoiListQueryRequest request = new WmPoiListQueryRequest();
// 经纬度对应北京望京国际研发园
request.setLongitude(116488645);
request.setLatitude(40007069);
// 查询望京国际研发园附件的外卖商家
WmPoiListQueryResult result = client.poiListQuery(request);
}
}
# 1. 请求参数
// 请求基类(公共请求参数)
public class WmBaseApiRequest {
private String sign;
private Long ts;
private String method;
// getter & setter 省略
}
// 业务请求示例(商家列表页查询)
public class WmPoiListQueryRequest extends WmBaseApiRequest {
private Integer longitude; // 用户当前经度
private Integer latitude; // 用户当前纬度
private String keyword;
private Integer sort_type;
private Integer page_index;
private Integer page_size;
// getter & setter 省略
}
// 响应基类
public class HttpBaseResult<T> {
private int status;
private String msg;
private T data;
}
// 业务响应体示例 (商家列表页查询结果)
public class PoiListDTO {
private Integer poi_total_num;
private Integer have_next_page;
private Integer current_page_index;
private Integer page_size;
private List<PoiBaseInfoDTO> openPoiBaseInfoList; // 参考接口文档定义
// getter & setter 省略
}
// 业务响应示例 (商家列表页查询)
public class WmPoiListQueryResult extends HttpBaseResult<PoiListDTO> {
}
# 2. 加密类 EncryptUtil
基于commons-codec实现,pom引入依赖
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.10</version>
</dependency>
/**
* Author: chenzhenfeng@meituan.com
* Date: 17/12/9 下午6:15
* Description:
*/
public class EncryptUtil {
private EncryptUtil() {
}
private static final Logger LOGGER = LoggerFactory.getLogger(EncryptUtil.class);
private static final String ALGORITHM_HMAC_SHA1 = "HmacSHA1";
private static final String BASE_NUMBER = "0123456789";
// md5加密
public static String md5(String plainText) {
if (StringUtils.isBlank(plainText)) {
return null;
} else {
return DigestUtils.md5Hex(plainText);
}
}
// sha1加密
public static String sha1(String plainText) {
if (StringUtils.isBlank(plainText)) {
return null;
} else {
return DigestUtils.sha1Hex(plainText);
}
}
// base64
public static String base64(String input) {
return Base64.encodeBase64String(input.getBytes());
}
// HMAC_SHA1加密
public static String hmacSha1(String plainText, String secretKey) {
SecretKeySpec secretKeySpec = new SecretKeySpec(org.apache.commons.codec.binary.StringUtils.getBytesUtf8(secretKey), ALGORITHM_HMAC_SHA1);
Mac mac;
try {
mac = Mac.getInstance(ALGORITHM_HMAC_SHA1);
mac.init(secretKeySpec);
return Base64.encodeBase64String(mac.doFinal(org.apache.commons.codec.binary.StringUtils.getBytesUtf8(plainText)));
} catch (GeneralSecurityException e) {
throw new IllegalArgumentException(e);
}
}
// 生成AES秘钥
public static String generateAESSecretKey() {
try {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom());
return Base64.encodeBase64String(kgen.generateKey().getEncoded());
} catch (NoSuchAlgorithmException e) {
LOGGER.error("AesCypher.genKey NoSuchAlgorithmException", e);
return null;
}
}
// AES加密
public static String aesEncrypt(String originText, String secret) throws Exception {
AesCypher cypher = new AesCypher(secret);
return cypher.encrypt(originText);
}
// AES解密
public static String aesDecrypt(String encryptedText, String secret) throws Exception{
AesCypher cypher = new AesCypher(secret);
return cypher.decrypt(encryptedText);
}
public static String getRandomString(int length) {
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(BASE_NUMBER.charAt(random.nextInt(BASE_NUMBER.length())));
}
return sb.toString();
}
// 内部aes类
static class AesCypher {
private static final Logger LOGGER = LoggerFactory.getLogger(com.meituan.bep.sqt.lib.common.util.AesCypher.class);
private static String DEFAULT_SECRET = "hqXj7NwyKYaL_Lc5TlCSzA==";
private byte[] linebreak;
private SecretKey key;
private Cipher cipher;
private Base64 coder;
public AesCypher(String secret) {
this.linebreak = new byte[0];
try {
this.coder = new Base64(32, this.linebreak, true);
byte[] secrets = this.coder.decode(secret);
// 转换为AES专用密钥
this.key = new SecretKeySpec(secrets, "AES");
// 创建密码器,算法/工作模式/补码方式 提供商
this.cipher = Cipher.getInstance("AES/ECB/PKCS5Padding", "SunJCE");
} catch (Exception e) {
LOGGER.error("AesCypher.genKey NoSuchAlgorithmException", e);
}
}
public AesCypher() {
this(DEFAULT_SECRET);
}
public synchronized String encrypt(String plainText) throws Exception {
this.cipher.init(Cipher.ENCRYPT_MODE, this.key);
byte[] cipherText = this.cipher.doFinal(plainText.getBytes());
return new String(this.coder.encode(cipherText));
}
public synchronized String decrypt(String codedText) throws Exception {
byte[] encypted = this.coder.decode(codedText.getBytes());
this.cipher.init(Cipher.DECRYPT_MODE, this.key);
byte[] decrypted = this.cipher.doFinal(encypted);
return new String(decrypted, "UTF-8");
}
}
public static void main(String[] args) throws Exception {
String plainText = "";
// aes解密 和 解密
String aesKey = EncryptUtil.generateAESSecretKey();
System.out.println("AES秘钥: " + aesKey);
String aesEncrypted = EncryptUtil.aesEncrypt(plainText, aesKey);
System.out.println("AES密文: " + aesEncrypted);
System.out.println("AES明文: " + EncryptUtil.aesDecrypt(aesEncrypted, aesKey));
}
}
# 3. Http封装 HttpClientUtil
基于apache httpclient实现,pom引入依赖
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
/**
* Author: chenzhenfeng@meituan.com
* Date: 17/12/11 下午3:25
* Description:
*/
public class HttpClientUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientUtil.class);
private static PoolingHttpClientConnectionManager httpClientConnectionManager = new PoolingHttpClientConnectionManager();
private static CloseableHttpClient httpClient = null;
private static CloseableHttpClient httpsClient = null;
private static HttpClientUtil.HttpConfig httpConfig = null;
private static SSLConnectionSocketFactory sslsf = null;
// 连接池最大连接数和每路由最大连接数可通过httpconf.properties进行配置
private static final String HTTP_CONF_FILE_NAME = "fenxiao-httpconf.properties";
private static final int MAX_TOTAL_CONNECTION = 800;
private static final int MAX_PER_ROUTE = 150;
private static String UTF_8 = "UTF-8";
interface ContentType {
String FORM_URLENCODED = "application/x-www-form-urlencoded";
String JSON = "application/json; charset=utf-8";
}
interface AcceptType {
String ACCEPT_JSON = "application/json";
String ACCEPT_ANNY = "*/*";
}
private HttpClientUtil() {
// prevent instantialization
}
static {
loadConf();
httpClientConnectionManager.setMaxTotal(httpConfig.getMaxTotalConnection()); // 连接池最大连接数
httpClientConnectionManager.setDefaultMaxPerRoute(httpConfig.getMaxPerRoute()); // 每路由最大连接数
// 通过连接池获取的httpClient
httpClient = HttpClients.custom().setConnectionManager(httpClientConnectionManager).build();
// 通过连接池获取的httpsClient, 能支持https
httpsClient = HttpClients.custom().setSSLSocketFactory(sslsf).setConnectionManager(httpClientConnectionManager).build();
LOGGER.info("HttpClient initialization");
}
/**
* 如果配置httpconf.properties, 则从该文件中读取http连接池的配置参数, 否则使用默认值
*/
private static void loadConf() {
if (httpConfig == null) {
httpConfig = new HttpConfig();
}
Properties properties = new Properties();
try {
InputStream inputStream = HttpClientUtil.class.getClassLoader().getResourceAsStream(HTTP_CONF_FILE_NAME);
if (inputStream == null) {
LOGGER.warn("httpConfig file={} does not exist", HTTP_CONF_FILE_NAME);
return;
}
properties.load(inputStream);
int maxTotalConnection = Integer.parseInt(properties.getProperty("max_total_connection"));
int maxPerRoute = Integer.parseInt(properties.getProperty("max_per_route"));
LOGGER.info("max_total_connection={}, max_per_route={}", maxTotalConnection, maxPerRoute);
httpConfig.setMaxTotalConnection(maxTotalConnection);
httpConfig.setMaxPerRoute(maxPerRoute);
} catch (IOException e) {
LOGGER.warn("read httpConfig from file={} failed", HTTP_CONF_FILE_NAME, e);
} catch (NumberFormatException e) {
LOGGER.warn("read httpConfig from file={} failed", HTTP_CONF_FILE_NAME, e);
} catch (Exception e) {
LOGGER.warn("read httpConfig from file={} failed", HTTP_CONF_FILE_NAME, e);
}
}
private static void initHttps() {
X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
// 初始化SSL上下文
sslContext.init(null, new TrustManager[]{trustManager}, null);
// SSL套接字连接工厂,NoopHostnameVerifier为信任所有服务器
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
} catch (NoSuchAlgorithmException | KeyManagementException e) {
LOGGER.error("初始化https支持失败", e);
}
}
static class HttpConfig {
private int maxTotalConnection = MAX_TOTAL_CONNECTION;
private int maxPerRoute = MAX_PER_ROUTE;
public int getMaxTotalConnection() {
return maxTotalConnection;
}
public void setMaxTotalConnection(int maxTotalConnection) {
this.maxTotalConnection = maxTotalConnection;
}
public int getMaxPerRoute() {
return maxPerRoute;
}
public void setMaxPerRoute(int maxPerRoute) {
this.maxPerRoute = maxPerRoute;
}
}
public static String invokePost(String url, Map<String, String> headers, Map<String, String> params) throws Exception {
URIBuilder uriBuilder = new URIBuilder();
valueForUriBuilder(url, uriBuilder);
HttpPost httpPost = new HttpPost(uriBuilder.build());
if (headers != null && headers.size() > 0) {
for (Map.Entry<String, String> param : headers.entrySet()) {
httpPost.addHeader(param.getKey(), param.getValue());
}
}
if (params != null && params.size() > 0) {
List<NameValuePair> pairs = new ArrayList<NameValuePair>();
for (Map.Entry<String, String> param : params.entrySet()) {
pairs.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
httpPost.setEntity(new UrlEncodedFormEntity(pairs, UTF_8));
}
return sendRequest(url, httpPost);
}
public static String invokePost(String url, Map<String, String> headers, String body) throws Exception {
URIBuilder uriBuilder = new URIBuilder();
valueForUriBuilder(url, uriBuilder);
HttpPost httpPost = new HttpPost(uriBuilder.build());
if (headers != null && headers.size() > 0) {
for (Map.Entry<String, String> param : headers.entrySet()) {
httpPost.addHeader(param.getKey(), param.getValue());
}
}
if (body != null) {
httpPost.setEntity(new StringEntity(body, Charset.forName(UTF_8)));
}
return sendRequest(url, httpPost);
}
public static String invokePost(String url, String body, String contentType, String accept) throws Exception {
URIBuilder uriBuilder = new URIBuilder();
valueForUriBuilder(url, uriBuilder);
HttpPost httpPost = new HttpPost(uriBuilder.build());
httpPost.addHeader("Content-type",contentType);
httpPost.setHeader("Accept", accept);
httpPost.setEntity(new StringEntity(body, Charset.forName(UTF_8)));
return sendRequest(url, httpPost);
}
public static String invokeGet(String url) throws Exception {
// 原始访问url
HttpGet httpGet = new HttpGet(url);
return sendRequest(url, httpGet);
}
private static String sendRequest(String url, HttpRequestBase request) throws Exception {
CloseableHttpClient client;
if (url.startsWith("https")) {
client = httpsClient;
} else {
client = httpClient;
}
long st = System.currentTimeMillis();
String responseStr;
int status = 0;
try {
CloseableHttpResponse response = client.execute(request);
HttpEntity entity = response.getEntity();
status = response.getStatusLine().getStatusCode();
if (entity != null) {
responseStr = EntityUtils.toString(entity, UTF_8);
response.close();
} else {
responseStr = StringUtils.EMPTY;
}
} catch (SocketTimeoutException e) {
LOGGER.error("HttpClient.sendRequest, url:{}, use_time:{} ms",
request.getURI(), (System.currentTimeMillis() - st), e);
throw e;
} catch (ClientProtocolException e) {
LOGGER.error("HttpClient.sendRequest, url:{}, use_time:{} ms",
request.getURI(), (System.currentTimeMillis() - st), e);
throw e;
} catch (IOException e) {
LOGGER.error("HttpClient.sendRequest, url:{}, use_time:{} ms",
request.getURI(), (System.currentTimeMillis() - st), e);
throw e;
} finally {
request.releaseConnection();
}
LOGGER.info("HttpClient Success, status:{}, url:{}, use_time:{} ms",
status, request.getURI(), (System.currentTimeMillis() - st));
return responseStr;
}
private static RequestConfig buildRequestConfig(Integer connTimeout, Integer readTimeout) {
RequestConfig.Builder customReqConf = RequestConfig.custom();
if (connTimeout != null) {
customReqConf.setConnectTimeout(connTimeout);
}
if (readTimeout != null) {
customReqConf.setSocketTimeout(connTimeout);
}
return customReqConf.build();
}
private static void valueForUriBuilder(String url, URIBuilder uriBuilder) {
Integer apartIndex = url.indexOf("?");
if (apartIndex == -1) {
uriBuilder.setPath(url);
} else {
uriBuilder.setPath(url.substring(0, apartIndex));
uriBuilder.setCustomQuery(url.substring(apartIndex + 1, url.length()));
}
}
}
# 4. nodeJs版加密解密参考
const crypto = require('crypto');
var SqtAESUtil = {};
SqtAESUtil.MyConstanst = {
ENCODING: 'utf8',
BASE64: 'base64',
MODE: 'aes-128-ecb',
IV: new Buffer(''),
BUFFER: 'buffer',
SECRETKEY: 'xd1nzb/N9Nx3+VoImzCsnw==' //美团企业版密钥
};
/**
* 加密
* @param plainText 要加密的明文内容
* @returns {string} 返回字符串
*/
SqtAESUtil.aesEncrypt = function (plainText) {
var secretkey = new Buffer(SqtAESUtil.MyConstanst.SECRETKEY, SqtAESUtil.MyConstanst.BASE64);
var cipherChunks = [];
var cipher = crypto.createCipheriv(SqtAESUtil.MyConstanst.MODE, secretkey, SqtAESUtil.MyConstanst.IV);
cipher.setAutoPadding(true);
cipherChunks.push(cipher.update(new Buffer(plainText, SqtAESUtil.MyConstanst.ENCODING), SqtAESUtil.MyConstanst.BUFFER, SqtAESUtil.MyConstanst.BASE64));
cipherChunks.push(cipher.final(SqtAESUtil.MyConstanst.BASE64));
return cipherChunks.join('');
}
/**
* 解密
* @param encryptText 密文
* @returns {string} 字符串
*/
SqtAESUtil.aesDecrypt = function (encryptText) {
var secretkey = new Buffer(SqtAESUtil.MyConstanst.SECRETKEY, SqtAESUtil.MyConstanst.BASE64);
var cipherChunks = [];
var decipher = crypto.createDecipheriv(SqtAESUtil.MyConstanst.MODE, secretkey, SqtAESUtil.MyConstanst.IV);
decipher.setAutoPadding(true);
cipherChunks.push(decipher.update(encryptText, SqtAESUtil.MyConstanst.BASE64, SqtAESUtil.MyConstanst.ENCODING));
cipherChunks.push(decipher.final(SqtAESUtil.MyConstanst.ENCODING));
return cipherChunks.join('');
}
/**
* 测试加密解密
*/
SqtAESUtil.test = function () {
var data = '{"sign":"sgW1bxc7oatFhOJXAeHnNg==","ts":1512964057,"method":"waimai.poi.list","longitude":116488645,"latitude":40007069}';
var result = SqtAESUtil.aesEncrypt(data);
console.log("加密之前明文内容: " + data);
console.log("nodeJs加密结果: " + result);
console.log("nodeJs解密结果: " + SqtAESUtil.aesDecrypt(result));
var sqt = 'UgJn07uNgW7S7fJK0R0xVbaLxoCGPQIzoP-_K4Hmp4RduGszhm2mbUs2toZhCtXKP5JGXVTZ9kGts2Wx3IJQCd90ptMoJTDB0vu7mkedEr4KZCvZn77EZLssMC5SpXilmQ-5RXHzvMIT0ASH-IXepTP_O16U37QqCkEb5L1WLy4';
console.log("美团企业版java生成的密文: " + sqt);
console.log("nodejs解密: " + SqtAESUtil.aesDecrypt(sqt));
}
// 测试
SqtAESUtil.test();
// 结果
/*
加密之前明文内容: UgJn07uNgW7S7fJK0R0xVbaLxoCGPQIzoP+/K4Hmp4RduGszhm2mbUs2toZhCtXKP5JGXVTZ9kGts2Wx3IJQCd90ptMoJTDB0vu7mkedEr4KZCvZn77EZLssMC5SpXilmQ+5RXHzvMIT0ASH+IXepTP/O16U37QqCkEb5L1WLy4=
nodeJs加密结果: UgJn07uNgW7S7fJK0R0xVbaLxoCGPQIzoP+/K4Hmp4RduGszhm2mbUs2toZhCtXKP5JGXVTZ9kGts2Wx3IJQCd90ptMoJTDB0vu7mkedEr4KZCvZn77EZLssMC5SpXilmQ+5RXHzvMIT0ASH+IXepTP/O16U37QqCkEb5L1WLy4=
nodeJs解密结果: {"sign":"sgW1bxc7oatFhOJXAeHnNg==","ts":1512964057,"method":"waimai.poi.list","longitude":116488645,"latitude":40007069}
美团企业版java生成的密文: UgJn07uNgW7S7fJK0R0xVbaLxoCGPQIzoP-_K4Hmp4RduGszhm2mbUs2toZhCtXKP5JGXVTZ9kGts2Wx3IJQCd90ptMoJTDB0vu7mkedEr4KZCvZn77EZLssMC5SpXilmQ-5RXHzvMIT0ASH-IXepTP_O16U37QqCkEb5L1WLy4
nodejs解密: {"sign":"sgW1bxc7oatFhOJXAeHnNg==","ts":1512964057,"method":"waimai.poi.list","longitude":116488645,"latitude":40007069}
*/