目录
前言
在现代软件开发中,地图服务的集成已成为众多应用的必备功能之一。大家日常使用频率较高的除了百度地图之外,高德地图也是其中的重要服务提供商,其丰富的 API 接口为开发者提供了便利。然而,在使用 Java 调用高德地图服务时,如果开发者开启了数字签名的机制,可能会遇到各种问题,其中最常见的便是 10007 INVALID_USER_SIGNATURE 错误。这一错误表明数字签名未通过验证,导致无法正常访问高德地图的服务。

在之前的博客中,我曾经对百度地图的SN数字签名机制进行了详细的讲解,包括基于Java的原生请求机制和基于UniHttp的封装请求机制,原文链接如下:
|----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 序号 | 博客地址 |
| 1 | Java 开发者如何搞定百度地图 SN 权限签名实践-以搜索2.0接口为例 |
| 2 | Java调用UniHttp接口请求失败?一次开源的深入实践-百度SN签名认证场景下参数乱序问题的三种解决策略 |
很多朋友反馈对百度地图的数字签名机制介绍很详细,能不能对高德地图的数字签名机制也进行介绍介绍。众所周知,数字签名机制是为了确保请求的安全性和合法性而设计的。当开发者在开放平台的控制台中开启了数字签名功能后,就需要按照指定的算法生成数字签名。如果签名生成过程中出现任何问题,如参数错误、算法不匹配或密钥错误等,都会导致请求错误,因此数字签名机制其实是一种安全保障机制。虽然之前对百度地图的数字签名机制有一定的了解,本来以为高德验证机制比较跟百度的类似,转换使用起来应该非常块,实践证明。相似性和直接照搬还是不同的,"纸上得来终觉浅,绝知此事要躬行"。两个开放平台之间还是有一定的差别的。
本文即在此背景下产生,文章主要讲解如何在高德地图中开启SIG数字签名,开启数字签名后可能会遇到什么问题,然后介绍使用Java进行调用时可能遇到的场景以及如何解决调用时返回"info":"INVALID_USER_SIGNATURE","infocode":"10007"的错误。通过这些内容的展开,开发者可以有效地解决 Java 调用高德地图服务时遇到的 10007 INVALID_USER_SIGNATURE 错误,同时由于高德地图在官网上也没有提供示例代码,因此本文也算是一种补充,让各位开发者可以直接参考使用。
一、如何开启高德的数字签名
本节将简单介绍如何在高德地图中开启数字签名的应用配置和官方的生成机制介绍。
1、应用配置
进入控制台-应用管理-我的应用。
1)新建Key/选择相应的Key。

2)点击"设置",打开数字签名,提交后关闭"设置"页面

(注意:该私钥与Key对应,请注意保存,谨防泄露。)
2、官方的生成机制
我们可以查看官方提供的数字签名生成机制说明网页链接,如下图所示:

