如何写出安全的API接口?接口参数加密签名设计思路
阅读原文时间:2021年04月20日阅读:1

开发中经常用到接口,尤其是在面向服务的soa架构中,数据交互全是用的接口。 

几年以前我认为,我写个接口,不向任何人告知我的接口地址,我的接口就是安全的,现在回想真是too young,too simple。但凡部署在广域网的应用程序,随随便便的好多工具可以根据ip或域名扫描应用程序的所有暴露的接口,进而分析参数,注入程序,分分钟被攻击。 

那咋才能保证接口的安全性呢?

(一)面临的主要安全问题

a.网络环境假设:

a1.假设公共网络(Internet,如:WIFI、非家庭网络、非办公网络等) 是不安全的,一切基于HTTP协议的请求/响应(Request or Response)都是可以被截获的、篡改、重放(重发)的。

b.接口安全要求:

b1.防伪装攻击(案例:在公共网络环境中,第三方 有意或恶意 的调用我们的接口)

b2.防篡改攻击(案例:在公共网络环境中,请求头/查询字符串/内容 在传输过程被修改)

b3.防重放攻击(案例:在公共网络环境中,请求被截获,稍后被重放或多次重放)

b4.防数据信息泄漏(案例:截获用户登录请求,截获到账号、密码等)

(二)可参考的商业标准

可参见: HTTP数据传输安全方案  HTTPS(HTTP安全商业标准)

http://baike.baidu.com/view/14121.htm

(三)可参考国内互联网厂商参考

新浪OpenAPI、腾讯、淘宝 等。

(四)设计原则

1.轻量级

2.适合于异构系统(跨操作系统、多语言简易实现)

3.易于开发

4.易于测试

5.易于部署

6.满足接口安全需求(满足b1 b2 b3要求),无过度设计。

其它:接口安全要求b4部分,主要针对目前用户中心的登录接口

设计原则是:使用HTTPS安全协议 或 传输内容使用非对称加密,目前我们采用的后者。

(五)适用范围

1.所有写操作接口(增、删、改 操作)

2.非公开的读接口(如:涉密/敏感/隐私 等信息)

(六)接口参数签名 实现思路参考

必要的输入参数

参数名

类型

必选

描述

_appid

string

调用方身份ID,接口提供方用此来识别调不同的调用者,该参数是API基本规范的一部分,请详见API公共规范。

_sign

string

一次接口调用的签名值,服务器端 “防止 伪装请求/防篡改/ 防重发” 识别的重要依据。

_timestamp

Int

时间戳(long Timestamp = DateTime.Now.Ticks;)

签名算法过程:

1.对除签名外的所有请求参数按key做的升序排列,value无需编码。
(假设当前时间的时间戳是12345678)

例如:有c=3,b=2,a=1 三个参,另加上时间戳后, 按key排序后为:a=1,b=2,c=3,_timestamp=12345678。

2 把参数名和参数值连接成字符串,得到拼装字符:a1b2c3_timestamp12345678

3 用申请到的appkey 连接到接拼装字符串头部和尾部,然后进行32位MD5加密,最后将到得MD5加密摘要转化成大写。

示例:假设appkey=test,md5(testa1b2c3_timestamp12345678test),取得MD5摘要值 C5F3EB5D7DC2748AED89E90AF00081E6 。

再看一个更具体的Sample Code:

如何得取如下请求的签名值:
http://api.demo.com/dog/add?1=壹&A=aaa&Z=zzz&_appid=club&_timestamp=12345678&a=AAA&z=ZZZ

C#实现代码如下 ( 请新建一个C#代码文件 SampleCode.cs ):

test()方法展示了如何取得该请的签名参数值 ( _sign=8B0E081689789CF66490E65BB8E1B0E7 ),现实业务中依据自己的情况,把创建请求的过程封装成公共方法,使得请求url的创建过程对开发人员透明,简化处理。

[下方会提供 .Net的 Sample Code]

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

