当前位置: 首页 > 工具软件 > lua-epoll > 使用案例 >

利用nginx的lua脚本和jwt给任意api接口做授权认证

苗康平
2023-12-01

安装nginx及lua

# 安装依赖
yum -y install make gcc gcc-c++ wget crontabs zlib zlib-devel \
 openssl openssl-devel perl patch bzip2 ca-certificates
 
# Stable version 下载nginx,openssl,luajit2源码
wget http://nginx.org/download/nginx-1.18.0.tar.gz
wget https://www.openssl.org/source/openssl-1.1.1g.tar.gz
wget https://github.com/openresty/luajit2/archive/v2.1-20190329.tar.gz
tar xf openssl-1.1.1g.tar.gz
tar xf nginx-1.18.0.tar.gz
tar xf v2.1-20190329.tar.gz

# 安装luajit2
cd luajit2-2.1-20190329
make && make install PREFIX=/usr/local/luajit && cd
cat > /etc/ld.so.conf.d/luajit.conf<<EOF
/usr/local/luajit/lib
EOF
ln -sf /usr/local/luajit/lib/libluajit-5.1.so.2 /lib64/libluajit-5.1.so.2
ldconfig

# 下载lua模块和nginx开发模块
wget https://github.com/openresty/lua-nginx-module/archive/v0.10.14.tar.gz
tar xf v0.10.14.tar.gz
wget https://github.com/vision5/ngx_devel_kit/archive/v0.3.1.tar.gz 
tar xf v0.3.1.tar.gz 
cat >/etc/profile.d/luajit.sh<<EOF
export LUAJIT_LIB=/usr/local/luajit/lib
export LUAJIT_INC=/usr/local/luajit/include/luajit-2.1
EOF
source /etc/profile.d/luajit.sh

# 安装nginx
useradd -s /sbin/nologin www
cd nginx-1.18.0
./configure --user=www --group=www --prefix=/usr/local/nginx --with-http_stub_status_module \
--with-http_ssl_module --with-http_v2_module --with-http_gzip_static_module --with-http_sub_module \
--with-stream --with-stream_ssl_module --with-openssl=/root/openssl-1.1.1g \
--with-openssl-opt='enable-weak-ssl-ciphers' --with-ld-opt=-Wl,-rpath,/usr/local/luajit/lib \
--add-module=/root/lua-nginx-module-0.10.14 --add-module=/root/ngx_devel_kit-0.3.1
make -j `grep 'processor' /proc/cpuinfo | wc -l`
make install
ln -sf /usr/local/nginx/sbin/nginx /usr/bin/nginx

mkdir -p /home/wwwroot/default
chown -R www.www /home/wwwroot/default
mkdir /home/wwwlogs && chown www.www /home/wwwlogs
mkdir /usr/local/nginx/conf/vhost
echo ok > /home/wwwroot/default/index.html
cat >/etc/systemd/system/nginx.service<<EOF
[Unit]
Description=The NGINX HTTP and reverse proxy server
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStart=/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=false

[Install]
WantedBy=multi-user.target
EOF
systemctl enable nginx
systemctl start nginx
curl localhost
ss -lntp | grep 80

至此,nginx和lua安装完毕,但还有一些参数和配置需要优化

www用户打开的文件描述符数,进程数:

cat >>/etc/security/limits.conf<<eof
* soft nproc 65535
* hard nproc 65535
* soft nofile 65535
* hard nofile 65535
eof
echo "fs.file-max=65535" >> /etc/sysctl.conf

nginx配置文件/usr/local/nginx/conf/nginx.conf,如下:

user  www www;
worker_processes auto;
worker_cpu_affinity auto;
error_log  /home/wwwlogs/nginx_error.log  crit;
pid        /usr/local/nginx/logs/nginx.pid;
worker_rlimit_nofile 51200;
events
    {
        use epoll;
        worker_connections 51200;
        multi_accept off;
        accept_mutex off;
    }

