(1)参考资料:https://www.jianshu.com/p/d47da77b6419
(2)参考书籍:高级软件架构师教程(311-312)
主要步骤如下:
(1)将所有业务请求参数按字母先后顺序排序。
(2)参数名称和参数值链接成一个字符串A。
(3)在字符串A的首尾加上apiSecret接口密匙组成一个新字符串B。
(4)对字符串进行MD5散列运算得到API签名sign,然后再进行Base64编码。
假设请求的参数为:f=1,b=23,k=33,排序后为b=23,f=1,k=33,参数名和参数值链接后为b23f1k33,首尾加上appsecret后md5:
C = md5(secretkey1value1key2value2...secret);
Base64.encode(C );
以上签名方法安全有效地解决了参数被篡改和身份验证的问题,如果参数被篡改,没事,因为别人无法知道apiSecret,也就无法重新生成新的sign,从而保证接口调用是可靠的。
Java代码
/**
* 添加参数到HTTP请求里面
* @param method
* @param path
* @param params
* @param apiKey
* @param apiSecret
*/
void addRequiredParams(String method, String path, Map params, String apiKey, String apiSecret) {
params.put("key", apiKey); //标识
params.put("sigVer", "1"); //签名版本
String ts = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss:SSS").format(LocalDateTime.now());
params.put("ts", ts); //时间戳
params.put("nonce", RandomStringUtils.randomAlphanumeric(16));
String sig = getSig(method, path, apiSecret, params);
params.put("sig", sig); //API签名
}
/**
* API签名生成
*
* @param method
* @param path
* @param apiSecret
* @param params
* @return
*/
private String getSig(String method, String path, String apiSecret, Map params) {
StringBuilder stringA = new StringBuilder();
///1、将所有业务请求参数按字母先后顺序排序.
// 2、参数名称和参数值链接成一个字符串A
Set keySet = new TreeSet<>(params.keySet());
for (String key : keySet) {
String value = params.get(key);
if (value == null) {
continue;
}
System.out.println(key + ":" + params.get(key));
stringA.append(key);
stringA.append("=");
stringA.append(params.get(key));
stringA.append("&");
}
stringA.setLength(stringA.length() - 1); // trim the last "&"
System.out.println("参数名称和参数值链接成一个字符串A:" + stringA.toString());
logger.debug("参数名称和参数值链接成一个字符串A:" + stringA.toString());
String unifiedString = method.toUpperCase() + ":" + path + ":" + stringA.toString();
logger.debug("unified string: " + unifiedString);
String sig = null;
///3、在字符串A的首尾加上apiSecret组成一个新字符串B
StringBuilder stringB = new StringBuilder();
stringB.append(apiSecret).append(stringA).append(apiSecret);
try {
//4、对字符串进行MD5散列运算得到签名sign,然后再进行Base64编码
byte[] bytes = Base64.getEncoder().encode(MD5Utils.md5(stringB.toString()).getBytes("UTF-8"));
sig = new String(bytes, "UTF-8");
} catch (Exception e) {
logger.error("getSig error: " + e.getMessage());
}
return sig;
}
主要步骤如下:
1、得到请求方携带的API签名。
2、将所有业务请求参数按字母先后顺序排序。
3、参数名称和参数值链接成一个字符串A。
4、在字符串A的首尾加上apiSecret接口密匙组成一个新字符串B。
5、对新字符串B进行MD5散列运算生成服务器端的API签名,将客户端的API签名进行Base64解码,然后开始验证签名。
6、如果服务器端生成的API签名与客户端请求的API签名是一致的,则请求是可信的,否则就是不可信的。
java代码
/**
* 检查API签名是否合法
* (1)客户端请求里面会携带签名(客户端利用apiSecret和给定的算法产生签名)
* (2)服务器端会使用存在服务器端的apiSecret和相同的算法产生一个签名。
* (3)服务器端对这两个签名进行校验,得出签名的有效性。如果有效,则正常走业务流程,否则拒绝请求。
*
* @param request
* @param apiSecret
* @return
*/
public static boolean checkSig(HttpServletRequest request, String apiSecret) throws IOException{
//1、得到请求方携带的API签名
String clientSig = null;
StringBuilder stringA = new StringBuilder();
Enumeration parameterNames = request.getParameterNames();
Set keySet = new TreeSet<>();
//2、将所有业务请求参数按字母先后顺序排序。
while (parameterNames.hasMoreElements()) {
String parameterName = parameterNames.nextElement();
if (parameterName.equals("sig")) {
//获取客户端的API签名
clientSig = request.getParameter("sig");
continue;
}
keySet.add(parameterName);
}
//3、参数名称和参数值链接成一个字符串A。
for (String key : keySet) {
String value = request.getParameter(key);
if (value == null) {
continue;
}
stringA.append(key);
stringA.append("=");
stringA.append(value);
stringA.append("&");
}
stringA.setLength(stringA.length() - 1); // trim the last "&"
//服务器端根据参数生成的签名
String serverSig = null;
//Base64解码客户端的签名
clientSig = new String(Base64.getDecoder().decode(clientSig), "UTF-8");
//4、在字符串A的首尾加上apiSecret接口密匙组成一个新字符串B。
StringBuilder stringB = new StringBuilder();
stringB.append(apiSecret).append(stringA).append(apiSecret);
//5、对新字符串B进行MD5散列运算生成服务器端的API签名,将客户端的API签名进行Base64解码,然后开始验证签名。
//6、如果服务器端生成的API签名与客户端请求的API签名是一致的,则请求是可信的,否则就是不可信的。
serverSig = MD5Utils.md5(stringB.toString());
return clientSig != null && serverSig != null && clientSig.equals(serverSig);
}
3、使用过滤器统一对访问OpenApi的请求进行API签名验证
使用过滤器的好处:统一对请求进行预处理,即验证API签名。如果签名验证失败则拒绝请求,并返回API签名验证失败的消息给请求方。
过滤器代码如下:
/**
* 描述: 验证OpenAPI接口签名的过滤器
*
* @author chenlw
* @create 2018-11-19 11:21
*/
public class OpenApiRequestCheckFilter implements Filter {
@Autowired
private SystemConnectionConfigClient systemConnectionConfigClient;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
System.out.println("-----------------OpenApiRequestCheckFilter--------------------");
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (systemConnectionConfigClient == null) {
//解决@Autowired无法自动注入的问题
BeanFactory factory = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
systemConnectionConfigClient = (SystemConnectionConfigClient) factory.getBean("systemConnectionConfigClient");
}
String apiKey = request.getParameter("key"); //apiKey
if(apiKey == null){
///API签名验证失败,把请求转发到处理不合法请求的Controller
UnauthorizedForward(request,response);
return;
}
String apiSecret = systemConnectionConfigClient.queryDcToFrontApiSecretByApiKey(apiKey);
//数据中心请求前置的接口密匙
if (apiSecret != null) {
///对API接口签名进行验证
boolean sigCheckResult = OpenApiRequestCheckUtils.checkSig(request, apiSecret);
if (sigCheckResult) {
//API签名验证通过
chain.doFilter(request, response);
} else {
///API签名验证失败,把请求转发到处理不合法请求的Controller
UnauthorizedForward(request,response);
}
} else {
///API签名验证失败,把请求转发到处理不合法请求的Controller
UnauthorizedForward(request,response);
}
}
private void UnauthorizedForward(HttpServletRequest request,HttpServletResponse response) throws IOException,ServletException{
///查询不到接口配置,把请求转发到处理不合法请求的Controller
if ("GET".equals(request.getMethod())) {
request.getRequestDispatcher("/api/dataCenterRequest/handleUnauthorizedGetRequest").forward(request, response);
} else {
request.getRequestDispatcher("/api/dataCenterRequest/handleUnauthorizedPostRequest").forward(request, response);
}
}
@Override
public void destroy() {
}
}