.Net

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace test3
{
    public class SampleCode
    {
        public static string test()
        {
            int _timestamp = 12345678;
            var param = new SortedDictionary<string, string>(new AsciiComparer());
            param.Add("z", "ZZZ");
            param.Add("a", "AAA");
            param.Add("Z", "zzz");
            param.Add("A", "aaa");
            param.Add("2", "贰");
            param.Add("1", "壹");
            param.Add("_appid", "club");
            param.Add("_timestamp", _timestamp.ToString());
            string _sign = GetSign(param);
            string urlParam = string.Join("&", param.Select(i => i.Key + "=" + i.Value));
            string url = "http://api.demo.com/dog/add?" + urlParam + "&_sign=" + _sign;
            return url;
        }
        public static string GetSign(SortedDictionary<string, string> paramList, string appKey = "test")
        {
            paramList.Remove("_sign");
            StringBuilder sb = new StringBuilder(appKey);
            foreach (var p in paramList)
                sb.Append(p.Key).Append(p.Value);
            sb.Append(appKey);
            return GetMD5(sb.ToString());
        }
        public static string GetMD5(string str)
        {
            if (string.IsNullOrEmpty(str))
                return str;
            var sb = new StringBuilder(32);
            var md5 = System.Security.Cryptography.MD5.Create();
            var output = md5.ComputeHash(Encoding.UTF8.GetBytes(str));
            for (int i = 0; i < output.Length; i++)
                sb.Append(output[i].ToString("X").PadLeft(2, '0'));
            return sb.ToString();
        }
    }
    /// <summary>
    /// 基于ASCII码排序规则的String比较器
    /// Author:HeDaHong
    /// </summary>
    public class AsciiComparer : System.Collections.Generic.IComparer<string>
    {
        public int Compare(string a, string b)
        {
            if (a == b)
                return 0;
            else if (string.IsNullOrEmpty(a))
                return -1;
            else if (string.IsNullOrEmpty(b))
                return 1;
            if (a.Length <= b.Length)
            {
                for (int i = 0; i < a.Length; i++)
                {
                    if (a[i] < b[i])
                        return -1;
                    else if (a[i] > b[i])
                        return 1;
                    else
                        continue;
                }
                return a.Length == b.Length ? 0 : -1;
            }
            else
            {
                for (int i = 0; i < b.Length; i++)
                {
                    if (a[i] < b[i])
                        return -1;
                    else if (a[i] > b[i])
                        return 1;
                    else
                        continue;
                }
                return 1;
            }
        }
    }
}

总结:

  1. 接口调用方和接口提供方约定好统一的参数加密算法

  2. 接口调用方在调用时把加密后的_sign放在参数中去请求接口

  3. 接口提供方接到响应后,判断时间戳是不是在有效时间内(这个时间间隔根据你的安全范围可以是10分钟,5分钟,20秒等,过期失效,前提是需要保证接口提供方和调用方的服务器时间为准确的网络同步时间)

  4. 把参数中除了_sign以外的参数进行加密,然后把加密结果和传过来的_sign比较,相同则执行调用请求。

1.完全开放的接口

有没有这样的接口,谁都可以调用,谁都可以访问,不受时间空间限制,只要能连上互联网就能调用,毫无安全可言。

实话说,这样的接口我们天天都在接触,你查快递,你查天气预报,你查飞机,火车班次等,这些都是有公共的接口。

我把这称之为裸奔时代。代码如下:

///

/// 接口对外公开 ///
///
[HttpGet]
[Route("NoSecure")] public HttpResponseMessage NoSecure(int age)
{ var result = new ResultModel() { ReturnCode = 0, Message = string.Empty, Result = string.Empty }; var dataResult = stulist.Where(T => T.Age == age).ToList();
result.Result = dataResult; return GetHttpResponseMessage(result);
}

2.接口参数加密(基础加密)

你写个接口,你只想让特定的调用方使用,你把这些调用的人叫到一个小屋子,给他们宣布说我这里有个接口只打算给你们用,我给你们每人一把钥匙,你们用的时候拿着这把钥匙即可。

这把钥匙就是我上文说到的参数加密规则,有了这个规则就能调用。

