openresty集成ssl验签功能

1 篇文章 0 订阅
1 篇文章 0 订阅

centos测试

使用阿里云的yum repo

mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d.bak/CentOS-Base.repo
wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
yum clean all
yum makecache
yum repolist

需要更新yum组件,否则安装openssl-devel提示冲突

yum update 

安装所需yum组件

#luaossl的依赖项
yum install epel-release
yum install openssl-devel
#lua包管理器
yum install luarocks
yum install lua-devel

安装所需lua组件

#ssl组件
luarocks install luaossl
#base64组件,openresty镜像中不用执行该命令,如执行会提示Error: No results matching query were found.
luarocks install mime

生成证书命令

openssl genrsa -out private.key 2048
openssl rsa -in private.key -pubout -out public.pem
openssl rsa -in private.key -outform der -out private.der

验证签名lua脚本

local pkey = require("openssl.pkey")
local digest = require("openssl.digest")
local mime = require("mime")

-- 读取pem格式公钥证书文件
local pk_cert_data = assert(io.open("/root/public.pem", "rb")):read("*a")

local pkey =pkey.new()

pkey:setPubinsiglicKey(pk_cert_data)

-- 原始数据
local data = "Hello, World!"

-- 创建SHA256摘要对象
local digest_data = digest.new("sha256")
print(digest_data)
-- 更新摘要
digest_data:update(data)
--使用java项目中签名方法生成的签名
local signatures ="fwOupXYu/6bl8RqMJWztrU6trsKkGXSh2nyQyHHmiuqlyCXWm5sE3zCzqoYVB5ppYIwzYGWYmhEH9jRzmB6R7oZXDLKnom/BaRnUMmrHjmgaI5AL9jAAyLKckNlOl1ptS35a7A3PyFDTBkOxq6gEZjJhMHxJVe4W98JJMpZqzZ5nM5nOVDKRsUrziSO0M7u/nDt03IoxB8wN34ljkjhXbT8v6my85QYTH3e0Pj9dcBiUV9Sv04GOCEr+eE9HznWirb169+MsulGERi2ILZAf+kTWJ/I5TYrGvbviowhtvRU4j1zJvnVRE1rjOEZpvxmBUAoSXpWOSUcqlVKAwsLbaQ=="
local signature =mime.unb64(signatures)
-- 打印签名
print("Signature:", signature)

-- 验证签名
local verify_result = pkey:verify(signature, digest_data)

-- 判断验签结果
if verify_result == true then
  print("Signature is valid.")
elseif verify_result == false then
  print("Signature is invalid.")
else
  print("Verification failed.")
end

输出

userdata: 0x21973e8
Signature:      ��v.�����%l��N��¤t��|��q���%֛��0����i`�3`e���4s���W
                                                                  ���o�i�2jǎh#�
                                                                               �0
Signature is valid.

生成签名的方法(AuthUtil)

    public static void main(String[] args) {
        String json2String = "Hello, World!";
        String sign = RsaUtil.standardSign(RsaUtil.getPrivateKeyFromDer("/opt/private.der"), json2String);
        System.out.println(sign);
        PublicKey publicKey = RsaUtil.getPublicKeyFromPem("/opt/public.pem");
        AuthUtil authUtil = new AuthUtil();
        boolean b = authUtil.standardVerifyMsgByPemPubKey(sign, json2String, publicKey);
        System.out.println(b);

    }
    /**
 * 获取私钥
 *
 * @param filename 私钥路径
 * @return 私钥
 */
@SneakyThrows
public static PrivateKey getPrivateKeyFromDer(String filename) {
    Security.addProvider(new BouncyCastleProvider());

    byte[] keyBytes = Files.readAllBytes(Paths.get(filename));
    PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
    KeyFactory kf = KeyFactory.getInstance(KEY_ALGORITHM);
    return kf.generatePrivate(spec);
}
 /**
 * 生成签名
 */
public static String standardSign(PrivateKey priKey, String tobeSigned) {
    try {
        Signature sign = Signature.getInstance(SIGNATURE_ALGORITHM);
        sign.initSign(priKey);
        sign.update(tobeSigned.getBytes(StandardCharsets.UTF_8));
        byte[] signed = sign.sign();
        return Base64.encodeAsString(signed);
    } catch (Exception e) {
        throw new BizException(407, "签名失败");
    }
}  
    
