apisix-插件开发-非对称性加签验签

需求

对所有的请求, 进行私钥加签, 公钥验签
不对响应进行处理

  • 首先, 统一处理请求, 那就需要网关了
  • 选好网关后, 约定好加签验签的规则
  • 公钥私钥的格式, 代码中公私钥变量值的来源
  • 签名放在哪里, 时间戳放在哪里

方案

这里使用的是apisix网关
使用apisix默认语言lua, 以插件的方式完成开发

签名约定

长度: 2048bit
格式: PKCS#8
内容约定: 公私钥begin和end
生成地址: http://www.metools.info/code/c80.html
举例:

# 公钥
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Rkxr3ksqneR/pEZ95w/
hPEbpg59tKzC1flDoNOn7m0yYdXubo3gwOFx+7urkyZ4MY6qB6qFDuRclBO8fy6n
zs5/27+31XY5KmB2nfRaaCQHfV0iq46QFRcLXiUIPR0TeXrijsEA04LcX8a/jstR
FH2JdL83qG9heSq7kmwdtP+U/9qu2XW0IiCf3h15DPfhxZq++7NNcOq89Fy19Uz/
TQLTkqJ5XclVawPfwvEnbLTSIGv1zjell3aXBPgaMJ1cBV4Eoj5EWjnlyKTn5F/m
Vmxwt++Z0+wqGNIY5NKkKyyyfdFddQmx+XOi6Z4MUfAlmLGjegnr5cmlUs74rkdo
QwIDAQAB
-----END PUBLIC KEY-----


# 私钥
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDRGTGveSyqd5H+
kRn3nD+E8RumDn20rMLV+UOg06fubTJh1e5ujeDA4XH7u6uTJngxjqoHqoUO5FyU
E7x/LqfOzn/bv7fVdjkqYHad9FpoJAd9XSKrjpAVFwteJQg9HRN5euKOwQDTgtxf
xr+Oy1EUfYl0vzeob2F5KruSbB20/5T/2q7ZdbQiIJ/eHXkM9+HFmr77s01w6rz0
XLX1TP9NAtOSonldyVVrA9/C8SdstNIga/XON6WXdpcE+BownVwFXgSiPkRaOeXI
pOfkX+ZWbHC375nT7CoY0hjk0qQrLLJ90V11CbH5c6LpngxR8CWYsaN6CevlyaVS
zviuR2hDAgMBAAECggEBAI6iEftxvB84WzV8g2sdgVLNKaAXqHYzluBhHVm6p4YD
pOeKCBAP5e2Mm7UtKnfBOSINAD0ke4lSCzjTUbSr/bobsKoU2HDbKVmX3bIXugfp
89X3TywZnn1Ub+OzfTA7AkrOoXbhfw/I50zKBWeBl3hTvg0OVwglmicOGE9kQstw
g5uUdkkDRQqK/BLJH34Zg+q2uaAveLSf4XYTIVKG+rIdryomLyUwoHEgH2KU7BoH
XbDPtDTbdhCWCFVcy3xyMKdrTsqihUQG5f1vPxiViVlDN2d/AJtcQn90vvlGj8ec
IbGINM+RQ07KQE7T5MEyTk+yimbUjdqdK2/VHxdKndkCgYEA/XpS/cgmuQ2NV7EB
LLmH+FIixAjttA3go57KvmkXqCcAIaqG3pTZstrDC6gArb5b26gLgFPbyzAnETx2
OOTn/blHFgD1YPz+sDJ9dTp3s7XClGMEW3G7IEhCd0o40XYpU3ZoWedCkVGbnWLy
koXEAu1nKrYE3M6Wdjj5rWkm3McCgYEA0y3S24g28lfJSMvFhiOATQVYhG9GfAnS
9WyQayegrOJPfY/1e2tLB+q+qH0cisB2l8nWtaUmCV8XuBCS3wnDx/yG3BaSV3Pz
68D4IJxU7nv3H6FewbG4kQjT2n15IuSuM0Z1camzEY96+9Yj2nnwxzB7ljV41nMW
CUK6ZzT4BKUCgYEAsNHlFN3LjWmTwKlcLWvbGvGJCQFFeEX5/4mk2sEK3KUgJVwE
qz5gwrByQS5YEttozsjiBQn6mDol9pMb0UJ1Rvw5R3MxuQ+jRxxhgiZgHD/d1Y9h
Gb0zkSh3HSnsismHuI4v6w8005R/HoJIvseLXZNoVVYV/EYslZnXKg3hKz8CgYA8
DByX1cyh7jpK275HnRKfU/TOe4GURYrZxEvwXC1A23z03BlWRbTpBGPALwsNnRpb
oMXPkq0VHxf0e6n3h6RG2lRSgoyMF2l1UMJ9K1avFUq4kL8L3of3nYX365OlS1cJ
N3CvqCxFwwGaWFKLjf7b9Lo/hObeO405huLP8+zODQKBgQD7XL5XINl48XltSjoi
Y2iyB8Ay9VVgshN0fQ/qZmxqdhrfogoIyG1ONK2P8mzdYijqh0VWu4qXXE/Gwg5e
fYEH9mNfy5CYMp+XSSXtAfVvx0k2OyYqWJk31NKv+ls95TCwMnYcB4EX82+Cf7GK
4LYEFbMjPBDNP01tSbDKdQ/uDA==
-----END PRIVATE KEY-----

lua语言的公私钥加签验签

参考文档

https://github.com/spacewander/lua-resty-rsa

该依赖也可以加密, 解密; 也可生成公私钥, 具体参考github readme