这有安全问题啊,这里面的某个成员如果哪个不小心丢了钥匙或者被人窃取,掌握钥匙的人是不是也可以来掉用接口了呢?而且他可以复制很多钥匙给不明不白的人用。

相当于有人拿到了你的请求链接,如果业务没有对链接唯一性做判断(实际上业务逻辑通常不会把每次请求的加密签名记录下来,所以不会做唯一性判断),就会被重复调用,有一定安全漏洞,怎么破?先看这个场景的代码,然后继续往下看!

///

/// 接口加密 ///
///
[HttpGet]
[Route("SecureBySign")] public HttpResponseMessage SecureBySign([FromUri]int age, long _timestamp, string appKey, string _sign)
{ var result = new ResultModel<object>()
{
ReturnCode = 0,
Message = string.Empty,
Result = string.Empty
}; #region 校验签名是否合法
var param = new SortedDictionary<string, string>(new AsciiComparer());
param.Add("age", age.ToString());
param.Add("appKey", appKey);
param.Add("_timestamp", _timestamp.ToString()); string currentSign = SignHelper.GetSign(param, appKey); if (_sign != currentSign)
{
result.ReturnCode = -2;
result.Message = "签名不合法"; return GetHttpResponseMessage(result);
} #endregion

        var dataResult = stulist.Where(T => T.Age == age).ToList();
        result.Result \= dataResult; return GetHttpResponseMessage(result);
    }

3.接口参数加密+接口时效性验证(一般达到这个级别已经非常安全了)

继上一步,你发现有不明不白的人调用你的接口,你很不爽,随即把真正需要调用接口的人又叫来,告诉他们每天给他们换一把钥匙。和往常一样,有个别伙伴的钥匙被小偷偷走了,小偷煞费苦心,经过数天的踩点观察,准备在一个月黑风高的夜晚动手。拿出钥匙,捣鼓了半天也无法开启你的神圣之门,因为小偷不知道你天天都在换新钥匙。

小偷不服,经过一段时间琢磨,小偷发现了你们换钥匙的规律。在一次获得钥匙之后,不加思索,当天就动手了,因为他知道他手里的钥匙在第二天你更换钥匙后就失效了。

结果,小偷如愿。怎么破?先看这个场景的代码,然后继续往下看!

///

/// 接口加密并根据时间戳判断有效性 ///
///
[HttpGet]
[Route("SecureBySign/Expired")] public HttpResponseMessage SecureBySign_Expired([FromUri]int age, long _timestamp, string appKey, string _sign)
{ var result = new ResultModel<object>()
{
ReturnCode = 0,
Message = string.Empty,
Result = string.Empty
}; #region 判断请求是否过期---假设过期时间是20秒 DateTime requestTime = GetDateTimeByTicks(_timestamp); if (requestTime.AddSeconds(20) < DateTime.Now)
{
result.ReturnCode = -1;
result.Message = "接口过期"; return GetHttpResponseMessage(result);
} #endregion

        #region 校验签名是否合法
        var param = new SortedDictionary<string, string\>(new AsciiComparer());
        param.Add("age", age.ToString());
        param.Add("appKey", appKey);
        param.Add("\_timestamp", \_timestamp.ToString()); string currentSign = SignHelper.GetSign(param, appKey); if (\_sign != currentSign)
        {
            result.ReturnCode \= -2;
            result.Message \= "签名不合法"; return GetHttpResponseMessage(result);
        } #endregion

        var dataResult = stulist.Where(T => T.Age == age).ToList();
        result.Result \= dataResult; return GetHttpResponseMessage(result);
    }

4.接口参数加密+时效性验证+私钥(达到这个级别安全性固若金汤)

继上一步,你发现道高一尺魔高一丈,仍然有偷盗事情发生。咋办呢?你打算下血本,给每个人配一把钥匙的基础上,再给每个人发个暗号,即使钥匙被小偷弄去了,小偷没有暗号,任然无法如愿,而且这样很容易定位是谁的暗号泄漏问题,找到问题根源,只需要给当前这个人换下钥匙就行了,不用大动干戈。

但这个并不是万无一失的,因为钥匙毕竟还有可能被小偷搞到。代码如下:

///

/// 接口加密并根据时间戳判断有效性而且带着私有key校验 ///
///
[HttpGet]
[Route("SecureBySign/Expired/KeySecret")] public HttpResponseMessage SecureBySign_Expired_KeySecret([FromUri]int age, long _timestamp, string appKey, string _sign)
{ //key集合,这里随便弄两个测试数据 //如果调用方比较多,需要审核授权,根据一定的规则生成key把这些数据存放在数据库中,如果功能扩展开来,可以针对不同的调用方做不同的功能权限管理 //在调用接口时动态从库里取,每个调用方在调用时带上他的key,调用方一般把自己的key放到网站配置中
Dictionary<string, string> keySecretDic = new Dictionary<string, string>();
keySecretDic.Add("key_zhangsan", "D9U7YY5D7FF2748AED89E90HJ88881E6");//张三的key,
keySecretDic.Add("key_lisi", "I9O6ZZ3D7FF2748AED89E90ZB7732M9");//李四的key

        var result = new ResultModel<object\>()
        {
            ReturnCode \= 0,
            Message \= string.Empty,
            Result \= string.Empty
        }; #region 判断请求是否过期---假设过期时间是20秒 DateTime requestTime \= GetDateTimeByTicks(\_timestamp); if (requestTime.AddSeconds(20) < DateTime.Now)
        {
            result.ReturnCode \= -1;
            result.Message \= "接口过期"; return GetHttpResponseMessage(result);
        } #endregion

        #region 根据appkey获取key值
        string secret = keySecretDic.Where(T => T.Key == appKey).FirstOrDefault().Value; #endregion

        #region 校验签名是否合法
        var param = new SortedDictionary<string, string\>(new AsciiComparer());
        param.Add("age", age.ToString());
        param.Add("appKey", appKey);

        param.Add("appSecret", secret);//把secret加入进行加密

param.Add("_timestamp", _timestamp.ToString()); string currentSign = SignHelper.GetSign(param, appKey); if (_sign != currentSign)
{
result.ReturnCode = -2;
result.Message = "签名不合法"; return GetHttpResponseMessage(result);
} #endregion

        var dataResult = stulist.Where(T => T.Age == age).ToList();
        result.Result \= dataResult; return GetHttpResponseMessage(result);
    }

5.接口参数加密+时效性验证+私钥+Https(我把这个级别称之为金钟罩,世间最安全莫过于此)

继上一步,我们给传输机制改为Https,这下小偷彻底懵逼了。那么问题来了,Https咋玩儿呢?可以在本地搭个环境,参考此文:http://www.cnblogs.com/naniannayue/archive/2012/11/19/2776948.html

另:本文的接口是用的MVC WebAPI写的,完全基于RESTful标准。如对此不是特别了解可以参考此文:http://www.cnblogs.com/landeanfen/p/5501490.html

完整demo下载

注:demo不能直接运行,需要把两个web项目配置到iis中,api代表接口提供方,他的主域需要配置到business的webconfig中,在浏览器地址栏分别请求business中的各个调用接口方法来实现接口调用。

1、如果想验证参数错误,需要在请求接口时打个断点把接口url取出,篡改url参数,然后在浏览器中模拟请求

2、如果想验证接口超时,需要在请求接口时打个断点把接口url取出,然后等到了超时时间,然后在浏览器中模拟请求

3、如果想验证私钥错误,需要在请求接口时打个断点把接口url取出,然后修改business的私钥配置,然后在浏览器中模拟请求

1.完全开放的接口

有没有这样的接口,谁都可以调用,谁都可以访问,不受时间空间限制,只要能连上互联网就能调用,毫无安全可言。

实话说,这样的接口我们天天都在接触,你查快递,你查天气预报,你查飞机,火车班次等,这些都是有公共的接口。

我把这称之为裸奔时代。代码如下:

///

/// 接口对外公开 ///
///
[HttpGet]
[Route("NoSecure")] public HttpResponseMessage NoSecure(int age)
{ var result = new ResultModel() { ReturnCode = 0, Message = string.Empty, Result = string.Empty }; var dataResult = stulist.Where(T => T.Age == age).ToList();
result.Result = dataResult; return GetHttpResponseMessage(result);
}

