专栏首页小勇DW3openresty实现接口签名安全认证

openresty实现接口签名安全认证

一)需求背景 现在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验证成功"; }

local cjson = require "cjson"
local secret = "xx"

local function union(table1,table2)
for k,v in pairs(table2) do
table1[k] = v
end
return table1
end

local function get_from_local(key)
local my_cache = ngx.shared.my_cache
local value = my_cache:get(key)
return value
end

function set_to_local(key, value)
local my_cache = ngx.shared.my_cache
local value = my_cache:set(key, value, 10 * 60)
end

local function get_from_redis(key)
local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
local ok, err = red:connect("xx", xx)
if not ok then
ngx.log(ngx.ERR, "redis failed to connect: ", err)
return nil
end

local res, err = red:auth("xx")
if not res then
ngx.log(ngx.ERR, "redis auth err: ", err)
return nil
end

local res, err = red:get(key)
if not res then
ngx.log(ngx.ERR, "redis failed to get key: ", err)
return nil
end

if res == ngx.null then
ngx.log(ngx.ERR, "redis cached null: ", res)
return nil
end

return res
end

-- 取值校验
local params = {}
local args = ngx.req.get_uri_args()
union(params,args)

local key = params["key"];
ngx.log(ngx.ERR, key)
if key == nil then
local mess = "key值为空"
ngx.log(ngx.ERR, mess)
local return_400 = {}
return_400["code"] = 400
return_400["result"] = mess
local message = cjson.encode(return_400)
ngx.say(message)
return
end

local token = params["token"];
ngx.log(ngx.ERR, token)
if token == nil then
local mess = "token值为空"
ngx.log(ngx.ERR, mess)
local return_400 = {}
return_400["code"] = 400
return_400["result"] = mess
local message = cjson.encode(return_400)
ngx.say(message)
return
end

local sign = params["sign"]
ngx.log(ngx.ERR, sign)
if sign == nil then
local mess="签名参数为空"
ngx.log(ngx.ERR, mess)
local return_400 = {}
return_400["code"] = 400
return_400["result"] = mess
local message = cjson.encode(return_400)
ngx.say(message)
return
end

local timestamp = params["time"]
ngx.log(ngx.ERR, timestamp)
if timestamp == nil then
local mess="时间戳参数为空"
ngx.log(ngx.ERR, mess)
local return_400 = {}
return_400["code"] = 400
return_400["result"] = mess
local message = cjson.encode(return_400)
ngx.say(message)
return
end

--时间戳有没有过期,10秒过期
local now_mill = ngx.now() * 1000 
if now_mill - timestamp > 5000 then
local mess="链接过期"
ngx.log(ngx.ERR, mess)
local return_400 = {}
return_400["code"] = 400
return_400["result"] = mess
local message = cjson.encode(return_400)
ngx.say(message)
return
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)
ngx.log(ngx.ERR, rightsign)
if sign ~= rightsign then
--如果签名错误返回错误信息并记录日志,
local mess="sign check error"
ngx.log(ngx.ERR, mess)
local return_400 = {}
return_400["code"] = 400
return_400["result"] = mess
local message = cjson.encode(return_400);
ngx.say(message)
return
else
ngx.log(ngx.ERR, "sign check success...")
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();
    }
}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java设计模式-单例模式

    作为对象的创建模式,单例模式确保其某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类。单例模式有以下特点:

    小勇DW3
  • redis中各种数据类型的常用操作方法汇总

    string是redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。 string类型是二进制安全的。意思是re...

    小勇DW3
  • 亿级流量场景下,大型缓存架构设计实现【3】---- 实现高可用

    在分布式系统中,每个服务都可能会调用很多其他服务,被调用的那些服务就是依赖服务,有的时候某些依赖服务出现故障也是很正常的。

    小勇DW3
  • linux安装部署Tomcat服务器

    unix和linux平台下做web服务器: -Apache,Nginx,Lighttpd(支持php,python) -Tomcat,IBM webspher...

    吴柯
  • Python模拟登陆新版知乎

    目前网上很多模拟登录知乎的代码已经无法使用,即使是二、三月的代码也已经无法模拟登陆知乎,所以我现在将新版知乎的模拟登录代码和讲解发布出来。

    喵叔
  • hadoop2-elasticsearch的安装

    Hongten
  • 各种选择+冒泡+插入排序图解

    由于排序题中大部分都只需要得到排序的最终结果,而不需要写排序的完整过程(例如冒泡排序,快速排序等过程)因此比赛时强烈建议使用C语言中的库函数qsort或是C++...

    编程范 源代码公司
  • FunDA(17)- 示范:异常处理与事后处理 - Exceptions handling and Finalizers

        作为一个能安全运行的工具库,为了保证占用资源的安全性,对异常处理(exception handling)和事后处理(final clean-up)的支持...

    用户1150956
  • 腾讯云 云开发 部署 Blazor网站

    Blazor 应用程序除了在 Github Pages/Gitee Pages等静态资源部署以外,现在你有了一个新的选择,那就是使用云开发静态网站功能来部署啦!

    张善友
  • 因为“文化堕落”,印度“封杀”抖音

    作为近两年最受人们关注的手机软件之一,抖音已经成为了很多年轻人的最爱,根据最新的数据显示,抖音在国内的日活达到了2.5亿,成为了当之无愧的新一代国民APP。

    谭庆波

扫码关注云+社区

领取腾讯云代金券