- config: 反向代理、缓存、负载均衡、安全、SSL 等配置模板 - lua: API 网关、认证、动态路由、限流、WebSocket 等脚本示例 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
164 lines
4.6 KiB
Lua
164 lines
4.6 KiB
Lua
-- JWT 验证示例 - HMAC-SHA256 纯 Lua 实现
|
||
--
|
||
-- 功能:从 Authorization Header 提取 Bearer Token,
|
||
-- 解码 JWT Header/Payload,使用 HMAC-SHA256 验证签名,
|
||
-- 检查过期时间。
|
||
--
|
||
-- 依赖:OpenResty (ngx.*, cjson, resty.hmac)
|
||
--
|
||
-- 使用方式(在 nginx.conf 中):
|
||
-- access_by_lua_file /path/to/jwt_validate.lua;
|
||
|
||
-- ==================== 配置 ====================
|
||
|
||
-- HMAC 签名密钥(生产环境应从环境变量或 Vault 读取)
|
||
local SECRET = "your-super-secret-key-change-in-production"
|
||
|
||
-- 是否跳过 OPTIONS 预检请求
|
||
local SKIP_PREFLIGHT = true
|
||
|
||
-- ==================== 工具函数 ====================
|
||
|
||
local cjson = require("cjson.safe")
|
||
local hmac = require("resty.hmac")
|
||
|
||
--- Base64URL 解码
|
||
-- 将 Base64URL 编码的字符串转为标准 Base64 并解码
|
||
local function base64url_decode(str)
|
||
str = str:gsub("-", "+"):gsub("_", "/")
|
||
local mod = #str % 4
|
||
if mod == 2 then
|
||
str = str .. "=="
|
||
elseif mod == 3 then
|
||
str = str .. "="
|
||
end
|
||
return ngx.decode_base64(str)
|
||
end
|
||
|
||
--- HMAC-SHA256 签名
|
||
local function hmac_sha256(key, message)
|
||
local hm = hmac:new(key, hmac.ALGOS.SHA256)
|
||
local ok = hm:update(message)
|
||
if not ok then
|
||
return nil, "hmac update failed"
|
||
end
|
||
local digest = hm:final()
|
||
hm:close()
|
||
return digest
|
||
end
|
||
|
||
--- Base64URL 编码(无填充)
|
||
local function base64url_encode(str)
|
||
return (ngx.encode_base64(str):gsub("+", "-"):gsub("/", "_"):gsub("=", ""))
|
||
end
|
||
|
||
--- 验证 JWT 签名
|
||
local function verify_signature(header_b64, payload_b64, signature_b64)
|
||
local signing_input = header_b64 .. "." .. payload_b64
|
||
|
||
local expected_sig = hmac_sha256(SECRET, signing_input)
|
||
if not expected_sig then
|
||
return false, "signing failed"
|
||
end
|
||
|
||
local actual_sig, err = base64url_decode(signature_b64)
|
||
if not actual_sig then
|
||
return false, "decode signature failed: " .. err
|
||
end
|
||
|
||
return expected_sig == actual_sig, nil
|
||
end
|
||
|
||
-- ==================== 主逻辑 ====================
|
||
|
||
-- 跳过 OPTIONS 预检请求
|
||
if SKIP_PREFLIGHT and ngx.req.get_method() == "OPTIONS" then
|
||
return
|
||
end
|
||
|
||
-- 提取 Authorization Header
|
||
local auth_header = ngx.req.get_headers()["Authorization"]
|
||
if not auth_header or not auth_header:match("^Bearer%s+") then
|
||
ngx.status = 401
|
||
ngx.header["Content-Type"] = "application/json"
|
||
ngx.say(cjson.encode({
|
||
error = "missing_or_invalid_token",
|
||
message = "Authorization header with Bearer token required",
|
||
}))
|
||
return ngx.exit(401)
|
||
end
|
||
|
||
local token = auth_header:match("^Bearer%s+(.+)$")
|
||
if not token then
|
||
ngx.status = 401
|
||
ngx.header["Content-Type"] = "application/json"
|
||
ngx.say(cjson.encode({
|
||
error = "missing_or_invalid_token",
|
||
message = "Invalid Bearer token format",
|
||
}))
|
||
return ngx.exit(401)
|
||
end
|
||
|
||
-- 分割 JWT 为三部分
|
||
local header_b64, payload_b64, signature_b64 = token:match("^(%S+)%.(%S+)%.(%S+)$")
|
||
if not header_b64 or not payload_b64 or not signature_b64 then
|
||
ngx.status = 401
|
||
ngx.header["Content-Type"] = "application/json"
|
||
ngx.say(cjson.encode({
|
||
error = "malformed_token",
|
||
message = "JWT must have exactly 3 parts separated by '.'",
|
||
}))
|
||
return ngx.exit(401)
|
||
end
|
||
|
||
-- 验证签名
|
||
local sig_valid, sig_err = verify_signature(header_b64, payload_b64, signature_b64)
|
||
if not sig_valid then
|
||
ngx.status = 401
|
||
ngx.header["Content-Type"] = "application/json"
|
||
ngx.say(cjson.encode({
|
||
error = "invalid_signature",
|
||
message = "Token signature verification failed: " .. (sig_err or ""),
|
||
}))
|
||
return ngx.exit(401)
|
||
end
|
||
|
||
-- 解码 Payload 并检查过期
|
||
local payload_decoded = base64url_decode(payload_b64)
|
||
if not payload_decoded then
|
||
ngx.status = 401
|
||
ngx.header["Content-Type"] = "application/json"
|
||
ngx.say(cjson.encode({
|
||
error = "decode_failed",
|
||
message = "Failed to decode JWT payload",
|
||
}))
|
||
return ngx.exit(401)
|
||
end
|
||
|
||
local payload, err = cjson.decode(payload_decoded)
|
||
if not payload then
|
||
ngx.status = 401
|
||
ngx.header["Content-Type"] = "application/json"
|
||
ngx.say(cjson.encode({
|
||
error = "invalid_payload",
|
||
message = "JWT payload is not valid JSON",
|
||
}))
|
||
return ngx.exit(401)
|
||
end
|
||
|
||
-- 检查过期时间
|
||
if payload.exp then
|
||
if ngx.now() >= payload.exp then
|
||
ngx.status = 401
|
||
ngx.header["Content-Type"] = "application/json"
|
||
ngx.say(cjson.encode({
|
||
error = "token_expired",
|
||
message = "JWT has expired",
|
||
}))
|
||
return ngx.exit(401)
|
||
end
|
||
end
|
||
|
||
-- 验证通过,将解析后的信息存入 ngx.ctx 供后续阶段使用
|
||
ngx.ctx.jwt_payload = payload
|