api 验签

什么是重放攻击

简单来说就是把同一个请求重复的发送数次.. 这样在特定的场景下 比如: 购买.消费 这种场景下.. 如果重放成功.. 是很麻烦的.. 所以,这种请求不允许重放

验签流程图

file

验签需要的参数

  • timestamp 时间戳 (虽然没什么用,因为客户端的时间戳不准.. 但是,建议还是传上来,指不定以后有用)
  • nonce 随机字符串
  • salt 加密的盐
  • sign 客户端生成的签名

其中 timestamp nonce sign 可以从header头中传输过来 salt 一定要在客户端藏好

服务端和客户端协商一致的加密算法

  • 建议这里的get参数一定要排序..否则. 加密结果会不一致

openresty 代码

http {
    ....
    init_worker_by_lua_block {
        redis = require("resty.redis")
        cjson = require "cjson"
    }

    server {
        ....
        location / {
            lua_code_cache on;
            access_by_lua_file /usr/local/openresty/lua/sign.lua;
        }
    }
}

sign.lua 代码

由于lua代码是边查询文档写的.. 太烂 勿喷

---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by ailuoy.
--- DateTime: 2019-04-22 09:53
---
-- 获得请求方式
local function getRequestMethod()
    return ngx.var.request_method
end
-- 判断是否是空table
local function isTableEmpty(t)
    if t == nil or _G.next(t) == nil then
        return true
    else
        return false
    end
end
-- 获得参数sort排序
local function getSortArgs(inTable)
    local keys, tmp = {}, {}
    --提出所有的键名并按字符顺序排序
    for k, _ in pairs(inTable) do
        keys[#keys + 1] = k
    end
    table.sort(keys)
    --根据排序好的键名依次读取值并拼接字符串成key=value&key=value
    for _, k in pairs(keys) do
        if type(inTable[k]) == "string" or type(inTable[k]) == "number" then
            tmp[#tmp + 1] = k .. "=" .. tostring(inTable[k])
        end
    end
    local signChar = table.concat(tmp, "&")
    return signChar
end
-- 获得get参数
local function getRetUriArgs()
    local args = ngx.req.get_uri_args()
    local getTable = {}
    for k, v in pairs(args) do
        getTable[k] = v
    end
    return getTable
end
-- 过滤不需要验签的路由比如上次头像
local function verifyByRouter()
    local noVerify = { "upload" }
    local routerTable = {}
    local docUri = ngx.var.document_uri
    local isPass = true
    --按照"/"分割字符串
    for w in string.gmatch(docUri, "([^'/']+)") do
        table.insert(routerTable, w)
    end
    for _, v in pairs(noVerify) do
        if (routerTable[1] == v) then
            isPass = false
            goto continue
        end
    end
    :: continue ::
    return isPass
end
local function verifyContentType(headersTab, ContentType)
    local reqContentType = headersTab["Content-Type"]
    if (reqContentType == nil) then
        return false
    end
    if (reqContentType ~= ContentType) then
        return false
    end
    return true
end
-- 获得get参数
local function getPostArgs()
    ngx.req.read_body()
    local postTable = {}
    local postData = ngx.var.request_body
    if (postData ~= nil) then
        postTable = cjson.decode(postData)
    end
    return postTable
end
-- 获得签名
local function makeSign(getTable, postStr, timeStamp, nonce, salt)
    local getStr = getSortArgs(getTable)
    local concatStr = timeStamp .. nonce .. salt
    if (postStr ~= nil) then
        concatStr = postStr .. concatStr
    end
    if (isTableEmpty(getTable) == false) then
        concatStr = getStr .. concatStr
    end
    local signMd5 = ngx.md5(concatStr)
    return signMd5
end
-- 返回request Header 中content-type不是 application/json
local function responseBadRequestContentType()
    ngx.status = 460
    ngx.header['Content-Type'] = 'application/json; charset=utf-8'
    ngx.say(cjson.encode({ code = 4600001, message = "Content-Type is not correct", details = {} }))
    ngx.exit(ngx.HTTP_OK)
end
-- header 中缺少验签的必要参数
local function responseMissSignHeaders()
    ngx.status = 460
    ngx.header['Content-Type'] = 'application/json; charset=utf-8'
    ngx.say(cjson.encode({ code = 4600002, message = "sign headers is not correct", details = {} }))
    ngx.exit(ngx.HTTP_OK)
end
-- header 签名和客户端不一致
local function responseSignError()
    ngx.status = 460
    ngx.header['Content-Type'] = 'application/json; charset=utf-8'
    ngx.say(cjson.encode({ code = 4600003, message = "signature error", details = {} }))
    ngx.exit(ngx.HTTP_OK)
end
-- header 签名已经重放
local function responseSignRepeat()
    ngx.status = 460
    ngx.header['Content-Type'] = 'application/json; charset=utf-8'
    ngx.say(cjson.encode({ code = 4600004, message = "Signature repeat", details = {} }))
    ngx.exit(ngx.HTTP_OK)
end
-- 关闭redis
local function closeRedis(red)
    if not red then
        return false
    end
    -- 连接池大小是100个,并且设置最大的空闲时间是 10 秒
    local ok, err = red:set_keepalive(10000, 100)
    if not ok then
        return false
    end
    return true
end
-- 检查验签的必要参数
local function verifySignHeaders(headersTab)
    local requiredHeadersTab = { 'Sign-Timestamp', 'Sign-Nonce', 'Sign-Sign' }
    isPass = true
    for _, v in pairs(requiredHeadersTab) do
        if (headersTab[v] == nil) then
            isPass = false
            goto continue
        end
    end
    :: continue ::
    return isPass
end
if (verifyByRouter() == true) then
    local headersTab = ngx.req.get_headers()
    --if (verifyContentType(headersTab, 'application/json') == false) then
    --    responseBadRequestContentType()
    --    return
    --end
    if (verifySignHeaders(headersTab) == false) then
        responseMissSignHeaders()
        return
    end
    ngx.req.read_body()
    local postStr = ngx.var.request_body
    local getTable = getRetUriArgs()
    local clientSign = headersTab['Sign-Sign']
    local signTimestamp = headersTab['Sign-Timestamp']
    local signNonce = headersTab['Sign-Nonce']
    local salt = '这里是验证签名的盐值'
    local serverSign = makeSign(getTable, postStr, signTimestamp, signNonce, salt)
    if (serverSign ~= clientSign) then
        responseSignError()
        return
    end
    local nowTime = os.time()
    --使用redis
    local red = redis:new()
    red:set_timeout(1000)
    local ok, err = red:connect("127.0.0.1", 6379)
    ok, err = red:auth("这里是redis密码")
    if ok then
        signCount, err = red:exists(serverSign)
        if (signCount > 0) then
            closeRedis(red)
            responseSignRepeat()
            return
        end
        ok, err = red:set(serverSign, nowTime)
        red:expire(serverSign, "3600")
        closeRedis(red)
    end
end

注意事项

  • 验签的Redis一定要和业务分开
  • 注意第三方回调的路由已一定要排除在外