从官方的说明可以知晓:
生成签名
签名格式:sig=MD5( 请求参数(包括key)键值对(按参数名的升序排序 ),加(请注意"加"字无需输入)私钥**)。例如:**
请求服务为"testservice";请求参数分别为"a=23,b=12,d=48,f=8,c=67";私钥为"bbbbb"。 则数字签名为:sig=md5(a=23&b=12&c=67&d=48&f=8bbbbb)
注意:
生成签名的内容,(上文提到的拼装的参数,也就是md5()中的内容),必须为utf-8编码格式。
在计算md5的参数如果出现+号,请正常计算sig,但在请求的时候,需要用urlencode进行编码再请求。
请求参数排序需要注意,如果参数名的第一个字母顺序相同,就比较第二个字母。以此类推,直至得到排序结果。
在请求中添加签名
将签名sig作为参数添加至请求参数中:参数名为sig,值为根据请求参数与私钥计算出的值。
二、Java集成UniHttp
本节将以UniHttp为例,详细讲解如何在Java中集成UniHttp,并且使用Java实现非Sig验证的接口访问以及如何定义高德数字签名工具类。通过本节可以了解如何在UniHttp中进行接口的定义以及高德数字签名的具体实现。
1、UniHttp接口定义
首先我们需要定义一个带数字签名的访问接口,这里以地理编码接口为例进行说明,关键代码如下:
java
package com.yelang.project.thridinterface;
import com.burukeyou.uniapi.http.annotation.HttpApi;
import com.burukeyou.uniapi.http.annotation.param.QueryPar;
import com.burukeyou.uniapi.http.annotation.request.GetHttpInterface;
import com.burukeyou.uniapi.http.core.response.HttpResponse;
@HttpApi(url = "https://restapi.amap.com/v3/geocode")
public interface AmapGeocodeWithSigService {
/**
* - 带sig签名的地理编码API服务
* @param address address
* @param city city
* @param key key
* @param sig 经过计算加密的sig
* @return
*/
@GetHttpInterface("/geo")
public HttpResponse<String> getGeoWithSig(@QueryPar("address") String address,@QueryPar("city") String city, @QueryPar("key") String key, @QueryPar("sig") String sig);
}
2、非SIG验证访问
为了让大家理解数字签名机制,这里我们简单介绍一下非SIG验证访问。即知道key就可以成功调用接口。简单方法如下:
java
package com.yelang.project.unihttp;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.LinkedHashMap;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.burukeyou.uniapi.http.core.response.HttpResponse;
import com.yelang.common.utils.StringUtils;
import com.yelang.project.thridinterface.AmapGeocodeService;
import com.yelang.project.thridinterface.AmapGeocodeWithSigService;
import com.yelang.project.thridinterface.signature.AmapSignature;
@SpringBootTest
@RunWith(SpringRunner.class)
public class AmapGeoSearchBySigCase {
private static final String AMAP_CLIENT_NOSIG_AK = "youramapkey";
@Autowired
private AmapGeocodeService geocodeService;//地理和逆地理编码
@Test
public void geoWithNoSig() throws InterruptedException {
String address = "湖南大学";
String city = "430100";//长沙
String key = AMAP_CLIENT_NOSIG_AK;
//step1、调用高德地理编码接口将地址转为经纬度
HttpResponse<String> result = geocodeService.getGeo(address, city, key);
if(StringUtils.isNotEmpty(result.getBodyResult())) {
System.out.println(result.getBodyResult());
}
}
}
在控制台中运行以上程序后,在控制台中可以看到以下输出,说明接口成功调用。