2.接口参数加密(基础加密)

你写个接口,你只想让特定的调用方使用,你把这些调用的人叫到一个小屋子,给他们宣布说我这里有个接口只打算给你们用,我给你们每人一把钥匙,你们用的时候拿着这把钥匙即可。

这把钥匙就是我上文说到的参数加密规则,有了这个规则就能调用。

这有安全问题啊,这里面的某个成员如果哪个不小心丢了钥匙或者被人窃取,掌握钥匙的人是不是也可以来掉用接口了呢?而且他可以复制很多钥匙给不明不白的人用。

相当于有人拿到了你的请求链接,如果业务没有对链接唯一性做判断(实际上业务逻辑通常不会把每次请求的加密签名记录下来,所以不会做唯一性判断),就会被重复调用,有一定安全漏洞,怎么破?先看这个场景的代码,然后继续往下看!

///

/// 接口加密 ///
///
[HttpGet]
[Route("SecureBySign")] public HttpResponseMessage SecureBySign([FromUri]int age, long _timestamp, string appKey, string _sign)
{ var result = new ResultModel<object>()
{
ReturnCode = 0,
Message = string.Empty,
Result = string.Empty
}; #region 校验签名是否合法
var param = new SortedDictionary<string, string>(new AsciiComparer());
param.Add("age", age.ToString());
param.Add("appKey", appKey);
param.Add("_timestamp", _timestamp.ToString()); string currentSign = SignHelper.GetSign(param, appKey); if (_sign != currentSign)
{
result.ReturnCode = -2;
result.Message = "签名不合法"; return GetHttpResponseMessage(result);
} #endregion

        var dataResult = stulist.Where(T => T.Age == age).ToList();
        result.Result \= dataResult; return GetHttpResponseMessage(result);
    }

3.接口参数加密+接口时效性验证(一般达到这个级别已经非常安全了)

继上一步,你发现有不明不白的人调用你的接口,你很不爽,随即把真正需要调用接口的人又叫来,告诉他们每天给他们换一把钥匙。和往常一样,有个别伙伴的钥匙被小偷偷走了,小偷煞费苦心,经过数天的踩点观察,准备在一个月黑风高的夜晚动手。拿出钥匙,捣鼓了半天也无法开启你的神圣之门,因为小偷不知道你天天都在换新钥匙。

小偷不服,经过一段时间琢磨,小偷发现了你们换钥匙的规律。在一次获得钥匙之后,不加思索,当天就动手了,因为他知道他手里的钥匙在第二天你更换钥匙后就失效了。

结果,小偷如愿。怎么破?先看这个场景的代码,然后继续往下看!

///

/// 接口加密并根据时间戳判断有效性 ///
///
[HttpGet]
[Route("SecureBySign/Expired")] public HttpResponseMessage SecureBySign_Expired([FromUri]int age, long _timestamp, string appKey, string _sign)
{ var result = new ResultModel<object>()
{
ReturnCode = 0,
Message = string.Empty,
Result = string.Empty
}; #region 判断请求是否过期---假设过期时间是20秒 DateTime requestTime = GetDateTimeByTicks(_timestamp); if (requestTime.AddSeconds(20) < DateTime.Now)
{
result.ReturnCode = -1;
result.Message = "接口过期"; return GetHttpResponseMessage(result);
} #endregion

        #region 校验签名是否合法
        var param = new SortedDictionary<string, string\>(new AsciiComparer());
        param.Add("age", age.ToString());
        param.Add("appKey", appKey);
        param.Add("\_timestamp", \_timestamp.ToString()); string currentSign = SignHelper.GetSign(param, appKey); if (\_sign != currentSign)
        {
            result.ReturnCode \= -2;
            result.Message \= "签名不合法"; return GetHttpResponseMessage(result);
        } #endregion

        var dataResult = stulist.Where(T => T.Age == age).ToList();
        result.Result \= dataResult; return GetHttpResponseMessage(result);
    }

