一种基于微服务应用对私域资源的安全访问方法

需求

在客户现场部署了微服务技术架构的业务产品,需要挂相关的连接能够访问本司的内部资源,如WIKI。除了在网络安全层面限制之外,还需要部署一套应用层面的安全网关,保证只有登录微服务产品的用户才能访问在线文档。

整体流程

在这里插入图片描述

nginx:源系统的代理网关,这里也用于拦截特殊的请求链接,获取cookies中的jwt token信息;

客户端openresty:验证token是否合法、是否过期,若使用redis存储token信息,则需访问redis;验证无误则进行签名后转发,签名密钥使用rsa pkcs1格式,以pem文件存储;

服务端openresty:验证请求是否合法,合法则进行转发,其中合法包括首次跳转合法和后续访问合法。

目标服务:请求全部由服务端openresty转发,服务应该验证白名单,在外部网段,只允许openresty访问。

技术框架

nginx、openresty、lua、jwt

关键技术点

获取token并转发

源微服务系统网关需要拦截这些特殊菜单请求, /online_wiki。并重定向到安全网关客户端。

upstream gateway_clients{  
​
      server 127.0.0.1:7777;  
​
 }
​
 location = /online_wiki{
​
     set $token '';  
     if ($http_cookie ~* "Admin-Token=([^;]+)(?:;|$)") {  
         set $token $1;  
     }  
     proxy_set_header Host $host;  
     proxy_set_header X-Real-IP $remote_addr;  
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;  
     proxy_set_header X-Forwarded-Proto $scheme;  
     
     proxy_pass http://gateway_clients/jwt_lua?token=$token;
​
}

客户端openresty脚本核心逻辑

  1. 解密jwt token

  2. 访问redis,验证是否过期

  3. 签名(结合timestamp和nonce实现防重放攻击)

local function load_config(config_file)
    local config = {}
    local file = io.open(config_file, "r")
    if not file then
        ngx.log(ngx.ERR, "Unable to open config file: ", config_file)
        return nil
    end
    for line in file:lines() do
        local key, value = line:match("^(%S+)=(%S+)$")
        if key and value then
            config[key] = value
            ngx.log(ngx.ERR,'key:value',key..value);
        end
    end
    file:close()
    return config
end
 
local function get_config_value(key)
    local config = ngx.shared.nonce_cache:get("config")
    if not config then
        local debug = require("debug")
        local info = debug.getinfo(2, "S")
        local path = info.source:sub(2):match("(.*/)");
        
        config = load_config(path..'config.conf')
        ngx.shared.nonce_cache:set("config", config)
    end
    return config[key]
end
​
​
​
​
local cjson = require("cjson")
local jwt = require("resty.jwt")
​
​
local jwt_token = string.sub(ngx.req.get_uri_args()['token'],1);
ngx.log(ngx.ERR,"token,",jwt_token);
local jwt_obj = jwt:verify(get_config_value('token.pwd'), jwt_token)
if jwt_obj.verified == false then
        ngx.log(ngx.WARN, "Invalid token: ".. jwt_obj.reason)
        
        ngx.status = ngx.HTTP_UNAUTHORIZED
        ngx.header.content_type = "application/json; charset=utf-8"
       -- ngx.say(cjson.encode(jwt_obj))
        ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
​
​
​
ngx.log(ngx.ERR,"md5,",ngx.md5(jwt_token))
​
​
local redis = require "resty.redis"
local red = redis:new()
 
red:set_timeout(1000) -- 1 second timeout
​
​
local ok, err = red:connect(get_config_value('redis.ip'), get_config_value('redis.port'))
      if not ok then
          ngx.say("failed to connect: ", err)
          ngx.exit(ngx.HTTP_UNAUTHORIZED)
      red:close();    
      end
​
​
local ok, err = red:auth(get_config_value('redis.pwd'));
      if not ok then
          ngx.say("failed to auth: ", err)
          ngx.exit(ngx.HTTP_UNAUTHORIZED)
      red:close();
      end
​
local ok,err = red:select(get_config_value('redis.db'));
​
if not ok then
          ngx.say("failed to select: ", err)
          ngx.exit(ngx.HTTP_UNAUTHORIZED)
     red:close();
end
 
local res, err = red:get(string.format('uas:user:login:%s',ngx.md5(jwt_token)))
      if not res then
          ngx.say("failed to get dog: ", err)
          ngx.exit(ngx.HTTP_UNAUTHORIZED)
      end
      if res == ngx.null then
          ngx.say("dog not found.",res)
          ngx.exit(ngx.HTTP_UNAUTHORIZED)
      end
 
​
​
local ok,err = red:set_keepalive(30000,20);
if not ok then
​
     ngx.log(ngx.ERR,"failed to set keepalive:",err);
end
​
​
local ppath = get_config_value('remotegateway.keypath');
local pname = get_config_value('remotegateway.keyname');
​
local file, err = io.open(ppath, "r")
if not file then
    ngx.log(ngx.ERR,"无法打开文件:",err);
    return
end
local private_key_path =file:read("*a")
 
-- 关闭文件
file:close()
​
​
​
ngx.log(ngx.ERR,"key:",private_key_path);
local b64 = require "ngx.base64"
local pk, err = require("resty.openssl.pkey").new(private_key_path)
if not pk then
    ngx.log(ngx.ERR, '[ERROR]:', err)
    return
