一)需求背景
现在app客户端请求后台服务是非常常用的请求方式,在我们写开放api接口时如何保证数据的安全,
我们先看看有哪些安全性的问题

请求来源(身份)是否合法?
请求参数被篡改?
请求的唯一性(不可复制)

二)为了保证数据在通信时的安全性,我们可以采用参数签名的方式来进行相关验证
案例:
我们通过给某 [移动端(app)] 写 [后台接口(api)] 的案例进行分析:     
客户端: 以下简称app
后台接口:以下简称api

我们通过app查询产品列表这个操作来进行分析:
app中点击查询按钮==》调用api进行查询==》返回查询结果==>显示在app中

一、不进行验证的方式
api查询接口:/getproducts?参数
app调用:http://api.chinasoft.com/getproducts?参数1=value1.......
如上,这种方式简单粗暴,通过调用getproducts方法即可获取产品列表信息了,但是 这样的方式会存在很严重的安全性问题,
没有进行任何的验证,大家都可以通过这个方法获取到产品列表,导致产品信息泄露。
那么,如何验证调用者身份呢?如何防止参数被篡改呢?

二、MD5参数签名的方式
我们对api查询产品接口进行优化:
1.给app客户端分配对应的key=1、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一致,就代表合法===》验证参数是否被篡改

三、不可复制

第二种方案就够了吗?我们会发现,如果我获取了你完整的链接,一直使用你的key和sign和一样的参数不就可以正常获取数据了,是的,仅仅是如上的优化是不够的

请求的唯一性:
为了防止别人重复使用请求参数问题,我们需要保证请求的唯一性,就是对应请求只能使用一次,这样就算别人拿走了请求的完整链接也是无效的

唯一性的实现:在如上的请求参数中,我们加入时间戳 timestamp(yyyyMMddHHmmss),同样,时间戳作为请求参数之一,
也加入sign算法中进行加密。

新的api接口:
app调用:
http://api.chinasoft.com/getproducts?key=app_key&sign=BCC7C71CF93F9CDBDB88671B701D8A35&timestamp=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.lua

