1. 原因
- 需求需要通过小程序定位拿到用户所在行政区信息,但是小程序定位只能拿到经纬度信息,所以需要调用腾讯地图的逆地址解析(我认为:微信是腾讯的,那么使用腾讯地图的逆地址解析经度应该不会损失太多)
- 如果WebServiceAPI Key配置中签名校验,那么调用接口就需要进行验签
2. WebServiceAPI(GET方法)签名计算
官方文档地址:以下内容是从官方文档摘写下来的验签规则
(1)通用概念:
a. 请求路径:调用接口时的路径,如:/ws/geocoder/v1,末尾是否带 / 均可,不做要求,但需要保持一致,比如调用路径用了/ws/geocoder/v1,签名计算的时候也要用/ws/geocoder/v1
b. SecretKey (SK):在腾讯位置服务控制台 > Key配置中,勾选WebServiceAPI的 SN校验时自动生成的随机字串,用于计算签名(sig)
c. sig:签名计算结果
通过以下示例讲解(本例为调用逆地址解析请求的url):
https://apis.map.qq.com/ws/geocoder/v1?location=28.7033487,115.8660847&key=5Q5BZ-5EVWJ-SN5F3-K6QBZ-B3FAO-*****
(2)GET请求分为:域名,请求路径和参数三个部分,用于签名计算的有:
请求路径:/ws/geocoder/v1
请求参数:location=28.7033487,115.8660847&key=5Q5BZ-5EVWJ-SN5F3-K6QBZ-B3FAO-*****
a. 首先对参数进行排序:按参数名升序(本例结果为key在前,location在后):
key=5Q5BZ-5EVWJ-SN5F3-K6QBZ-B3FAO-*****&location=28.7033487,115.8660847
b. 签名计算(sig):
请求路径+”?”+请求参数+SK进行拼接,并计算拼接后字符串md5值(字符必须为小写),即为签名(sig):
要求:请求参数必须是未进行任何编码(如urlencode)的原始数据
md5("/ws/geocoder/v1?key=5Q5BZ-5EVWJ-SN5F3-*****&location=28.7033487,115.8660847SWvT26ypwq5Nwb5RvS8cLi6NSoH8HlJX")
本例计算得到结果为:90da272bfa19122547298e2b0bcc0e50
c. 生成最终请求:将计算得到的签名sig,放到请求中(参数名即为:sig):
https://apis.map.qq.com/ws/geocoder/v1?key=5Q5BZ-5EVWJ-SN5F3-K6QBZ-B3FAO-*****&location=28.7033487,115.8660847&sig=90da272bfa19122547298e2b0bcc0e50
注意:计算 sig 要使用原始参数值,不要进行任何编码,但最终发送时的参数,是需要对接口传入的参数值做url编码的
url编码方式:
以地点检索接口位例:
请求:...域名省略.../place/v1/search?boundary=region(北京)&keyword=美食
错误方式:"...域名省略.../place/v1/search?"+urlencode("boundary=region(北京)&keyword=美食")
正确方式:"...域名省略.../place/v1/search?boundary="+urlencode("region(北京)")+"&keyword="+urlencode("美食")
注:示例中的urlencode()代表url编码函数,不同开发语言的存在不同,以您实际为准
3. 腾讯地图java验签工具类
常量信息
- KEY和SK自己去后台查看(我没有封装为配置,有需要的可以自行封装)
- 需要新增接口就在这个常量中新增,然后在工具类中引用就行
package com.applets.manager.core.constant;
/**
* @author zr 2024/4/26
*/
public class TencentMapConstant {
/** KEY */
public static final String KEY = "xxxxx-xxxxx-xxxxx-xxxxx-xxxxx-QGBVW";
/** SecretKey (SK) */
public static final String SK = "xxxxxxxxxxxxxxxxxxxxxxxx";
/** 域名 */
public static final String HOST = "https://apis.map.qq.com";
/** 逆地址解析api */
public static final String GEOCODER_API = "/ws/geocoder/v1";
// /** 距离矩阵 */
// public static final String DISTANCE_API = "/ws/distance/v1/matrix";
// /** IP定位 */
// public static final String IP_LOCATION_API = "/ws/location/v1/ip";
}
工具类
- 附带get接口验签和post接口验签
package com.applets.manager.core.util;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import com.applets.manager.core.constant.TencentMapConstant;
import lombok.extern.slf4j.Slf4j;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
/**
* @author zr 2024/4/26
*/
@Slf4j
public class TencentMapUtil {
/**
* 签名计算(sig)_GET:
* @param path 请求路径
* @param params 请求参数
* @return
*/
public static String generateSignatureGet(String path, Map<String, String> params) {
try {
//将参数按照 key 进行字典序排序
TreeMap<String, String> sortedParams = new TreeMap<>(params);
//构建原始签名字符串
StringBuilder rawSignatureBuilder = new StringBuilder();
rawSignatureBuilder.append(path).append("?");
int entryIndex = 0;
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
rawSignatureBuilder.append(entry.getKey()).append("=").append(entry.getValue());
if (entryIndex < sortedParams.size() - 1) {
rawSignatureBuilder.append("&");
}
entryIndex++;
}
rawSignatureBuilder.append(TencentMapConstant.SK);
String rawSignature = rawSignatureBuilder.toString();
// 计算 MD5 签名
byte[] signatureBytes = MessageDigest.getInstance("MD5").digest(rawSignature.getBytes("UTF-8"));
String signature = byteArrayToHexString(signatureBytes);
return URLEncoder.encode(signature, "UTF-8"); // 进行 URL 编码
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
log.error("签名计算失败: {}", e.getMessage(), e);
return null;
}
}
private static String byteArrayToHexString(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
hexString.append(String.format("%02x", b));
}
return hexString.toString();
}
/**
* 对参数进行编码
* @param params
* @return
*/
public static String encodeParams(Map<String, String> params) {
TreeMap<String, String> sortedParams = new TreeMap<>(params);
StringBuilder rawSignatureBuilder = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
rawSignatureBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
return rawSignatureBuilder.toString();
}
/**
* 签名计算(sig)_POST:
* @param jsonObject
*/
public static String generateSignaturePost(String path, JSONObject jsonObject) {
//一级属性名排序字符升序排序
Set<String> propertyNames = jsonObject.keySet();
List<String> sortedPropertyNames = new ArrayList<>(propertyNames);
Collections.sort(sortedPropertyNames);
JSONObject sortedJsonObject = new JSONObject();
for (String propertyName : sortedPropertyNames) {
// Value转成JSON string
sortedJsonObject.put(propertyName, JSONUtil.toJsonStr(jsonObject.get(propertyName)));
}
//替换原始的 JSONObject
jsonObject.clear();
jsonObject.putAll(sortedJsonObject);
//拼接成rawSignatureBuilder型的字符串
StringBuilder rawSignatureBuilder = new StringBuilder();
for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
String propertyName = entry.getKey();
String propertyValue = (String) entry.getValue();
// 拼接成 rawSignatureBuilder 型的字符串
rawSignatureBuilder.append(propertyName)
.append("=")
.append(propertyValue)
.append("&");
}
//去除末尾的"&"
if (rawSignatureBuilder.length() > 0) {
rawSignatureBuilder.deleteCharAt(rawSignatureBuilder.length() - 1);
}
//签名计算(sig) 请求路径+”?”+请求参数+SK
rawSignatureBuilder.insert(0, path + "?").append(TencentMapConstant.SK);
String rawSignature = rawSignatureBuilder.toString();
// 计算 MD5 签名
try {
byte[] signatureBytes = MessageDigest.getInstance("MD5").digest(rawSignature.getBytes("UTF-8"));
String signature = byteArrayToHexString(signatureBytes);
return URLEncoder.encode(signature, "UTF-8"); // 进行 URL 编码
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
log.error("签名计算失败: {}", e.getMessage(), e);
}
return null;
}
}
4. 测试
package com.applets.manager.core;
import cn.hutool.core.map.MapUtil;
import com.alibaba.fastjson.JSON;
import com.applets.manager.core.constant.TencentMapConstant;
import com.applets.manager.core.util.TencentMapUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;
import java.util.*;
/**
* @author zr 2024/4/26
*/
@RunWith(SpringRunner.class)
//@SpringBootTest(classes = StartApplication.class)
@Slf4j
public class TencentTest {
@Test
public void getTest() {
/** get请求示例 */
Map<String, String> params = MapUtil.builder(new HashMap<String, String>())
.put("location", "28.7033487,115.8660847")
.put("key", TencentMapConstant.KEY)
.build();
// 生成最终请求
String signature = TencentMapUtil.generateSignatureGet(TencentMapConstant.GEOCODER_API, params);
String finalRequest = TencentMapConstant.HOST + TencentMapConstant.GEOCODER_API + "?" + TencentMapUtil.encodeParams(params) + "sig=" + signature;
System.out.println(finalRequest);
RestTemplate restTemplate = new RestTemplate();
TencentMapResult forObject = restTemplate.getForObject(finalRequest, TencentMapResult.class);
log.info("地址:{}", JSON.toJSONString(forObject));
}
@Test
public void postTest() {
// /** post请求示例 */
// JSONObject requestData = new JSONObject();
// requestData.set("mode", "driving");
// requestData.set("key", TencentMapConstant.KEY);
// requestData.set("from", "32.139063,118.724270");
// requestData.set("to", "32.150763,118.734398;32.157158,118.696632");
//
// String signature = TencentMapUtil.generateSignaturePost(TencentMapConstant.DISTANCE_API, requestData);
// String finalRequest = TencentMapConstant.HOST + TencentMapConstant.DISTANCE_API + "?mode=driving" + "&sig=" + signature;
// System.out.println(finalRequest);
// RestTemplate restTemplate = new RestTemplate();
// System.out.println(restTemplate.postForObject(finalRequest, requestData, TencentMapResult.class));
}
@Data
public static class TencentMapResult<T> {
/**
* 状态码,0为正常,其它为异常
*/
private Integer status;
/**
* 状态说明
*/
private String message;
/**
* 本次请求的唯一标识
*/
private String request_id;
/**
* 查询结果总数量
*/
private Integer count;
/**
* 返回数据 > 数组
*/
private T data;
/**
* 返回数据 > 对象
*/
private T result;
}
}
测试结果
因为我这里只开通了一个get接口的使用权限,所以post的就不测了,测试方法在TencentTest.postTest(),需要将TencentMapConstant.DISTANCE_API换成自己的即可