基于 OpenResty 实现 JWT 验证,


〇、必备技能

  • 熟悉 Nginx
  • 对 Lua 及 OpenResty 中进行交互有基本了解
  • 熟悉 Linux 的基本操作

一、环境准备

  • CentOS 7
  • OpenResty 1.13.6.2

二、安装

1、安装 OpenResty

参考官方文档 OpenResty® Linux 包——CentOS

$ sudo yum install yum-utils

# 添加仓库
$ sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

# 安装 OpenResty
$ sudo yum install openresty

# 安装命令行工具 resty
$ sudo yum install openresty-resty

# 安装命令行工具 opm
$ sudo yum install openresty-opm

可以列出所有 openresty 仓库里头的软件包,根据需要选择安装:

$ sudo yum --disablerepo="*" --enablerepo="openresty" list available

OpenResty RPM 包 页面包含更多细节。

2、测试 OpenResty

修改 nginx.conf 配置文件:

worker_processes  1;
error_log logs/error.log;
events {
    worker_connections 1024;
}
http {
    server {
        listen 8080;
        location / {
            default_type text/html;
            content_by_lua_block {
                ngx.say("<p>hello, world</p>")
            }
        }
    }
}

启动服务,通过 cURL 或者浏览器测试:

$ curl http://localhost:8080/

返回

<p>hello, world</p>

表示 OpenResty 安装成功,Lua 脚本正常工作。

3、安装 JWT 的 Lua 插件

SkyLothar/lua-resty-jwt 是用于 ngx_lua 和 LuaJIT 的 Lua 实现库。在项目 README 的 Installation 部分有安装说明。本文采用的是 opm 的安装方式:

$ sudo opm get SkyLothar/lua-resty-jwt

4、测试 JWT 插件

参考配置

# nginx.conf

lua_package_path "/path/to/lua-resty-jwt/lib/?.lua;;";

server {
    default_type text/plain;

    location = /verify {

        content_by_lua_block {
            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))
        }
    }

    location = /sign {
        content_by_lua_block {
            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)
        }
    }
}

说明:

  • lua_package_path:指定 Lua 脚本位置。
    注意:上述配置中搜索路径的最后出现了 ;; 两个半角分号,代表的是 LuaJIT 安装时的原始搜索路径,如果在前面的搜索路径里面无法搜索到需要的模块,就会依次搜索后面的路径。

  • default_type:指定输出为纯文本,无实际作用,只是为了在浏览器中方便查看换行等符号。

  • /verify:校验 JWT

$ curl http://lh:39100/verify

执行结果:

{
  "signature": "VAoRL1IU0nOguxURF2ZcKR0SGKE1gCbqwyh8u2MLAyY",
  "reason": "everything is awesome~ :p",
  "valid": true,
  "raw_header": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9",
  "payload": {
    "foo": "bar"
  },
  "header": {
    "alg": "HS256",
    "typ": "JWT"
  },
  "verified": true,
  "raw_payload": "eyJmb28iOiJiYXIifQ"
}
  • /sign:生成 JWT
$ curl http://lh:39100/sign

执行结果:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.VxhQcGihWyHuJeHhpUiq2FU7aW2s_3ZJlY6h1kdlmJY

得到以上执行结果,表示 JWT 插件安装成功。

三、编写验证逻辑

通过前面的步骤,已经完成了基本环境的搭建。后续编写校验逻辑,根据校验结果控制访问行为。

目标:
验证 JWT:

  • 成功:将请求转发至后台服务
  • 失败:返回 401,并以 JSON 形式返回错误原因

思路:
通过 Header 传 JWT:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.VxhQcGihWyHuJeHhpUiq2FU7aW2s_3ZJlY6h1kdlmJY

Lua 拿到 JWT 之后,校验合法性。

代码:
校验脚本:

-- nginx-jwt.lua


local cjson = require "cjson"
local jwt = require "resty.jwt"

--your secret
local secret = "a secret key"

local M = {}


function M.auth()
    -- require Authorization request header
    local auth_header = ngx.var.http_Authorization

    if auth_header == nil then
        ngx.log(ngx.WARN, "No Authorization header")
        ngx.exit(ngx.HTTP_UNAUTHORIZED)
    end

    ngx.log(ngx.INFO, "Authorization: " .. auth_header)

    -- require Bearer token
    local _, _, token = string.find(auth_header, "Bearer%s+(.+)")

    if token == nil then
        ngx.log(ngx.WARN, "Missing token")
        ngx.exit(ngx.HTTP_UNAUTHORIZED)
    end

    ngx.log(ngx.INFO, "Token: " .. token)

    local jwt_obj = jwt:verify(secret, 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.INFO, "JWT: " .. cjson.encode(jwt_obj))

end

return M

nginx.conf 配置:

# nginx.conf

location /check-jwt {
    default_type text/plain;
    access_by_lua_block {
        local obj = require('nginx-jwt')
        obj.auth()
    }
    proxy_pass "http://backend-server/";
}

延展:
1、对于实际应用的场景,可以根据需要将 JWT 放在自己需要的位置,比如 Cookie 中。
2、对于错误时的返回值,本例只是将验证结果以 JSON 形式返回,这样暴露了很多信息。可以考虑将详细信息记入日志,将裁剪过的内容返回给请求方。

四、参考资料

  • Nginx实现JWT验证-基于OpenResty实现
  • OpenResty 不完全指南
  • nginx_lua_cookbook
  • OpenResty最佳实践 - github.io
  • OpenResty 最佳实践 - 极客学院
  • Openresty 执行阶段与api之间的关系(一)
  • JSON Web Token (JWT) 简介
  • Return JSON responses when using OpenResty + Lua

(完)

相关内容

    暂无相关文章