/**
 * 获取公钥
 *
 * @param fileName 公钥路径
 * @return 公钥
 */
@SneakyThrows
public static PublicKey getPublicKeyFromPem(String fileName) {
    File file = new File(fileName);
    String parentPath = file.getParent();
    String filename = file.getName();
    byte[] key = Files.readAllBytes(Paths.get(parentPath, filename));

    Security.addProvider(new BouncyCastleProvider());

    final PemObject pemObject;

    try (PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(key)))) {
        pemObject = pemReader.readPemObject();
    }

    X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(pemObject.getContent());

    KeyFactory factory = KeyFactory.getInstance(KEY_ALGORITHM);

    return factory.generatePublic(pubKeySpec);
}

/**
 * 签名验证
 *
 * @param sign   待验证码
 * @param param  文本
 * @param pubKey 公钥
 * @return 验证结果
 */
@SneakyThrows
public boolean standardVerifyMsgByPemPubKey(String sign, String param, PublicKey pubKey) {

    log.info("sign: {}", sign);
    log.info("param: {}", param);

    Signature verify = Signature.getInstance(SIGNATURE_ALGORITHM);
    verify.initVerify(pubKey);
    verify.update(param.getBytes(StandardCharsets.UTF_8));
    boolean verify1 = verify.verify(Base64.getDecoder().decode(sign));
    log.info(" verify result : {}", verify1);
    return verify1;
}

输出

fwOupXYu/6bl8RqMJWztrU6trsKkGXSh2nyQyHHmiuqlyCXWm5sE3zCzqoYVB5ppYIwzYGWYmhEH9jRzmB6R7oZXDLKnom/BaRnUMmrHjmgaI5AL9jAAyLKckNlOl1ptS35a7A3PyFDTBkOxq6gEZjJhMHxJVe4W98JJMpZqzZ5nM5nOVDKRsUrziSO0M7u/nDt03IoxB8wN34ljkjhXbT8v6my85QYTH3e0Pj9dcBiUV9Sv04GOCEr+eE9HznWirb169+MsulGERi2ILZAf+kTWJ/I5TYrGvbviowhtvRU4j1zJvnVRE1rjOEZpvxmBUAoSXpWOSUcqlVKAwsLbaQ==
18:23:19.195 [main] INFO com.rkg.test.AuthUtil - sign: fwOupXYu/6bl8RqMJWztrU6trsKkGXSh2nyQyHHmiuqlyCXWm5sE3zCzqoYVB5ppYIwzYGWYmhEH9jRzmB6R7oZXDLKnom/BaRnUMmrHjmgaI5AL9jAAyLKckNlOl1ptS35a7A3PyFDTBkOxq6gEZjJhMHxJVe4W98JJMpZqzZ5nM5nOVDKRsUrziSO0M7u/nDt03IoxB8wN34ljkjhXbT8v6my85QYTH3e0Pj9dcBiUV9Sv04GOCEr+eE9HznWirb169+MsulGERi2ILZAf+kTWJ/I5TYrGvbviowhtvRU4j1zJvnVRE1rjOEZpvxmBUAoSXpWOSUcqlVKAwsLbaQ==
18:23:19.199 [main] INFO com.rkg.test.AuthUtil - param: Hello, World!
18:23:19.199 [main] INFO com.rkg.test.AuthUtil -  verify result : true
true

参考文件

https://github.com/zhaozg/lua-openssl#documentation

参考附件.User Guide to luaossl, Comprehensive OpenSSL Module for Lua

构建ssl验签镜像

基础镜像制作

启动容器

docker run -it openresty/openresty:latest bash

安装依赖

#进入openresty容器
#更新包索引
apt update
#安装openssl lua
apt install -y openssl libssl-dev luarocks lua5.4 
#ssl组件
luarocks install luaossl
#安装resty-http组件
luarocks install lua-resty-http
#创建机构证书存放路径,请注意这个路径是固定的不要修改,如果修改下面proxy_route_gateway_rsa.lua脚本中publicCertFolder值也要一并修改

mkdir -p /usr/local/openresty/nginx/gateway-public-cert
#更改文件夹所属用户为nobody,否则下面脚本创建证书文件时提示没有权限操作 
chown nobody /usr/local/openresty/nginx/gateway-public-cert