end
local parameters, err = pk:get_parameters()
local e = parameters.e
​
​
local ts = os.time();
​
local uuid = require "resty.jit-uuid"
local my_uuid = uuid()
local data = ts..':'..my_uuid..':'..pname;
local digest, err = require("resty.openssl.digest").new("SHA256")
digest:update(data)
​
local signature, err = pk:sign(digest)
local wholetable = {c=pname,timestamp=ts,nonce=my_uuid,nsign=b64.encode_base64url(signature)}
--ngx.log(ngx.ERR,"数字签名参数,",ngx.escape_uri(cjson.encode(wholetable)));
ngx.redirect(get_config_value('remotegateway.url')..'/verify_lua?all='..ngx.escape_uri(cjson.encode(wholetable)),ngx.HTTP_MOVED_TEMPORARILY);
​
​
​
​

服务端openresty脚本核心逻辑

  1. 验签

  2. 保存链接,为后续验证_Referer_头准备

​
​
local function load_config(config_file)
    local config = {}
    local file = io.open(config_file, "r")
    if not file then
        ngx.log(ngx.ERR, "Unable to open config file: ", config_file)
        return nil
    end
    for line in file:lines() do
        local key, value = line:match("^(%S+)=(%S+)$")
        if key and value then
            config[key] = value
        end
    end
    file:close()
    return config
end
 
local function get_config_value(key)
    local config = ngx.shared.nonce_cache:get("config")
    if not config then
        local debug = require("debug")
        local info = debug.getinfo(2, "S")
        local path = info.source:sub(2):match("(.*/)");
        
        config = load_config(path..'config.conf')
        ngx.shared.nonce_cache:set("config", config)
    end
    return config[key]
end
​
​
​
​
​
local cjson = require("cjson")
​
​
​
​
​
local b64 = require "ngx.base64"
​
​
local args = ngx.req.get_uri_args();
​
local all = args["all"];
local o = ngx.unescape_uri(all);
ngx.log(ngx.ERR,"接收数字签名,",o)
local args = cjson.decode(o);
​
local c = args["c"];
local ts = args["timestamp"];
local nonce = args["nonce"];
local nsign = args["nsign"];
​
​
local ppath = get_config_value('localgateway.keypath');
local file, err = io.open(ppath..c..'.pem', "r")
if not file then
    ngx.log(ngx.ERR,"无法打开文件:",err);
    return
end
local private_key_path =file:read("*a")
​
-- 关闭文件
file:close()
​
​
local pk, err = require("resty.openssl.pkey").new(private_key_path)
if not pk then
    ngx.log(ngx.ERR, '[ERROR]:', err)
    return
end
​
​
local data = ts..':'..nonce..':'..c;
ngx.log(ngx.ERR, 'sign-o',private_key_path)
local digest, err = require("resty.openssl.digest").new("SHA256")
digest:update(data)
​
local ok, err = pk:verify(b64.decode_base64url(nsign),digest)
​
if not ok then
    ngx.log(ngx.ERR, '验签失败',err)
    ngx.exit(ngx.HTTP_UNAUTHORIZED)
          
end
​
--check timestamp nonce
--ngx.log(ngx.ERR, '验签成功')
local at = tonumber(ts);
local ct = os.time();
​
if ct - at > 5 then
    ngx.log(ngx.ERR, '时间差太长')
    ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
​
--ngx.log(ngx.ERR, '时间合法,ct=',ct,',at=',at);
 
local red = ngx.shared.nonce_cache;
local res = red:get(string.format('wiki:lua:login:%s',ngx.md5(c)))
      if not res then
          red:set(string.format('wiki:lua:login:%s',ngx.md5(c)),'1', 5);
          --jump
          local scheme = ngx.var.scheme
          local host = ngx.var.host
          local port = ngx.var.server_port
          local request_uri = ngx.var.request_uri
          local wholeurl = ''
          --ngx.log(ngx.ERR,'端口号',port);
          if (scheme == "http" and port == '80') or (scheme == "https" and port == '443') then
              wholeurl = scheme .. "://" .. host .. request_uri
          else
              wholeurl = scheme .. "://" .. host .. ":" .. port .. request_uri
          end
          red:set('wiki:refered:'..wholeurl,'1', 300);
          --ngx.log(ngx.ERR,'设置地址','wiki:refered:'..wholeurl);
          return
      end
      ngx.log(ngx.ERR,"重复的请求",res);
      ngx.exit(ngx.HTTP_UNAUTHORIZED);
​
​
​
​
​
​
​

会话实现

在nginx中代理所有请求到后端

在这里插入图片描述

check.lua 检查referer并刷新缓存(续命)如下

​
local headers = ngx.req.get_headers()
local referer = headers["Referer"] or headers["referer"]
 
if referer then
   -- ngx.log(ngx.ERR,"Referer header: ", referer)
else
    ngx.log(ngx.ERR,"Referer header does not exist.")
    ngx.exit(ngx.HTTP_UNAUTHORIZED);
end
​
 
local red = ngx.shared.nonce_cache;
--ngx.log(ngx.ERR,'获取值,','wiki:refered:'..referer);
local res = red:get('wiki:refered:'..referer)
      if not res then
          ngx.log(ngx.ERR,"非法的请求");
​
          ngx.exit(ngx.HTTP_UNAUTHORIZED);
          return;
      end
     -- ngx.log(ngx.ERR,"合法请求",res);
      red:set('wiki:refered:'..referer,'1', 300);
​
​

在线安装

OpenResty - Download下载最新包,对于非最新的linux而言,编译openresty时,需要下载openssl源码包,在编译时指定

如下

./configure --prefix=/home/user/app/openresty --with-openssl=/home/user/app/openssl-1.1.1k
​
 make && make install

进入openresty/bin目录,下载相关依赖的lua、openssl、jwt、uuid,分别进行加解密、token处理、uuid生成。

 opm get fffonion/lua-resty-openssl 
​
 opm get SkyLothar/lua-resty-jwt
​
 opm get openresty/lua-resty-jit-uuid
  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值