依赖文件

https://github.com/spacewander/lua-resty-rsa/blob/master/lib/resty/rsa.lua

使用方法

项目克隆

git clone https://github.com/spacewander/lua-resty-rsa.git
git clone git@github.com:spacewander/lua-resty-rsa.git

方法参考

local resty_rsa = require "resty.rsa"

-- 公钥
local rsa_public_key = [[-----BEGIN PUBLIC KEY-----
xxx
-----END PUBLIC KEY-----]]

-- 私钥
local rsa_priv_key = [[-----BEGIN PRIVATE KEY-----
xxx
-----END PRIVATE KEY-----]]

-- 算法
local algorithm = "SHA256"

local priv, err = resty_rsa:new({ private_key = rsa_priv_key, algorithm = algorithm })
if not priv then
    ngx.say("new rsa err: ", err)
    return
end

-- 加签
local str = "hello"
local sig, err = priv:sign(str)
if not sig then
    ngx.say("failed to sign:", err)
    return
end

local pub, err = resty_rsa:new({ public_key = rsa_public_key, algorithm = algorithm })
if not pub then
    ngx.say("new rsa err: ", err)
    return
end

-- 验签
local verify, err = pub:verify(str, sig)
if not verify then
    ngx.say("verify err: ", err)
    return
end

注意

该依赖生成的签名, 打印出来是乱码, 且有换行
使用其自带的签名生成算法, 和padding内容, 都无效
导致header传递签名的时候, 客户端需要序列化, 服务端需要反序列化
解决参考: https://www.cnblogs.com/loseself/p/16184360.html

image

local priv, err = resty_rsa:new({
    private_key = rsa_priv_key,
    padding = resty_rsa.PADDING.RSA_PKCS1_PADDING,
    algorithm = algorithm,
}) 

插件开发

参考文档

插件开发参考
https://www.cnblogs.com/loseself/p/16151876.html

公私钥的来源与传递参考
https://www.cnblogs.com/loseself/p/16157785.html

客户端-加签内容

请求的uri, get入参, 请求体, 请求方法
时间戳, 签名放在header里

-- 获取加签验签的内容
local function get_signing_string(time, ctx)
    -- 请求方发, 请求头, url参数, 请求体
    local request_method = ngx.req.get_method()
    local query_params = ngx.req.get_uri_args()
    local signing_string_items = {
        ctx.var.uri,
        request_method,
        query_params_string = core.json.encode(query_params),
        time
    }

    local body = core.request.get_body()
    if body then
        signing_string_items.body = body
    end

    local signing_string = core.table.concat(signing_string_items, "\n") .. "\n"
    return signing_string
end

客户端-签名, 放入header

注意: lua依赖的方法, 要放在该方法上面
HEADER_KEY_SIGNATURE 相关的大写变量是常量, 可以自己补充, 同下

-- 请求, 租户私钥加签, 在rewrite阶段即可
function _M.rewrite(conf, ctx)
    local time = ngx_time()
    local signing_string = get_signing_string(time, ctx)
    -- 获取私钥的方式, 自己定义
    local private_key = get_private_key()
    local signature, err = generate_signature(private_key, signing_string)
    if not signature then
        return HTTP_ERROR_CODE, { msg = "请求加签失败, 错误信息: " .. err }
    end

    -- 签名有换行, 乱码, 等. 
    -- 传递时为防止被转义, 需要序列化, 服务端需要反序列化
    local json_signature = core.json.encode(signature)

    -- 赋值header
    core.request.set_header(ctx, HEADER_KEY_SIGNATURE, json_signature)
    core.request.set_header(ctx, HEADER_KEY_SIGNATURE_TIME, time)
end

服务端-验签

-- 公钥验签
local function validate(ctx, rsa_public_key)
    local time = core.request.header(ctx, HEADER_KEY_SIGNATURE_TIME)
    local signature = core.request.header(ctx, HEADER_KEY_SIGNATURE)
    if not time or not signature then
        return nil, "请求头时间或者签名不存在"
    end

    -- 防重放攻击
    local diff = abs(ngx_time() - time)
    if diff > 60 then
        return nil, "签名超时, 请重新生成签名"
    end

    -- 按照约定的序列化规则, 反序列化签名
    local decode_signature, err3 = core.json.decode(signature)
    if err3 then
        return nil, "签名约定反序列化失败, " .. err3
    end

    if type(decode_signature) ~= "string" then
        return nil, "签名数据类型错误"
    end

    local publicObject, err = resty_rsa:new({ public_key = rsa_public_key, algorithm = ALGORITHM })
    if not publicObject then
        return nil, err
    end

    local signing_string = get_signing_string(time, ctx)
    local verify, err2 = publicObject:verify(signing_string, decode_signature)
    return verify, err2
end
-- 请求, 租户公钥验签, 在rewrite阶段即可
function _M.rewrite(conf, ctx)
    local verify, err = validate(ctx, get_tenant_public_key(ctx))
    if not verify then
        return HTTP_VALIDATE_ERROR_CODE, { msg = "请求验签失败, 错误信息: " .. err }
    end
end

优化

参考插件可以参考apisix提供的对称性加密插件hmac-auth
该插件除了加密方式外, 其他思想都是相同的
因为现在是简单实现, 具体优化的地方, 性能的优化可以根据需求, 然后参考, 来进行操作
https://apisix.apache.org/zh/docs/apisix/plugins/hmac-auth

posted @ 2022-04-24 09:08  loseself  阅读(1393)  评论(0编辑  收藏  举报