背景
在 Java 开发中,调用第三方接口时,请求参数的顺序对于某些签名认证机制至关重要。百度地图开放平台的 SN 签名认证要求参数按特定顺序排列。若使用 UniHttp 框架调用此类接口,由于框架内部处理可能导致参数顺序变化,从而引发 SN 校验失败(错误码 211)。本文探讨该问题的成因及三种解决方案。
问题场景
1. UniHttp 模式下 SN 接口定义
按照官方文档规范,定义接口如下:
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
public interface BaiduGeoSearchWithSnService {
/**
* - 百度行政区划区域检索接口
*/
@GetHttpInterface(url="https://api.map.baidu.com/place/v2/search")
public HttpResponse<String> getSearch(
@QueryPar("query") String query,
@QueryPar("region") String region,
@QueryPar("output") String output,
@QueryPar("scope") String scope,
@QueryPar("ret_coordtype") String ret_coordtype,
@QueryPar("page_size") int pageSize,
@QueryPar("page_num") int pageNum,
@QueryPar("ak") String ak,
@QueryPar("sn") String sn);
}
2. 第一次正式调用
测试类中使用 LinkedHashMap 保存参数以维持插入顺序,并计算 SN 签名:
package com.yelang.project.unihttp;
import java.io.UnsupportedEncodingException;
import java.util.LinkedHashMap;
import java.util.Map;
import org.junit.Test;
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.project.thridinterface.BaiduGeoSearchWithSnService;
import com.yelang.project.thridinterface.signature.BaiduSignature;
@SpringBootTest
@RunWith(SpringRunner.class)
public class BaiduGeoSearchWithSnServiceCase {
private static final String AK_VALUE = "yourak";
private static final String SK_VALUE = "yoursk";
@Autowired
private BaiduGeoSearchWithSnService bdGeoSearchWithSnService;
@Test
public void searchBySn() throws UnsupportedEncodingException {
Map<String, String> paramsMap = new LinkedHashMap<>();
String api_prefix = "/place/v2/search?";
String query = "湘菜";
String region = "158";
String output = "json";
String scope = "2";
String ret_coordtype = "WGS84";
int pageSize = 20;
int pageNum = 0;
paramsMap.put("query", query);
paramsMap.put("region", region);
paramsMap.put("output", output);
paramsMap.put("scope", scope);
paramsMap.put("ret_coordtype", ret_coordtype);
paramsMap.put("page_size", String.valueOf(pageSize));
paramsMap.put("page_num", String.valueOf(pageNum));
paramsMap.put("ak", AK_VALUE);
BaiduSignature signature = new BaiduSignature(AK_VALUE, SK_VALUE, paramsMap, api_prefix);
String sn = signature.getSnByMap();
HttpResponse<String> result = bdGeoSearchWithSnService.getSearch(query, region, output, scope, ret_coordtype, pageSize, pageNum, AK_VALUE, sn);
System.out.println(result.getBodyResult());
}
}
3. 遇到 211 APP SN 校验失败
运行后控制台返回 {"status":211,"message":"APP SN 校验失败"}。经 Debug 检查 uniHttpRequest 对象中的 QueryParam,发现参数顺序与定义不一致,导致服务端计算的 SN 值与客户端不一致。
问题分析
UniHttp 框架在处理请求时,默认可能未保持参数定义的顺序,或者在序列化过程中发生了重排。当服务端依赖严格的参数顺序进行签名验证时,这种乱序会导致验签失败。开源项目 UniHttp 的作者建议通过实现 HttpApiProcessor 并重写 postBeforeHttpRequest 方法来调整参数顺序。
解决方案
1. 加密顺序按实际请求参数求解
已知 UniHttp 发送时的参数顺序后,直接在客户端生成 SN 时按照该顺序构建 Map。例如将参数放入 LinkedHashMap 的顺序调整为与服务端预期一致。
@Test
public void searchBySnFixed() throws UnsupportedEncodingException {
Map<String, String> paramsMap = new LinkedHashMap<>();
String api_prefix = "/place/v2/search?";
// 按实际请求顺序来加密
String output = "json";
String query = "湘菜";
String scope = "2";
int pageNum = 0;
String region = "158";
String ret_coordtype = "WGS84";
int pageSize = 20;
paramsMap.put("output", output);
paramsMap.put("query", query);
paramsMap.put("scope", scope);
paramsMap.put("page_num", String.valueOf(pageNum));
paramsMap.put("ak", AK_VALUE);
paramsMap.put("region", region);
paramsMap.put("ret_coordtype", ret_coordtype);
paramsMap.put("page_size", String.valueOf(pageSize));
BaiduSignature signature (AK_VALUE, SK_VALUE, paramsMap, api_prefix);
signature.getSnByMap();
}
此方法简单但耦合度高,每次参数变动需手动调整。
2. 兼容 Get 请求参数动态调整的方法
通过自定义 Processor 重写 postBeforeHttpRequest 方法,在请求发送前手动拼接 URL 查询字符串。
@Override
public UniHttpRequest postBeforeHttpRequest(UniHttpRequest uniHttpRequest, HttpApiMethodInvocation<BaiduHttpApi> methodInvocation) {
Map<String, Object> queryParam = uniHttpRequest.getHttpUrl().getQueryParam();
// 重新排序或过滤参数
Map<String, Object> paramsMap = reorderingQueryParamMap(null, queryParam);
// 清空原有 Param,直接设置新 URL
uniHttpRequest.getHttpUrl().setQueryParam(null);
String queryString = buildQueryString(paramsMap);
String url = uniHttpRequest.getHttpUrl().getUrl() + uniHttpRequest.getHttpUrl().getPath() + "?" + queryString;
uniHttpRequest.getHttpUrl().setUrl(url);
uniHttpRequest.getHttpUrl().setPath("");
return uniHttpRequest;
}
protected Map<String, Object> reorderingQueryParamMap(String snMapParams, Map<String, Object> queryParam) {
Map<String, Object> paramsMap = new LinkedHashMap<>();
if (StringUtils.isNotEmpty(snMapParams)) {
String[] snMap = snMapParams.split(",");
for (String paramKey : snMap) {
paramsMap.put(paramKey, queryParam.get(paramKey));
}
} else {
paramsMap = queryParam;
}
paramsMap.put("ak", queryParam.get("ak"));
paramsMap.put("sn", queryParam.get("sn"));
return paramsMap;
}
注意:接口定义路径不要包含问号,否则会导致双问号报错。
3. 使用注解来设置重排序规则
结合自定义注解与 Processor,实现更灵活的重排序配置。
定义注解:
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@HttpApi(processor = BaiduHttpApiProcessor.class)
public @interface BaiduHttpApi {
String url() default "${unihttp.channel.baidu.url}";
String snMapParams() default "";
boolean snMode() default false;
}
使用注解:
@BaiduHttpApi(snMode = true, snMapParams = "query,region,output,scope,ret_coordtype,page_size,page_num")
public interface BaiduGeoSearchSnProcessorService {
}
在 Processor 中解析 snMapParams 并重组参数:
@Override
public UniHttpRequest postBeforeHttpRequest(UniHttpRequest uniHttpRequest, HttpApiMethodInvocation<BaiduHttpApi> methodInvocation) {
BaiduHttpApi apiAnnotation = methodInvocation.getProxyApiAnnotation();
String snMapParams = apiAnnotation.snMapParams();
boolean snMode = apiAnnotation.snMode();
if (snMode && StringUtils.isNotEmpty(snMapParams)) {
Map<String, Object> queryParam = uniHttpRequest.getHttpUrl().getQueryParam();
Map<String, Object> paramsMap = this.reorderingQueryParamMap(snMapParams, queryParam);
uniHttpRequest.getHttpUrl().setQueryParam(paramsMap);
}
return uniHttpRequest;
}
4. 三种方法的使用场景及对比
| 序号 | 处理方式 | 通用性 | 改造复杂度 | 缺点 |
|---|---|---|---|---|
| 1 | 客户端兼容 | 低 | 低 | 需要根据请求参数定制 |
| 2 | 兼容 Get 调参 | 中 | 中 | 只支持 Get 方法,URL 拼接需注意 |
| 3 | 重写 QueryParam | 高 | 高 | 代码稍微复杂一点,维护难度高 |
总结
针对 Java 调用 UniHttp 接口时因参数乱序导致的百度 SN 签名认证失败问题,可通过手动对齐加密顺序、重写 Processor 动态调整 URL、或结合注解配置重排序规则来解决。开发者应根据业务场景对通用性和维护成本的要求选择合适的方案。


