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


    一)需求背景
    现在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) 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代码,模仿请求

    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=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)

     

  • 相关阅读:
    高效 Java Web 开发框架 JessMA v3.2.3 正式发布
    跨平台日志清理工具 Log-Cutter v2.0.1 RC-1 发布
    跨平台日志清理工具 Log-Cutter v1.0.3 正式发布
    高性能 Windows Socket 组件 HP-Socket v2.2.3 正式发布
    7. Oracle数据加载和卸载
    6. Oracle闪回特性
    5. RAMN备份与恢复
    4. Oracle数据库用户管理备份与恢复
    3. Oracle数据库逻辑备份与恢复
    后台系统依据路由生成tabs标签页
  • 原文地址:https://www.cnblogs.com/reblue520/p/11418589.html
Copyright © 2020-2023  润新知