基于公网的http协议的请求/响应时不安全的,存在被截获,篡改,重发的可能。
Restful API接口要求
-
防伪装攻击(如:公网,第三方有意或恶意的调用接口);
-
防篡改攻击(如:公网,请求头/查询字符串/内容,在传输过程中被篡改);
-
防重发攻击(如:公网,请求被截获,稍后被重发多次);
-
防数据信息泄露(如:截获用户请求,截获到账号,密码等);
基于HTTP协议的接口(通用数据交互接口)需要满足前三条要求(主要通过时间戳和签名),第四条采用HTTPS协议或传输内容使用非对称加密(用在用户登录,授权,解密接口)。
基于内网的API
通过签名,IP白名单搞定。
参考HTTPS方案
参考OAuth授权机制
基于HTTP方案
设计原则:
-
轻量级;
-
适合于异构系统(跨操作系统,多语言简易实现);
-
易于开发;
-
易于测试;
-
易于部署;
-
满足接口安全要求,无过度设计;
接口签名参考:
- _appid:调用方身份ID,接口提供方用此来识别不同的调用者;
- _sign:一次接口调用签名值(随机),服务器端“防止 伪装请求/防篡改/防重发”识别的主要依据;
- _timestamp:时间戳,允许容错+-10分钟,用于识别过期调用,同时用于签名的盐值,获取当前时间戳,需要调用统一时间戳接口;
签名算法过程:
-
对除签名外的所有请求参数按key做升序排列,value无需编码, (假设当前时间的时间戳是12345678) 例如:有c=3,b=2,a=1 三个参,另加上时间戳后, 按key排序后为:a=1,b=2,c=3,_timestamp=12345678。 ;
-
把参数名和参数值链接组成字符串, a1b2c3_timestamp12345678;
-
用申请得到的appkey链接到拼接字符串的头部和尾部,然后进行32位md5加密,最后得到md5加密摘要转换成大写, 示例:假设appkey=test,md5(testa1b2c3_timestamp12345678test),取得MD5摘要值 C5F3EB5D7DC2748AED89E90AF00081E6 。 ;
代码实现:
认证过程
- 用户发送用户名/密码到服务端,服务端通过DB验证用户名/密码,验证成功返回 约定算法加密(Hash(随机码[随机盐])+签名)到客户端;
- 客户端通过约定算法解密 加入随机盐,hash随机盐与服务端签名对比,通过则认证,返回客户端 hash(随机盐)+签名到服务端;
- 公钥 = 客户端/服务端约定的算法;私钥 = 通过对方提供的随机数可以解密与签名对比,得到签名值;
Java代码示例
/**
* Description
* 签名工具
*
* @author Mr. Chun.
*/
public class SignBuilder {
/**
* 生成签名结果
*
* @param sArray 要签名的数组
* @return 签名结果字符串
*/
public static String buildMysign(Map<String, String> sArray, String secret) {
String prestr = createLinkString(sArray); //把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
prestr = secret+ prestr + secret; //把拼接后的字符串再与安全校验码直接连接起来
String mysign;
try {
mysign = DigestUtils.md5Hex(prestr.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("MD5签名过程中出现错误,指定的编码集错误");
}
return mysign;
}
/**
* 除去数组中的空值和签名参数
*
* @param sArray 签名参数组
* @return 去掉空值与签名参数后的新签名参数组
*/
private static Map<String, String> paraFilter(Map<String, String> sArray) {
Map<String, String> result = new HashMap<String, String>();
if (sArray == null || sArray.size() <= 0) {
return result;
}
for (String key : sArray.keySet()) {
String value = sArray.get(key);
if (value == null || value.equals("") || key.equalsIgnoreCase("sign") || key.equalsIgnoreCase("mobileDevice") || key.equals("v")) {
continue;
}
result.put(key, value);
}
return result;
}
/**
* 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
*
* @param params 需要排序并参与字符拼接的参数组
* @return 拼接后字符串
*/
public static String createLinkString(Map<String, String> params) {
params = paraFilter(params);
List<String> keys = new ArrayList<String>(params.keySet());
Collections.sort(keys);
String prestr = "";
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = params.get(key);
// if (i == keys.size() - 1) {//拼接时,不包括最后一个&字符
// prestr = prestr + key + "=" + value;
// } else {
// prestr = prestr + key + "=" + value + "&";
// }
prestr = prestr + key + value;
}
return prestr;
}
/**
* 做map的转换
*
* @param parameterMap
* @return
*/
public static Map<String, String> convertRequestMap(Map<String, String[]> parameterMap) {
Map<String, String> alipayParameter = new HashMap<String, String>();
for (Iterator<String> iter = parameterMap.keySet().iterator(); iter.hasNext(); ) {
String name = (String) iter.next();
String[] values = parameterMap.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ",";
}
alipayParameter.put(name, valueStr);
}
return alipayParameter;
}
}
Go代码
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"sort"
"time"
)
// Signature used to generate signature with the appsecret/method/params/RequestURI
func Signature(appsecret, method string, params url.Values, RequestURL string) (result string) {
var b bytes.Buffer
keys := make([]string, len(params))
pa := make(map[string]string)
for k, v := range params {
pa[k] = v[0]
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
if key == "_signature" {
continue
}
val := pa[key]
if key != "" && val != "" {
b.WriteString(key)
b.WriteString(val)
}
}
stringToSign := fmt.Sprintf("%v\n%v\n%v\n", method, b.String(), RequestURL)
fmt.Println(stringToSign)
sha256 := sha256.New
hash := hmac.New(sha256, []byte(appsecret))
hash.Write([]byte(stringToSign))
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
}
func main() {
//http://xxx:8091/v2api/dv/filter?_appid=test&_signature=sdfdf&_timestamp=2017-11-09%2019%3A39%3A00&_index=app_service_homework_miniactivefinish_di&_type=app_service_homework_miniactivefinish_di&_size=10&_from=0
appsecret := "bigdatawmw"
method := "GET"
RequestURL := "/v2api/dv/filter"
params := make(url.Values)
params.Add("_appid", "bigdata")
params.Add("_timestamp", time.Now().Format("2006-01-02 15:04:05"))
params.Add("_index", "app_service_superbook_packorder_da")
params.Add("_type", "app_service_superbook_packorder_da")
params.Add("_size", "10")
params.Add("_from", "0")
fmt.Println("http://xxx:8091" + RequestURL + "?" + params.Encode() + "&_signature=" + Signature(appsecret, method, params, RequestURL))
//signature := "mFdpvLh48ca4mDVEItE9++AKKQ/IVca7O/ZyyB8hR58="
// fmt.Println(Signature(appsecret, method, params, RequestURL))
fmt.Println(time.Now().Format("2006-01-02 15:04:05"))
}