制作镜像

#提交镜像
docker commit 上面的容器id openresty/openresty:lua-openssl-base 
#推送到harbor,如果harbor已存在该镜像为了避免出现其他问题,建议先删除harbor上的同名镜像,其他机器使用时一样,如果有同名镜像建议先删除
docker push openresty/openresty:lua-openssl-base 

打开nginx日志开关

打开nginx开关,方便排查日志,修改nginx.conf打开error_log的注释

#如果需要查看lua脚本输出的日志打开这里的配置
error_log  logs/error.log;

修改docker-compose.yml

添加command命令 ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"],必须要添加这个,不添加容器启动不了

version: '3.4'
services:
  insightext:
    image: openresty/openresty:lua-openssl-base 
    container_name: insightext
    hostname: insightext
    restart: always
    ports:
      - 1443:1443
      - 9443:9443
      - 19001:19001
      - 19002:19002
      - 19003:19003
      - 19101:19101
    volumes:
      #设置时区使用宿主机时区,防止时间少8小时
      - ./localtime:/etc/localtime:ro
      #替换nginx.conf
      - ./nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf
      # 日志
      - ./logs:/usr/local/openresty/nginx/logs
      # https 证书
      - ./config/ssl:/usr/local/openresty/nginx/ssl
      # 子配置文件
      - ./config/conf.d:/usr/local/openresty/nginx/conf/conf.d
      # lua文件夹目录
      - ./lua/:/etc/lua
    command: ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]
    networks:
      test:
        ipv4_address: 172.18.0.211
networks:
  test:
    external: true

修改java相关的http接口引用proxy_route_gateway_rsa.lua

修改nginx conf文件中,java使用的http端口中content_by_lua_file的文件名为proxy_route_gateway_rsa.lua;


server {
    listen       1443 ssl;
    server_name  localhost;
    add_header X-XSS-Protection 1;
    add_header X-Content-Type-Options "nosniff";
    add_header Content-Security-Policy "script-src 'self';";
    add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
    add_header Set-Cookie “Secure”;
    proxy_intercept_errors on;
    large_client_header_buffers 4 1024k;
    client_max_body_size 1024m;
    ssl_certificate     /usr/local/openresty/nginx/ssl/http/server.crt;
    ssl_certificate_key /usr/local/openresty/nginx/ssl/http/server.key;
    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout  5m;
    ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256;
    ssl_prefer_server_ciphers   on;
    ssl_protocols TLSv1.2;

    location / {
      content_by_lua_file  /etc/lua/proxy_route_gateway_rsa.lua;
    }
}

创建proxy_route_gateway_rsa.lua

在lua文件夹中创建proxy_route_gateway_rsa.lua,内容如下

gatewayUrl是后端提供了一个接口,返回ssl证书,这里可以根据需要自定义

--后端gateway地址,注意结尾没有/,如果和gateway在同一个docker网络中,可以直接写http://insightgateway:10000
local gatewayUrl = "http://10.10.1.26:10000"
--路由转发自定义请求头
local header_name = 'proxy'
--路由转发的实际地址,header_name中json的哪个key
local route_name = 'route1'

-- Lua数组表示不需要登录认证的URL路径
ignoredUrlArr = {
    "/auth/auth/login",
    "/auth/auth/aes/key",
    "/base/admin/versions"
}

--------------------------------下面内容,不要随意修改,如需修改请联系任凯歌--------------------------------------------

--------------------------------字符串切割函数----------------------------------------------------------------------
function split(input, delimiter)
    local arr = {}
    string.gsub(input, '[^' .. delimiter .. ']+', function(w) table.insert(arr, w) end)
    return arr
end
--------------------------------字符串切割函数----------------------------------------------------------------------

--------------------------------检查变量suffix是否以数组元素结尾-----------------------------------------------------------
function endsWith(str, suffix)
    return string.sub(str, -string.len(suffix)) == suffix
end
--------------------------------检查变量a是否以数组元素结尾-----------------------------------------------------------

--机构公钥存放路径,注意必须写/usr/local/openresty/nginx/gateway-public-cert/,这个目录在基础镜像中手动创建并chown nobody gateway-public-cert了
local publicCertFolder="/usr/local/openresty/nginx/gateway-public-cert/"
--------------------------------获取请求头信息----------------------------------------------------------------------
--获取机构信息
local originInstitutionHeader = ngx.req.get_headers()["originInstitution"] or ''
--截取机构信息
local originInstitutionHeaderArr = split(originInstitutionHeader, ",")
--获取机构id
local institutionId = originInstitutionHeaderArr[1]
--获取superAdmin
local superAdminHeader = ngx.req.get_headers()["superAdmin"] or ''
--获取签名
local signHeader = ngx.req.get_headers()["sign"] or ''

--------------------------------获取请求头信息----------------------------------------------------------------------

--------------------------------请求后端gateway生成证书文件放到本地--------------------------------------------------
function createInstitutionPublicCert(institutionId)
    if (institutionId ~= nil and institutionId ~= '') then
        local url = gatewayUrl .. "/publickey?institutionId=" .. institutionId
        local httpc = require("resty.http").new()
        -- Single-shot requests use the `request_uri` interface.
        local res, err = httpc:request_uri(url, {
            method = "GET",
            body = "",
            -- 禁用证书验证
            ssl_verify = false,  
            headers = {
            },
        })
        if not res then
            ngx.log(ngx.ERR, "Failed to request gateway get institution public cert", err)
            return false
        end
        --获取证书
        local body = res.body
        if (body == nil or body == '') then
            ngx.log(ngx.ERR, "unattained institutionId:" ..
                institutionId .. " public cert from insightgateway,please check")
        end
        --注意必须写到/usr/local/openresty/nginx/gateway-public-cert/下,这个目录在基础镜像中手动创建并chown nobody gateway-public-cert
        local fileName = publicCertFolder .. institutionId .. ".pem"
        local function write_to_file(str)
            local file, err = io.open(fileName, "w")
            if file then
                file:write(str)
                file:close()
            end
        end
        -- 将字符串写入文件
        write_to_file(body)
        return true
    end
    ngx.log(ngx.ERR, "Failed to request gateway get institution public cert error, institutionId is null, please check")
    return false
end
--------------------------------请求后端gateway生成证书文件放到本地--------------------------------------------------

--------------------------------端口转发,实现proxy_pass功能----------------------------------------------------------
function proxyPass(proxy_target)
    -- 获取原始请求的URI
    local uri = ngx.var.request_uri 
    local http = require "resty.http"
    local httpc = http.new()
    ngx.req.read_body()
    local res, err = httpc:request_uri(proxy_target .. uri, {
        method = ngx.req.get_method(),
        headers = ngx.req.get_headers(),
        -- 禁用证书验证
        ssl_verify = false,  
        body = ngx.req.get_body_data(),
    })
    if not res then
        ngx.log(ngx.ERR,"Failed to request upstream server: ", err)
        ngx.header.content_type = 'application/json'
        ngx.status = 500
        ngx.print('{"status":500,"data":"Failed to request upstream server"}')
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end

    ngx.status = res.status
    -- 设置代理响应的Content-Type头
    ngx.header["Content-Type"] = res.headers["Content-Type"]
    --ngx.header["Content-Type"] = "application/json"
    ngx.print(res.body)
end
--------------------------------端口转发,实现proxy_pass功能----------------------------------------------------------

--------------------------------数字签名校验,等同于gateway中的InstitutionTypeFilterHandler(没加时间校验)---------------
function checkSign(instId,data, signatures)
    local fileName = publicCertFolder .. instId .. ".pem"
    ngx.log(ngx.INFO,"fileName="..fileName)
    -- 读取pem格式公钥证书文件
    local pk_cert_data = io.open(fileName, "rb"):read("*a")

    local pkey = require("openssl.pkey")
    local digest = require("openssl.digest")
    local pkey = pkey.new()
    pkey:setPublicKey(pk_cert_data)
    -- 创建SHA256摘要对象
    local digest_data = digest.new("sha256")
    -- 更新摘要
    digest_data:update(data)
    --使用java项目中签名方法生成的签名
    --local signatures ="fwOupXYu/6bl8RqMJWztrU6trsKkGXSh2nyQyHHmiuqlyCXWm5sE3zCzqoYVB5ppYIwzYGWYmhEH9jRzmB6R7oZXDLKnom/BaRnUMmrHjmgaI5AL9jAAyLKckNlOl1ptS35a7A3PyFDTBkOxq6gEZjJhMHxJVe4W98JJMpZqzZ5nM5nOVDKRsUrziSO0M7u/nDt03IoxB8wN34ljkjhXbT8v6my85QYTH3e0Pj9dcBiUV9Sv04GOCEr+eE9HznWirb169+MsulGERi2ILZAf+kTWJ/I5TYrGvbviowhtvRU4j1zJvnVRE1rjOEZpvxmBUAoSXpWOSUcqlVKAwsLbaQ=="
    local signature =ngx.decode_base64(signatures)
    -- 验证签名
    return pkey:verify(signature, digest_data)