3、高德数字签名的实现
根据官网文档里面介绍的数字签名的实现方式,我们新建一个高德地图的数字签名生成器,能够自动处理数据,并生成正确的数字签名。核心代码如下:
java
package com.yelang.project.thridinterface.signature;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* - 高德 signature签名
* @author 夜郎king
*/
public class AmapSignature {
/**
* -应用申请的AK值
*/
private String akValue;
/**
* - 应用申请的SK值
*/
private String skValue;
/**
* - Map参数集合
*/
private Map<String, ?> data;
/**
* - API接口访问前缀
*/
private String apiPrefix;
public String getAkValue() {
return akValue;
}
public void setAkValue(String akValue) {
this.akValue = akValue;
}
public String getSkValue() {
return skValue;
}
public void setSkValue(String skValue) {
this.skValue = skValue;
}
public Map<String, ?> getData() {
return data;
}
public void setData(Map<String, ?> data) {
this.data = data;
}
public String getApiPrefix() {
return apiPrefix;
}
public void setApiPrefix(String apiPrefix) {
this.apiPrefix = apiPrefix;
}
public AmapSignature() {
super();
}
public AmapSignature(String akValue, String skValue, Map<String, ?> data) {
super();
this.akValue = akValue;
this.skValue = skValue;
this.data = data;
}
//来自stackoverflow的MD5计算方法,调用了MessageDigest库函数,并把byte数组结果转换成16进制
private String MD5(String md5) {
try {
java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
byte[] array = md.digest(md5.getBytes());
StringBuffer sb = new StringBuffer();
for (int i = 0; i < array.length; ++i) {
sb.append(Integer.toHexString((array[i] & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString();
} catch (java.security.NoSuchAlgorithmException e) {
}
return null;
}
/**
* 生成签名
*
* @param reOrder 重排序
* @return 签名
*/
public String generateSignature(boolean reOrder) {
// 参数排序
List<String> keys = new ArrayList<>(this.data.keySet());
// 需要重排序
if (reOrder) {
Collections.sort(keys);
}
// 拼接参数
StringBuilder sb = new StringBuilder();
for (String key : keys) {
sb.append(key).append("=").append(this.data.get(key)).append("&");
}
System.out.println("其它md5求解:" + this.MD5(sb.substring(0, sb.length() - 1) + this.skValue));
// 计算MD5
return MD5(sb.substring(0, sb.length() - 1) + this.skValue);
}
}
重排序的设置非常重要,主要是为了解决参数乱序时导致的请求数字签名不一致的问题。在下文中会详细涉及。
三、常见问题及解决办法
使用Java定义了高德地图的数字签名的具体实现后,下面我们就来使用三个真实的场景进行测试,看看会遇到什么问题以及我们是如何解决的。通过这几个问题的解决,就可以掌握在实际开发中容易遇到的问题。
1、编程式参数顺序设置
其实不管是百度地图还是高德地图,他们的数字签名的认证设计思想都比较相同,尤其是加密时的参数顺序,一定是不能错的,一旦有的参数顺序不对,那么最终加密得到的结果已经是错的。因此保证请求参数顺序非常重要。保证参数顺序的方式有很多种,首先我们来讲解第一种方式编程式参数顺序设置,顾名思义就是使用代码的方式来指定参数顺序。在Java当中,我们可以使用LinkedHashMap或者TreeMap来进行参数的设置,都是可以的。只是使用TreeMap默认就是按照字符来进行排序的。两者相差不大,我们以地理编码接口为例:
java
@Test
public void getWithSig() {
String city = "430100";//长沙
String key = AMAP_CLIENT_SIG_AK;
String address = "岳麓山风景区";
String secret_key = AMAP_CLIENT_SECRET_KEY;
LinkedHashMap<String, String> data = new LinkedHashMap<String, String>();
data.put("address", address);
data.put("city", city);
data.put("key", key);
System.out.println(data);
AmapSignature amapSignature = new AmapSignature(key, secret_key, data);
String sig = amapSignature.generateSignature(false);
System.out.println("sig==" + sig);
//step1、调用高德地理编码接口将地址转为经纬度
HttpResponse<String> result = geoCodeWithSigService.getGeoWithSig(address, city, key,sig);
System.out.println("开始执行");
if(StringUtils.isNotEmpty(result.getBodyResult())) {
System.out.println(result.getBodyResult());
}
}
这里的参数我们按照参数的key的值进行了手动排序,在程序中我们设置不需要参数排序,
java
String sig = amapSignature.generateSignature(false);
来看一下程序的运行结果:

我们随便改变参数的顺序,比如把key放到最前面去,在次运行结果如下:

2、参数重排序设置
编程式参数设置严重的依赖开发工程师,而且极易被忘记,因此我们在生成sig时直接对参数进行重排序,然后按照重排序的方式进行处理即可。重排序的方法很简单,创建sig时传入计算参数即可:
java
String sig = amapSignature.generateSignature(true);
传入参数后,计算的参数的函数逻辑如下:

这里会对keys这个集合进行重排序,这样就得到了参数顺序统一化的处理。在此运行后,无论前面的集合使用什么类型,哪怕是HashMap都没有问题,因为在生成数字签名时进行了统一排序就保证了正确性。
3、特殊字符的处理
在官网的手册上,有一个关于特殊字符的说明:
在计算md5的参数如果出现+号,请正常计算sig,但在请求的时候,需要用urlencode进行编码再请求。
这里的+号只是一个符号,还有可能有其它的符号,可以看下在查询字符串中增加特殊符号后,会报什么错误?

有了特殊字符后又会报这个10007的错误,在上面的提示中已经包含了解决方法,sig计算正常计算数字签名,但是请求时需要编码。编码的核心方法如下:
java
//step2、对包含特殊字符的进行值重编码
address = URLEncoder.encode(address, "UTF-8");
其它没有特殊字符的值可以不做处理,再次运行后在控制台可以看到接口访问正常了。

四、高德地图与百度地图数字签名对比
这里简单对两个平台的数字签名进行一个简单的对比。
|------------|------|------|
| | 百度地图 | 高德地图 |
| 请求前缀是否带入计算 | 是 | 否 |
| 请求参数可否乱序 | 否 | 是 |
| 编码复杂度 | 高 | 中 |
| 是否包含示例 | 有 | 无 |
五、总结
以上就是本文的主要内容,文章主要讲解如何在高德地图中开启SIG数字签名,开启数字签名后可能会遇到什么问题,然后介绍使用Java进行调用时可能遇到的场景以及如何解决调用时返回"info":"INVALID_USER_SIGNATURE","infocode":"10007"的错误。通过详细讲解如何在Java中集成UniHttp,集成UniHttp之后可能会碰到什么问题,最后通过问题的解决积累开发经验,同时对百度地图和高德地图的数字签名方式进行了简单的对比,为大家实际开发时做一个简单的参考。行文仓促,定有许多的不足之处,欢迎各位朋友在评论区批评指正,不胜感激。