需求
对所有的请求, 进行私钥加签, 公钥验签
不对响应进行处理
- 首先, 统一处理请求, 那就需要网关了
- 选好网关后, 约定好加签验签的规则
- 公钥私钥的格式, 代码中公私钥变量值的来源
- 签名放在哪里, 时间戳放在哪里
方案
这里使用的是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
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