4.接口参数加密+时效性验证+私钥(达到这个级别安全性固若金汤)

继上一步,你发现道高一尺魔高一丈,仍然有偷盗事情发生。咋办呢?你打算下血本,给每个人配一把钥匙的基础上,再给每个人发个暗号,即使钥匙被小偷弄去了,小偷没有暗号,任然无法如愿,而且这样很容易定位是谁的暗号泄漏问题,找到问题根源,只需要给当前这个人换下钥匙就行了,不用大动干戈。

但这个并不是万无一失的,因为钥匙毕竟还有可能被小偷搞到。代码如下:

///

/// 接口加密并根据时间戳判断有效性而且带着私有key校验 ///
///
[HttpGet]
[Route("SecureBySign/Expired/KeySecret")] public HttpResponseMessage SecureBySign_Expired_KeySecret([FromUri]int age, long _timestamp, string appKey, string _sign)
{ //key集合,这里随便弄两个测试数据 //如果调用方比较多,需要审核授权,根据一定的规则生成key把这些数据存放在数据库中,如果功能扩展开来,可以针对不同的调用方做不同的功能权限管理 //在调用接口时动态从库里取,每个调用方在调用时带上他的key,调用方一般把自己的key放到网站配置中
Dictionary<string, string> keySecretDic = new Dictionary<string, string>();
keySecretDic.Add("key_zhangsan", "D9U7YY5D7FF2748AED89E90HJ88881E6");//张三的key,
keySecretDic.Add("key_lisi", "I9O6ZZ3D7FF2748AED89E90ZB7732M9");//李四的key

        var result = new ResultModel<object\>()
        {
            ReturnCode \= 0,
            Message \= string.Empty,
            Result \= string.Empty
        }; #region 判断请求是否过期---假设过期时间是20秒 DateTime requestTime \= GetDateTimeByTicks(\_timestamp); if (requestTime.AddSeconds(20) < DateTime.Now)
        {
            result.ReturnCode \= -1;
            result.Message \= "接口过期"; return GetHttpResponseMessage(result);
        } #endregion

        #region 根据appkey获取key值
        string secret = keySecretDic.Where(T => T.Key == appKey).FirstOrDefault().Value; #endregion

        #region 校验签名是否合法
        var param = new SortedDictionary<string, string\>(new AsciiComparer());
        param.Add("age", age.ToString());
        param.Add("appKey", appKey);

        param.Add("appSecret", secret);//把secret加入进行加密

param.Add("_timestamp", _timestamp.ToString()); string currentSign = SignHelper.GetSign(param, appKey); if (_sign != currentSign)
{
result.ReturnCode = -2;
result.Message = "签名不合法"; return GetHttpResponseMessage(result);
} #endregion

        var dataResult = stulist.Where(T => T.Age == age).ToList();
        result.Result \= dataResult; return GetHttpResponseMessage(result);
    }

5.接口参数加密+时效性验证+私钥+Https(我把这个级别称之为金钟罩,世间最安全莫过于此)

继上一步,我们给传输机制改为Https,这下小偷彻底懵逼了。那么问题来了,Https咋玩儿呢?可以在本地搭个环境,参考此文:http://www.cnblogs.com/naniannayue/archive/2012/11/19/2776948.html

另:本文的接口是用的MVC WebAPI写的,完全基于RESTful标准。如对此不是特别了解可以参考此文:http://www.cnblogs.com/landeanfen/p/5501490.html

**完整demo下载
**

注:demo不能直接运行,需要把两个web项目配置到iis中,api代表接口提供方,他的主域需要配置到business的webconfig中,在浏览器地址栏分别请求business中的各个调用接口方法来实现接口调用。

1、如果想验证参数错误,需要在请求接口时打个断点把接口url取出,篡改url参数,然后在浏览器中模拟请求

2、如果想验证接口超时,需要在请求接口时打个断点把接口url取出,然后等到了超时时间,然后在浏览器中模拟请求

3、如果想验证私钥错误,需要在请求接口时打个断点把接口url取出,然后修改business的私钥配置,然后在浏览器中模拟请求