lolly/docs/lua/authentication/jwt_validate.lua
xfy 6543422281 docs: 添加 Nginx 配置和 Lua 脚本示例文档
- config: 反向代理、缓存、负载均衡、安全、SSL 等配置模板
- lua: API 网关、认证、动态路由、限流、WebSocket 等脚本示例

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 17:59:22 +08:00

164 lines
4.6 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 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