OpenResty
OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。
OpenResty在国内很多公司都在使用,包含京东在内的很多大公司都是在此基础上实现的自己内部高性能服务器,再加上lua本身小巧简单,生态好,在服务器端搭配nginx可以说是如虎添翼好不威风。
为什么使用OpenResty?
OpenResty其实就是nginx+lua增强版,这里直接说lua,lua的好处这里就不陈述了,官网有详细介绍,我们一般使用lua可以嵌入到已有的系统中,对原有系统功能增强且性能客观。
本文中主要使用lua来实现token存在黑名单时禁止访问的功能,按理说这个功能在SpringCloud-Gateway 或者 Zuul中就可以实现,但是我司并没有使用这种网关组件且系统目前很稳定,所以没有必要现在再去大动干戈引入新组件,所以我使用lua脚本来实现这个功能。
lua脚本可以做的功能还很多不仅限于此,像游戏领域、unity3D等等。
快速启动
这里使用Docker来运行,-v 后面的路径和nginx.conf请提前创建好,下面做映射会用到。
注意版本openresty:1.19.3.1-alpine
- 1
- 2
- 3
- 4
- 5
# 安装opm
sudo yum install -y openresty-opm
# -v /data/nginx/conf/lua:/usr/local/openresty/nginx/conf/lua 映射自定义的脚本位置
docker run --name nginx -d -p 80:80 -p 443:443 -v /data/nginx/html:/usr/local/openresty/nginx/html -v /data/nginx/conf/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf -v /data/nginx/logs:/usr/local/openresty/nginx/logs -v /data/nginx/conf/lua:/usr/local/openresty/nginx/conf/lua -v /usr/local/openresty/site:/usr/local/openresty/site openresty/openresty:1.19.3.1-alpine
源码编译
或者使用源码编译的方式
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
wget https://openresty.org/package/centos/openresty.repo
sudo mv openresty.repo /etc/yum.repos.d/
sudo yum check-update
sudo yum install -y openresty-resty
sudo yum install -y openresty-opm
sudo yum --disablerepo="*" --enablerepo="openresty" list available
# 如果使用源码编译就需要编辑 vim /usr/local/openresty/nginx/ 下的文件
添加新modules
下面会用到三个modules:
- 1
- 2
- 3
- 4
opm get SkyLothar/lua-resty-jwt
opm install spacewander/lua-resty-rsa
# 默认安装在usr/local/openresty/site,将此路径映射到docker容器中即可。
案例
nginx.conf
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
pcre_jit on;
events {
worker_connections 1024;
}
http {
// 映射的自己的lua脚本位置,引用时: require('nginx-jwt')
lua_package_path "/usr/local/openresty/nginx/conf/?.lua;;";
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
lua_code_cache off;
default_type 'text/html';
# rewrite_by_lua_file conf/lua/token.lua;
# access_by_lua_file conf/lua/token.lua;
# 这里使用access_by_lua_block,限制在访问阶段,lua有多个执行级别,具体参考lua文档
access_by_lua_block {
local obj = require('nginx-jwt')
obj.auth()
}
}
# 根据referer处理逻辑
location /filter{
set $proxy "";
lua_code_cache off;
rewrite_by_lua_file conf/lua/filter.lua;
# $proxy 被filter.lua动态改变
proxy_pass http://$proxy$uri;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
nginx-jwt.lua
实现token存在黑名单时禁止访问 其他情况不处理
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
local cjson = require "cjson"
local jwt = require "resty.jwt"
local redis = require("resty.redis")
local secret = "internal"
local M = {}
local function close_connection( red )
if not red then
return
end
local ok, err = red:close()
if not ok then
ngx.log(ngx.ERROR, "close error ")
end
end
-- 定义函数
function M.auth()
local auth_header = ngx.var.http_Authorization
if auth_header == nil then
-- pass 不处理
return
end
ngx.log(ngx.INFO, "LUA Authorization: " .. auth_header)
local _, _, token = string.find(auth_header, "Bearer%s+(.+)")
if token == nil then
return
end
ngx.log(ngx.INFO, "Token: " .. token)
local red = redis:new()
red:set_timeout(2000)
local ip = "xx.xx.xx.xx"
local port = 6379
local ok, err = red:connect(ip, port)
if not ok then
return
end
local res, err = red:auth("xx")
if not res then
ngx.say("connect to redis error : ", err)
return
end
local jwt_obj = jwt:verify(secret, token)
if ( jwt_obj["payload"] ~= null ) then
local jti = jwt_obj["payload"]["jti"]
local resp, err = red:get("token:logout:"..jti)
close_connection(red)
if not resp then
ngx.say("get msg erro:", err)
return
end
-- nred:get 的值是特殊null,需要使用ngx.null
if resp ~= ngx.null then
-- 设置响应状态返回
ngx.log(ngx.WARN, "the token has been withdrawn ".. 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
end
ngx.log(ngx.INFO, "JWT: " .. cjson.encode(jwt_obj))
end
-- 导出函数
return M
filter.lua
根据http_referer判断,只处理符合条件的,比如带着参数a的跳转A网站,带着参数b的跳转B网站,
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
function urldecode(s)
s = s:gsub('+', ' ')
:gsub('%%(%x%x)', function(h)
return string.char(tonumber(h, 16))
end)
return s
end
function parseurl(s)
local ans = {}
for k,v in s:gmatch('([^&=?]-)=([^&=?]+)' ) do
ans[ k ] = urldecode(v)
end
return ans
end
-- t = parseurl("www.baidu.com/?hosId=SDSL2015")
t = parseurl(ngx.var.http_referer)
ngx.say(t.hosId)
-- proxy需要先定义
ngx.var.proxy = "www.ServerA.com"
if (t.hosId) then
ngx.var.proxy = "www.ServerB.com"
end
-- 自身版
set $proxy "";
rewrite_by_lua_block {
local s = ngx.var.http_referer
local ans = {}
for k,v in s:gmatch('([^&=?]-)=([^&=?]+)' ) do
ans[ k ] = v:gsub('+', ' '):gsub('%%(%x%x)', function(h)
return string.char(tonumber(h, 16))
end)
end
ngx.say(ans.hosId)
ngx.var.proxy = "www.ServerA.com"
if (ans.hosId) then
ngx.var.proxy = "www.ServerB.com"
end
ngx.say(ngx.var.proxy)
}
对接口的请求和响应加密
目前对称加密仅能对240字节以内的数据加密。所以下面使用的是对称加密
rest.rsa 只能用公钥加密数据,key_type 有两种类型:
- rsa.KEY_TYPE.PKCS1 The input key is in PKCS#1 BEGIN ==RSA== PUBLIC格式(usually starts with -----BEGIN RSA PUBLIC).
- rsa.KEY_TYPE.PKCS8 The input key is in PKCS#8 BEGIN PUBLIC格式(usually starts with -----BEGIN PUBLIC).
```
目前采用的对称加密
http { lua_package_path "/usr/local/openresty/nginx/conf/lua/?.lua;;"; include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; server { listen 80; # 开启对请求body的数据获取,否则拿不到前端请求体数据 lua_need_request_body on; server_name localhost; location / { # default_type 'text/html'; lua_code_cache off; rewrite_by_lua_file conf/lua/request.lua; proxy_pass https://api.madaoo.com/ar/category ; # 重写响应体导致数据长度变换,这里需要置为nil header_filter_by_lua_block { ngx.header.content_length = nil } body_filter_by_lua_file conf/lua/response.lua;
# body_filter_by_lua_block { # ngx.arg[1] = 6666 # } } }
}
- 1
- 2
- 3
- request.lua
> aes:new("这个密码")必须是十六位
前端传过来的是十六进制数据(便于传输),这里需要转换
local aes = require "resty.aes" local str = require "resty.string" function hex2bin(hexstr) local str = "" for i = 1, string.len(hexstr) - 1, 2 do local doublebytestr = string.sub(hexstr, i, i+1); local n = tonumber(doublebytestr, 16); if 0 == n then str = str .. '\00' else str = str .. string.format("%c", n) end end return str end
local aes_128_cbc_with_iv = assert(aes:new("1234567890123456",nil, aes.cipher(128,"cbc"), {iv="1234567890123456"})) local encrypted_body = ngx.req.get_body_data() ngx.log(ngx.ERR,"param encrypted_body is: ", encrypted_body) local decrypted_body = aes_128_cbc_with_iv:decrypt(hex2bin(encrypted_body)) ngx.log(ngx.ERR,"AES 128 CBC (WITH IV) Decrypted: ", decrypted_body) ngx.req.set_body_data(decrypted_body)
- 1
- 2
%0A-%20response.lua
-- 默认padding是pkcs7 local aes = require "resty.aes" local str = require "resty.string" local aes_128_cbc_with_iv = assert(aes:new("shunnengcnsecret",nil, aes.cipher(128,"cbc"), {iv="shunnengcnsecret"})) local encrypted_body = aes_128_cbc_with_iv:encrypt(ngx.arg[1]) ngx.log(ngx.ERR,"AES 128 CBC (WITH IV) Encrypted HEX: ", str.to_hex(encrypted_body)) local chunk, eof = ngx.arg[1], ngx.arg[2] local info = ngx.ctx.buf chunk = chunk or "" -- 将原本的内容记录下来,因为body_filter_by_lua_file会执行多遍 if info then ngx.ctx.buf = info .. chunk else ngx.ctx.buf = chunk end if eof then local encrypted_body = aes_128_cbc_with_iv:encrypt(ngx.ctx.buf) ngx.arg[1] = str.to_hex(encrypted_body) else ngx.arg[1] = nil end
- 1
- 2
- 前端
keyStr必须是十六位
#App.vue import CryptoJS from '@/api/article/CryptoJS' export default { name: 'App', methods: { savePwd(e) { const a = CryptoJS.encrypt('123') console.log(a) const b = CryptoJS.decrypt('5625a9e0f5894f98f1f3e506d64fcc80') console.log(b) } } }
CryptoJS.js
import CryptoJS from 'crypto-js' export default { // 加密 encrypt(word, keyStr, ivStr) { keyStr = keyStr || '1234567890123456' ivStr = ivStr || '1234567890123456' const key = CryptoJS.enc.Utf8.parse(keyStr) const iv = CryptoJS.enc.Utf8.parse(ivStr) const srcs = CryptoJS.enc.Utf8.parse(word)
- 1
- 2
- 3
- 4
- 5
- 6
const encrypted = CryptoJS.AES.encrypt(srcs, key, {
iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
})
return encrypted.ciphertext.toString()
}, // 解密 decrypt(encryptedStr, keyStr, ivStr) { // 拿到字符串类型的密文需要先将其用Hex方法parse一下 var encryptedHexStr = CryptoJS.enc.Hex.parse(encryptedStr)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
// 将密文转为Base64的字符串
// 只有Base64类型的字符串密文才能对其进行解密
var encryptedBase64Str = CryptoJS.enc.Base64.stringify(encryptedHexStr)
keyStr = keyStr || '1234567890123456'
ivStr = ivStr || '1234567890123456'
var key = CryptoJS.enc.Utf8.parse(keyStr)
const iv = CryptoJS.enc.Utf8.parse(ivStr)
var decrypt = CryptoJS.AES.decrypt(encryptedBase64Str, key, {
iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
})
return decrypt.toString(CryptoJS.enc.Utf8)
} }
```
note
- ngxin不正常工作时一般就是lua脚本有问题没写对检查下
- body_filter_by_lua_file 会执行多遍,所以需要将数据暂存在某个字段,然后拼接。参考response.lua
- 实现接口数据加解密时遇到 Error: incorrect header checkView in Console ,这是Kong导致的问题, 参考 Kong + Gzip doesn't work