一)需求背景
如今app客户端请求后台服务是很是经常使用的请求方式,在咱们写开放api接口时如何保证数据的安全,
咱们先看看有哪些安全性的问题
请求来源(身份)是否合法?
请求参数被篡改?
请求的惟一性(不可复制)
二)为了保证数据在通讯时的安全性,咱们能够采用参数签名的方式来进行相关验证
案例:
咱们经过给某 [移动端(app)] 写 [后台接口(api)] 的案例进行分析:
客户端: 如下简称app
后台接口:如下简称api
咱们经过app查询产品列表这个操做来进行分析:
app中点击查询按钮==》调用api进行查询==》返回查询结果==>显示在app中
1、不进行验证的方式
api查询接口:/getproducts?参数
app调用:http://api.chinasoft.com/getproducts?参数1=value1.......
如上,这种方式简单粗暴,经过调用getproducts方法便可获取产品列表信息了,可是 这样的方式会存在很严重的安全性问题,
没有进行任何的验证,你们均可以经过这个方法获取到产品列表,致使产品信息泄露。
那么,如何验证调用者身份呢?如何防止参数被篡改呢?
2、MD5参数签名的方式
咱们对api查询产品接口进行优化:
1.给app客户端分配对应的key=一、secret秘钥
2.Sign签名,调用API 时须要对请求参数进行签名验证,签名方式以下:
a. 按照请求参数名称将全部请求参数按照字母前后顺序排序获得:keyvaluekeyvalue...keyvalue
字符串如:将arong=1,mrong=2,crong=3 排序为:arong=1, crong=3,mrong=2 而后将参数名和参数值进行拼接
获得参数字符串:arong1crong3mrong2。
b. 将secret加在参数字符串的头部后进行MD5加密 ,加密后的字符串需大写。即获得签名Sign
新api接口代码:
app调用:http://api.chinasoft.com/getproducts?key=app_key&sign=BCC7C71CF93F9CDBDB88671B701D8A35&参数1=value1&参数2=value2.......
注:secret 仅做加密使用, 为了保证数据安全请不要在请求参数中使用。
如上,优化后的请求多了key和sign参数,这样请求的时候就须要合法的key和正确签名sign才能够获取产品数据。
这样就解决了身份验证和防止参数篡改问题,若是请求参数被人拿走,没事,他们永远也拿不到secret,由于secret是不传递的。
再也没法伪造合法的请求。
http://api.chinasoft.com/getproducts?a=1&c=world&b=hello
http://api.chinasoft.com/getproducts?a=1&c=world&b=hello&key=1&sign=BCC7C71CF93F9CDBDB88671B701D8A35
客户端的算法 要和 咱们服务器端的算法是一致的
"a=1&b=hello&c=world&key=1"
和秘钥进行拼接
secret=123456
"a=1&b=hello&c=world&123456" =》md5 加密 ===》字符串sign = BCC7C71CF93F9CDBDB88671B701D8A35
-----------------------------------
http://api.chinasoft.com/getproducts?a=1&c=world&b=hello&key=2&sign=BCC7C71CF93F9CDBDB88671B701D8A35
key去判断 是否客户端身份是合法
参数是否被篡改 服务器这边 也去生成一个sign签名,算法和客户端一致
a=2&c=world&b=hello ==》"a=2&b=hello&c=world" ==》secret=123456==》 "a=2&b=hello&c=world&123456" ==》md5
===》服务器生成的sign ===》若是和客户端传过来的sign一致,就表明合法===》验证参数是否被篡改
3、不可复制
第二种方案就够了吗?咱们会发现,若是我获取了你完整的连接,一直使用你的key和sign和同样的参数不就能够正常获取数据了,是的,仅仅是如上的优化是不够的
请求的惟一性:
为了防止别人重复使用请求参数问题,咱们须要保证请求的惟一性,就是对应请求只能使用一次,这样就算别人拿走了请求的完整连接也是无效的
惟一性的实现:在如上的请求参数中,咱们加入时间戳 timestamp(yyyyMMddHHmmss),一样,时间戳做为请求参数之一,
也加入sign算法中进行加密。
新的api接口:
app调用:
http://api.chinasoft.com/getproducts?key=app_key&sign=BCC7C71CF93F9CDBDB88671B701D8A35×tamp=201803261407&参数1=value1&参数2=value2.......
http://api.chinasoft.com/getproducts?a=1&c=world&b=hello
http://api.chinasoft.com/getproducts?a=1&c=world&b=hello&key=1&sign=BCC7C71CF93F9CDBDB88671B701D8A35&time=20190827
time是客户端发起请求的那一时刻,传过来的
客户端的算法 要和 咱们服务器端的算法是一致的
"a=1&b=hello&c=world&time=20190827"
和秘钥进行拼接
secret=123456
"a=1&b=hello&c=world&time=20190827&123456" =》md5 加密 ===》字符串sign= BCC7C71CF93F9CDBDB88671B701D8A35
---------------------------------
key=1 是否身份验证合法
time=客户端在调用这个接口那一刻传的时间
服务器去处理这个接口请求的当前时间 相减,若是这个大于10s;这个连接应该是被人家截取
若是小于10s,表示正常请求
如上,咱们经过timestamp时间戳用来验证请求是否过时。这样就算被人拿走完整的请求连接也是无效的。
Sign签名安全性分析:
经过上面的案例,咱们能够看出,安全的关键在于参与签名的secret,整个过程当中secret是不参与通讯的,
因此只要保证secret不泄露,请求就不会被伪造。
总结
上述的Sign签名的方式可以在必定程度上防止信息被篡改和伪造,保障通讯的安全,这里使用的是MD5进行加密,
固然实际使用中你们能够根据实际需求进行自定义签名算法,好比:RSA,SHA等。
-----------------------------------------
编辑nginx.conf的server部分
location /sign {
access_by_lua_file /usr/local/lua/access_by_sign.lua;
echo "sign验证成功";
}
==============================编辑/usr/local/lua/access_by_sign.luajava
--判断table是否为空 local function isTableEmpty(t) return t == nil or next(t) == nil end --两个table合并 local function union(table1,table2) for k,v in pairs(table2) do table1[k] = v end return table1 end --检验请求的sign签名是否正确 --params:传入的参数值组成的table --secret:项目secret,根据key找到secret local function signcheck(params,secret) --判断参数是否为空,为空报异常 if isTableEmpty(params) then local mess = "参数为空" ngx.log(ngx.ERR, mess) return false,mess end if secret == nil then local mess="私钥为空" ngx.log(ngx.ERR, mess) return false,mess end --平台分配给某客户端类型的keyID local key = params["key"]; if key == nil then local mess = "key值为空" ngx.log(ngx.ERR, mess) return false,mess end --判断是否有签名参数 local sign = params["sign"] if sign == nil then local mess="签名参数为空" ngx.log(ngx.ERR, mess) return false,mess end --是否存在时间戳的参数 local timestamp = params["time"] if timestamp == nil then local mess="时间戳参数为空" ngx.log(ngx.ERR, mess) return false,mess end --时间戳有没有过时,10秒过时 local now_mill = ngx.now() * 1000--毫秒级 if now_mill - timestamp > 30000 then local mess="连接过时" ngx.log(ngx.ERR, mess) return false,mess end local keys, tmp = {}, {} --提出全部的键名并按字符顺序排序 for k, _ in pairs(params) do if k ~= "sign" then keys[#keys+1] = k end end table.sort(keys) --根据排序好的键名依次读取值并拼接字符串成key=value&key=value for _,k in pairs(keys) do if type(params[k]) == "string" or type(params[k]) == "number" then tmp[#tmp+1] = k .. "=" .. tostring(params[k]) end end --将salt添加到最后,计算正确的签名sign值并与传入的sign签名对比, local signchar = table.concat(tmp, "&") .. "&" ..secret local rightsign = ngx.md5(signchar) if sign ~= rightsign then --若是签名错误返回错误信息并记录日志, --local mess="sign error: sign,"..sign.."right sign:"..rightsign.." sign_char:"..signchar local mess="sign error: sign,"..sign.."right sign:"..rightsign.." sign_char:"..table.concat(tmp, "&") ngx.log(ngx.ERR, mess) return false,mess end return true end local params = {} local get_args = ngx.req.get_uri_args(); ngx.req.read_body() local post_args = ngx.req.get_post_args(); union(params,get_args) union(params,post_args) --根据keyID到后台服务获取secret local secret = "abc123" local checkResult,mess = signcheck(params,secret) if not checkResult then ngx.say(mess); return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403 end
java代码,模仿请求python
import java.io.IOException; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SignApplication { public static void main(String[] args) throws IOException { SpringApplication.run(SignApplication.class, args); HashMap<String,String> params = new HashMap<String,String>(); params.put("key", "1"); params.put("a", "1"); params.put("c", "w"); params.put("b", "2"); long time = new Date().getTime(); params.put("time", "" + time); System.out.println(time); String sign = getSignature(params,"123456"); System.out.println(sign); params.put("sign", sign); String resp = HttpUtil.doGet("http://10.11.0.215/sign",params); System.out.println(resp); } /** * 签名生成算法 * @param HashMap<String,String> params 请求参数集,全部参数必须已转换为字符串类型 * @param String secret 签名密钥 * @return 签名 * @throws IOException */ public static String getSignature(HashMap<String,String> params, String secret) throws IOException { // 先将参数以其参数名的字典序升序进行排序 Map<String, String> sortedParams = new TreeMap<String, String>(params); Set<Entry<String, String>> entrys = sortedParams.entrySet(); // 遍历排序后的字典,将全部参数按"key=value"格式拼接在一块儿 StringBuilder basestring = new StringBuilder(); for (Entry<String, String> param : entrys) { if(basestring.length() != 0){ basestring.append("&"); } basestring.append(param.getKey()).append("=").append(param.getValue()); } basestring.append("&"); basestring.append(secret); System.out.println("basestring="+basestring); // 使用MD5对待签名串求签 byte[] bytes = null; try { MessageDigest md5 = MessageDigest.getInstance("MD5"); bytes = md5.digest(basestring.toString().getBytes("UTF-8")); } catch (GeneralSecurityException ex) { throw new IOException(ex); } String strSign = new String(bytes); System.out.println("strSign="+strSign); // 将MD5输出的二进制结果转换为小写的十六进制 StringBuilder sign = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(bytes[i] & 0xFF); if (hex.length() == 1) { sign.append("0"); } sign.append(hex); } return sign.toString(); } }
python代码模仿请求nginx
#coding=utf-8 import time import requests # 生成签名的字符串 def getSignature(params, secret): # basestring=a=1&b=hello&c=world&key=1&time=1566877802288 ivlist = [] # 拼凑字符串 for i,v in params.items(): tmpstr=str(i)+"="+str(v) ivlist.append(tmpstr) ivlist.append(secret) basestr = "&".join(ivlist) print("basestr = %s" % basestr) # 因为MD5模块在python3中被移除 # 在python3中使用hashlib模块进行md5操做 import hashlib # 建立md5对象 m = hashlib.md5() # 此处必须encode,若写法为m.update(str) 报错为: Unicode-objects must be encoded before hashing # 由于python3里默认的str是unicode # 或者 b = bytes(str, encoding='utf-8'),做用相同,都是encode为bytes b = basestr.encode(encoding='utf-8') m.update(b) str_md5 = m.hexdigest() return str_md5 if __name__ == "__main__": # 拼凑访问url params = {"a":22,"b":"hello","c":"wrold","key":1} time = int(round(time.time() * 1000)) params["time"] = time sinstr = getSignature(params, "abc123") print(sinstr) params["sign"] = sinstr url = "http://10.11.0.215/sign?a=1&b=hello&c=world&key=1&time={time}&sign={sign}".format(time = time, sign = sinstr) print("url = %s" % url) # 模拟正确的请求 res = requests.get("http://10.11.0.215/sign", params = params, timeout=10) res.encoding="utf-8" print(res.content)