This website requires JavaScript.

Lua实战

1 2022-09-14 20:22:22 53

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:

  • lua-resty-jwt
  • lua-redis
  • lua-cjson
  • rsa 其中cjson和redis是OpenResty自带的,所以需要安装新的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
- response.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

  1. ngxin不正常工作时一般就是lua脚本有问题没写对检查下
  2. body_filter_by_lua_file 会执行多遍,所以需要将数据暂存在某个字段,然后拼接。参考response.lua
  3. 实现接口数据加解密时遇到 Error: incorrect header checkView in Console ,这是Kong导致的问题, 参考 Kong + Gzip doesn't work

REF

免责声明

本站提供Hack区的一切软件、教程和仅限用于学习和研究目的;不得将其用于商业或者非法用途,否则,一切后果由用户自己承担 您必须在下载后的24个小时之内, 从您的电脑中彻底删除。如果条件支持,请支持正版,得到更好的服务。 另如有侵权请邮件与我 联系处理。敬请谅解!

本文于   2022/9/14 下午  发布 

永久地址: https://madaoo.com/article/1354093378479984640

版权声明: 自由转载-署名-非商业性使用   |   Creative Commons BY-NC 3.0 CN