--判断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) dotable1[k] = vendreturn table1end--检验请求的sign签名是否正确
--params:传入的参数值组成的table
--secret:项目secret,根据key找到secret
local function signcheck(params,secret)--判断参数是否为空,为空报异常if isTableEmpty(params) thenlocal mess = "参数为空"ngx.log(ngx.ERR, mess)return false,messendif secret == nil thenlocal mess="私钥为空"ngx.log(ngx.ERR, mess)return false,messend--平台分配给某客户端类型的keyIDlocal key = params["key"];if key == nil thenlocal mess = "key值为空"ngx.log(ngx.ERR, mess)return false,messend--判断是否有签名参数local sign = params["sign"]if sign == nil thenlocal mess="签名参数为空"ngx.log(ngx.ERR, mess)return false,messend--是否存在时间戳的参数local timestamp = params["time"]if timestamp == nil thenlocal mess="时间戳参数为空"ngx.log(ngx.ERR, mess)return false,messend--时间戳有没有过期,10秒过期local now_mill = ngx.now() * 1000--毫秒级if now_mill - timestamp > 30000 thenlocal mess="链接过期"ngx.log(ngx.ERR, mess)return false,messendlocal keys, tmp = {}, {}--提出所有的键名并按字符顺序排序for k, _ in pairs(params) doif k ~= "sign" thenkeys[#keys+1] = kendendtable.sort(keys)--根据排序好的键名依次读取值并拼接字符串成key=value&key=valuefor _,k in pairs(keys) doif type(params[k]) == "string" or type(params[k]) == "number" thentmp[#tmp+1] = k .. "=" .. tostring(params[k])endend--将salt添加到最后,计算正确的签名sign值并与传入的sign签名对比,local signchar = table.concat(tmp, "&") .. "&" ..secretlocal rightsign = ngx.md5(signchar)if sign ~= rightsign then--如果签名错误返回错误信息并记录日志,--local mess="sign error: sign,"..sign.."right sign:"..rightsign.." sign_char:"..signcharlocal mess="sign error: sign,"..sign.."right sign:"..rightsign.." sign_char:"..table.concat(tmp, "&")ngx.log(ngx.ERR, mess)return false,messendreturn true
endlocal 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 thenngx.say(mess);return ngx.exit(ngx.HTTP_FORBIDDEN)    --直接返回403
end

java代码,模仿请求

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代码模仿请求

#coding=utf-8
import time
import requests# 生成签名的字符串
def getSignature(params, secret):# basestring=a=1&b=hello&c=world&key=1&time=1566877802288ivlist = []# 拼凑字符串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为bytesb = basestr.encode(encoding='utf-8')m.update(b)str_md5 = m.hexdigest()return str_md5if __name__ == "__main__":# 拼凑访问urlparams = {"a":22,"b":"hello","c":"wrold","key":1}time = int(round(time.time() * 1000))params["time"] = timesinstr = getSignature(params, "abc123")print(sinstr)params["sign"] = sinstrurl = "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)

转载于:https://www.cnblogs.com/reblue520/p/11418589.html

openresty开发系列39--nginx+lua实现接口签名安全认证相关推荐

  1. openresty开发系列38--通过Lua+Redis 实现动态封禁IP

    openresty开发系列38--通过Lua+Redis 实现动态封禁IP 一)需求背景为了封禁某些爬虫或者恶意用户对服务器的请求,我们需要建立一个动态的 IP 黑名单.对于黑名单之内的 IP ,拒绝 ...

  2. openresty开发系列24--openresty中lua的引入及使用

    openresty开发系列24--openresty中lua的引入及使用 openresty 引入 lua 一)openresty中nginx引入lua方式 1)xxx_by_lua   ---> ...

  3. openresty开发系列40--nginx+lua实现获取客户端ip所在的国家信息

    openresty开发系列40--nginx+lua实现获取客户端ip所在的国家信息 为了实现业务系统针对不同地区IP访问,展示包含不同地区信息的业务交互界面.很多情况下系统需要根据用户访问的IP信息 ...

  4. openresty开发系列33--openresty执行流程之2重写赋值阶段

    openresty开发系列33--openresty执行流程之2重写赋值阶段 一)重写赋值阶段 1)set_by_lua 语法:set_by_lua $res <lua-script-str&g ...

  5. openresty开发系列20--lua的时间操作

    openresty开发系列20--lua的时间操作 在 Lua 中,函数 time.date 和 difftime 提供了所有的日期和时间功能. 在 OpenResty 的世界里,不推荐使用这里的标准 ...

  6. openresty开发系列37--nginx-lua-redis实现访问频率控制

    openresty开发系列37--nginx-lua-redis实现访问频率控制 一)需求背景 在高并发场景下为了防止某个访问ip访问的频率过高,有时候会需要控制用户的访问频次 在openresty中 ...

  7. openresty开发系列36--openresty执行流程之6日志模块处理阶段

    openresty开发系列36--openresty执行流程之6日志模块处理阶段 一)header_filter_by_lua 语法:header_filter_by_lua <lua-scri ...

  8. openresty开发系列35--openresty执行流程之5内容content阶段

    openresty开发系列35--openresty执行流程之5内容content阶段 content 阶段 ---init阶段---重写赋值---重写rewrite---access content ...

  9. openresty开发系列34--openresty执行流程之4访问阶段

    openresty开发系列34--openresty执行流程之4访问阶段 访问阶段 用途:访问权限限制 返回403 nginx:allow 允许,deny 禁止 allow ip: deny ip: ...

最新文章

  1. 直播源码:直播+内容营销新趋势
  2. RecyclerView的优化:RecycledViewPool
  3. 阅读JavaScript高级程序设计(第二版)笔记
  4. 偏差/方差、经验风险最小化、联合界、一致收敛
  5. 怎么才能钓到产品经理妹子?|PMCAFF
  6. java 入门 博客园_javaweb入门
  7. OpenGL ES GLKit初探
  8. bootstrap精简教程
  9. K-Means ++ 算法
  10. 常用而又不为大多数人所知的三种网页特效
  11. Netty 从源码的角度深入剖析 ByteBuffer
  12. Maven学习总结(10)——使用Maven编译项目gbk的不可映射问题
  13. 小程序入门学习10--云开发03
  14. HTTPS为什么更安全,先看这些 , 网络加密 , 加密解密
  15. JavaScript备忘录
  16. python字典题_Python字典练习题
  17. Pycharm删除键、复制粘贴键失灵解决方案
  18. 计算机蓝屏幕亮度,电脑屏幕亮度怎么调,电脑屏幕亮度调节方法
  19. 11.2 RAC 系列-安装新主机,识别老存储-识别ASM磁盘
  20. 2021综述:一般目标检测中的遮挡处理

热门文章

  1. 《R语言实战》第6章
  2. Java反射机制浅析
  3. Spring之Bean的配置(二)
  4. Java 利用反射实现C#的委托
  5. [转]chroot的使用
  6. android项目允许命令,android 项目 gradle 命令
  7. mysql逻辑删除的问题_数据库表涉及-逻辑删除
  8. java list数据的更新,java – ObservableList不更新ArrayList
  9. ORACLE强大的令人发指
  10. Serverless 架构模式及演进