end
--------------------------------数字签名校验,等同于gateway中的InstitutionTypeFilterHandler(没加时间校验)--------------

--------------------------------代理转发----------------------------------------------------------------------------
function proxyRoute()
    local cjson = require "cjson.safe"

    local user_header_str = ngx.req.get_headers()[header_name] or ''

    --ngx.log(ngx.INFO,"proxy:",user_header_str)
    --如果需要查看lua脚本输出的日志打开这里的配置
    --ngx.log(ngx.INFO, " rkg-test-headers: ", user_header_str)
    if (user_header_str == nil or user_header_str == '') then
        ngx.header.content_type = 'application/json'
        ngx.print('{"status":400,"data":"Missing Or Wrong request header name ' .. header_name .. '"}')
        ngx.log(ngx.ERR,"Missing Or Wrong request header name ",header_name)
        return ngx.exit(ngx.HTTP_BAD_REQUEST)
    end
    --自定义请求头转换为json对象
    local user_header_json = cjson.decode(user_header_str)
    --判断自定义请求头是否为table,json的type是table
    local user_header_json_type = type(user_header_json)
    if user_header_json_type == "table" then
        --获取代理转发地址
        local proxy_url = user_header_json[route_name] or ''
        --调用路由转发函数
        proxyPass(proxy_url)
    else
        ngx.header.content_type = 'application/json'
        ngx.print('{"status":400,"data":"request header proxy is not json,please check"}')
        ngx.log(ngx.ERR,"request header proxy is not json")
        return ngx.exit(ngx.HTTP_BAD_REQUEST)
    end
end

--------------------------------代理转发----------------------------------------------------------------------------

--------------------------------主逻辑----------------------------------------------------------------------------

--superAdmin校验
if (superAdminHeader == "superAdmin") then
    --如果是superAdmin则直接放行,不走签名校验逻辑
    return proxyRoute()
end
--是否忽略url
local isIgnore = false
local reqUrl = ngx.var.uri 
for _, str in ipairs(ignoredUrlArr) do
    if endsWith(reqUrl,str) then
        isIgnore = true
        break
    end
end

if isIgnore then
    --如果是忽略的url则直接放行
    ngx.log(ngx.INFO,"Ignore signature verification,url:",reqUrl)
    return proxyRoute()
else

    if (institutionId == nil or institutionId == '') then
        ngx.header.content_type = 'application/json'
        ngx.print('{"status":400,"data":"Missing Or Wrong request header name originInstitution,please check"}')
        ngx.log(ngx.ERR, "Missing Or Wrong request header name originInstitution")
        return ngx.exit(ngx.HTTP_BAD_REQUEST)
    end
    --非superAdmin从后端gateway获取证书,写到本地文件夹
    if (createInstitutionPublicCert(institutionId)) then
        --签名校验
        if (checkSign(institutionId,originInstitutionHeader, signHeader)) then
            --签名校验通过,执行代理转发逻辑
            --ngx.log(ngx.INFO,"check sign success")
            return proxyRoute()
        else
            --签名校验失败
            ngx.header.content_type = 'application/json'
            ngx.print('{"status":400,"data":"Missing Or Wrong request header name sign,invalid signature,please check"}')
            ngx.log(ngx.ERR, "Missing Or Wrong request header name sign,invalid signature")
            return ngx.exit(ngx.HTTP_BAD_REQUEST)
        end
    else
        ngx.header.content_type = 'application/json'
        ngx.status = 500
        ngx.print('{"status":500,"data":"Failed to request gateway get institution public cert"}')
        ngx.log(ngx.ERR, "Failed to request gateway get institution public cert,please check")
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
end

--------------------------------主逻辑----------------------------------------------------------------------------

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值