http
    {
        include       mime.types;
        default_type  application/octet-stream;

        server_names_hash_bucket_size 128;
        client_header_buffer_size 32k;
        large_client_header_buffers 4 32k;
        client_max_body_size 50m;

        sendfile on;
        sendfile_max_chunk 512k;
        tcp_nopush on;
        keepalive_timeout 60;
        tcp_nodelay on;
        
        gzip on;
        gzip_min_length  1k;
        gzip_buffers     4 16k;
        gzip_http_version 1.1;
        gzip_comp_level 2;
        gzip_types     text/plain application/javascript application/x-javascript text/javascript text/css application/xml application/xml+rss;
        gzip_vary on;
        gzip_proxied   expired no-cache no-store private auth;
        gzip_disable   "MSIE [1-6]\.";
        server_tokens off;
        access_log off;

server
    {
        listen 80 default_server reuseport;
        server_name _;
        index index.html index.htm;
        root  /home/wwwroot/default;
        location /lua { default_type text/html; content_by_lua 'ngx.say("hello world")'; }
        access_log  off;
    }
include vhost/*.conf;
}

检验lua是否安装成功:curl localhost/lua
更多参考:lnmp.org

jwt生成及验证

参考:https://jwt.io/
PHP参考:https://github.com/firebase/php-jwt
python:https://github.com/jpadilla/pyjwt/
jwt的库有各种语言的,使用起来非常简单
jwt本身是明文的,如果使用是的http,https会加密所有内容
nbf为not before,即不早于某时间戳
exp为expired,即过期时间戳
pip install pyjwt

Python 2.7.5 (default, Apr  2 2020, 13:16:51) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import jwt
>>> encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256')
>>> encoded
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg'
>>> jwt.decode(encoded, 'secret', algorithms=['HS256'])
{u'some': u'payload'}

接口授权认证

如果是HTTP协议的接口,可以将jwt放在cookie中,前端请求时会自动带上cookie
当然,也可以保存到localStorage,发送Ajax请求时主动带上
在nginx上使用lua判断jwt是否有效

如果是非HTTP的接口,也可以在nginx上使用stream反向代理tcp/udp接口。

lua和nginx搭配最好的是openresty,还有API管理工具Kong,openresty中有各种lua模块,kong中有各种api管理工具,这里以nginx为主,仅解决jwt的依赖

nginx中以lua脚本方式使用jwt的依赖有:lua-cjson、lua-resty-hmac、lua-resty-string、lua-resty-jwt

将所有的依赖放入/usr/local/luajit/lib/resty

安装lua-cjson库,cjson是一个json的解析库,lua中非常好用

wget https://www.kyne.com.au/~mark/software/download/lua-cjson-2.1.0.tar.gz
tar xf lua-cjson-2.1.0.tar.gz
sed -i 's#^LUA_INCLUDE_DIR\s*=\s*$(PREFIX)/include#LUA_INCLUDE_DIR = $(PREFIX)/luajit/include/luajit-2.1#' Makefile
# lua_cjson.c:1298:13: error: static declaration of ‘luaL_setfuncs’ follows non-static declaration
sed -i '1298s/^static void/void/' lua_cjson.c
make && make install

因为是写脚本,修改nginx.conf中error_log /home/wwwlogs/nginx_error.log info;将错误级别改为info以便调试,结束后可以改回crit

# 在http中添加lua的path路径,注意最后是两个分号
lua_package_path "/usr/local/luajit/lib/?.lua;;";

# 在server中添加测试代码
location /jwt { default_type text/html; content_by_lua '
local cjson = require "cjson"
ngx.say(cjson.encode({3, 4, 5}))
'; }

nginx -s reload
curl localhost/jwt
# [3,4,5]

mkdir /usr/local/luajit/lib/resty
wget https://github.com/openresty/lua-resty-string/archive/v0.12.tar.gz
tar xf v0.12.tar.gz && cp lua-resty-string-0.12/lib/resty/*.lua /usr/local/luajit/lib/resty/
wget https://github.com/SkyLothar/lua-resty-jwt/releases/download/v0.1.11/lua-resty-jwt-0.1.11.tar.gz
tar lua-resty-jwt-0.1.11.tar.gz && cp lua-resty-jwt-0.1.11/lib/resty/*.lua /usr/local/luajit/lib/resty/
wget https://github.com/jkeys089/lua-resty-hmac/archive/v0.05.tar.gz
tar xf v0.05.tar.gz && cp -y lua-resty-hmac-0.05/lib/resty/hmac.lua /usr/local/luajit/lib/resty/

测试jwt是否可用,nginx.conf中添加:

location  /sign {
    content_by_lua '
        local cjson = require "cjson"
        local jwt = require "resty.jwt"

        local jwt_token = jwt:sign(
            "lua-resty-jwt",
            {
                header={typ="JWT", alg="HS256"},
                payload={foo="bar"}
            }
        )
        ngx.say(jwt_token)
    ';
}
location  /verify {
    content_by_lua '
        local cjson = require "cjson"
        local jwt = require "resty.jwt"

        local jwt_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" ..
            ".eyJmb28iOiJiYXIifQ" ..
            ".VAoRL1IU0nOguxURF2ZcKR0SGKE1gCbqwyh8u2MLAyY"
        local jwt_obj = jwt:verify("lua-resty-jwt", jwt_token)
        ngx.say(cjson.encode(jwt_obj))
    ';
}

结果:

nginx -s reload
curl localhost/sign
#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.VxhQcGihWyHuJeHhpUiq2FU7aW2s_3ZJlY6h1kdlmJY
curl localhost/verify
#{"payload":{"foo":"bar"},"reason":"everything is awesome~ :p","raw_header":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9","valid":true,"header":{"alg":"HS256","typ":"JWT"},"signature":"VAoRL1IU0nOguxURF2ZcKR0SGKE1gCbqwyh8u2MLAyY","verified":true,"raw_payload":"eyJmb28iOiJiYXIifQ"}

这里给出一个验证逻辑:

首次打开时,后端生成jwt,添加到GET参数jwt中,如果不是浏览器访问,可以直接加到header中
nginx中判断,如果有jwt,在header中设置Authorization为jwt
lua验证时,先从header中读取Authorization,如果没有从cookie中读取
验证成功后,添加cookie,并设置授权变量,反向代理到api,api中可进行自己的处理
退出时,删除cookie
因为,jwt存在cookie中,安全性不足,一般需要HTTPS

location ^~ /api/ {
    if ($arg_jwt) {
        set $http_Authorization "Bearer $arg_jwt";
    }
    set $apiUser '';
    access_by_lua '
        local jwt = require("resty.api-jwt")
        jwt.auth()
    ';
    proxy_set_header X-WEBAUTH-USER $apiUser;
    proxy_pass http://127.0.0.1:4000/;
}
location ^~ /api/logout {
    add_header Set-Cookie "api_token=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/api; HttpOnly; Secure";
    proxy_pass http://127.0.0.1:4000/;
}

resty中的api-jwt.lua脚本:

local jwt = require "resty.jwt"
local cjson = require "cjson"
--fill your real secret
local secret = "lua-resty-jwt"

local M = {}

function M.auth(claim_specs)
    -- require Authorization request header
    local auth_header = ngx.var.http_Authorization
    if auth_header == nil then
        auth_header = ngx.var.cookie_api_token
    end

    if auth_header ~= nil then
        local _, _, token = string.find(auth_header, "Bearer%s+(.+)")
        if token ~= nil then
            local jwt_obj = jwt:verify(secret, token)
            if jwt_obj.verified == false then
                ngx.log(ngx.WARN, "Invalid token: ".. jwt_obj.reason)
                ngx.exit(ngx.HTTP_UNAUTHORIZED)
            end
            if not ngx.var.cookie_api_token or ngx.var.cookie_api_token ~= auth_header then
                ngx.header["Set-Cookie"] = "api_token=Bearer "..token.."; path=/api; httpOnly=1; Secure;"
            end
            ngx.var.apiUser = jwt_obj["payload"]["user"]
        else
        	ngx.log(ngx.WARN, "Missing token")
            ngx.exit(ngx.HTTP_UNAUTHORIZED)
        end
    else
    	ngx.log(ngx.WARN, "require authorization request header")
        ngx.exit(ngx.HTTP_UNAUTHORIZED)
    end
end

return M

lua获取get参数及判断是否是数组:

local uri_args = ngx.req.get_uri_args()
local uid = 0
if type(uri_args['uid']) == 'table' then
    uid= uri_args['uid'][1]
else
    uid= uri_args['uid']
end

kong中有许多非常有用的lua脚本

参考:
https://www.jianshu.com/p/66d5163b9e99
https://segmentfault.com/a/1190000015677681

 类似资料: