OpenResty 高性能网关开发教程 / 第 07 章 - 认证与鉴权
第 07 章 - 认证与鉴权
7.1 认证 vs 鉴权
认证(Authentication)─→ "你是谁?"
验证请求者的身份
方式:JWT、API Key、OAuth2、mTLS
鉴权(Authorization)─→ "你能做什么?"
验证请求者的权限
方式:RBAC、ACL、Policy
7.2 JWT 认证
JWT(JSON Web Token)是目前最流行的无状态认证方案。
JWT 结构
Header.Payload.Signature
Header: {"alg":"HS256","typ":"JWT"} → Base64URL 编码
Payload: {"sub":"user123","exp":1704067200} → Base64URL 编码
Signature: HMACSHA256(header + "." + payload, secret)
7.2.1 安装 lua-resty-jwt
# 使用 OPM 安装
sudo opm get SkyLothar/lua-resty-jwt
# 或手动下载
cd /usr/local/openresty/lualib
git clone https://github.com/SkyLothar/lua-resty-jwt.git resty/jwt
git clone https://github.com/SkyLothar/lua-resty-string.git resty/string
7.2.2 JWT 认证模块
-- /usr/local/openresty/lua/auth/jwt_auth.lua
local _M = {}
local jwt = require "resty.jwt"
local cjson = require "cjson"
-- 配置
local config = {
secret = os.getenv("JWT_SECRET") or "your-256-bit-secret-key-change-in-production",
algorithm = "HS256",
-- Token 来源
token_header = "Authorization",
token_prefix = "Bearer ",
-- Cookie 备选
token_cookie = "access_token",
-- 白名单路径(不需要认证)
whitelist = {
"/api/health",
"/api/version",
"/api/auth/login",
"/api/auth/register",
},
}
-- 检查路径是否在白名单中
local function is_whitelisted(uri)
for _, path in ipairs(config.whitelist) do
if uri == path or uri:match("^" .. path:gsub("%-", "%%-") .. "/") then
return true
end
end
return false
end
-- 从请求中提取 Token
local function extract_token()
-- 方式 1:Authorization Header
local auth_header = ngx.req.get_headers()[config.token_header]
if auth_header then
local token = auth_header:match("^" .. config.token_prefix .. "(.+)$")
if token then
return token
end
end
-- 方式 2:Cookie
local cookie_token = ngx.var["cookie_" .. config.token_cookie]
if cookie_token then
return cookie_token
end
-- 方式 3:查询参数
local args = ngx.req.get_uri_args()
if args.access_token then
return args.access_token
end
return nil
end
-- 验证 JWT
function _M.authenticate()
local uri = ngx.var.uri
-- 白名单检查
if is_whitelisted(uri) then
return true
end
-- 提取 Token
local token = extract_token()
if not token then
ngx.status = 401
ngx.header["Content-Type"] = "application/json"
ngx.say(cjson.encode({
error = "Unauthorized",
message = "Missing authentication token",
}))
return false
end
-- 尝试从缓存获取
local cache = ngx.shared.jwt_cache
local cached = cache:get(token)
if cached then
local payload = cjson.decode(cached)
ngx.var.user_id = payload.sub or ""
ngx.var.user_role = payload.role or "user"
return true
end
-- 验证 JWT
local jwt_obj = jwt:verify(config.secret, token)
if not jwt_obj.verified then
ngx.log(ngx.WARN, "JWT verification failed: ", jwt_obj.reason)
ngx.status = 401
ngx.header["Content-Type"] = "application/json"
ngx.say(cjson.encode({
error = "Unauthorized",
message = "Invalid or expired token",
reason = jwt_obj.reason,
}))
return false
end
-- 检查过期时间(jwt:verify 已自动检查,此处双重保险)
local payload = jwt_obj.payload
if payload.exp and payload.exp < ngx.time() then
ngx.status = 401
ngx.say(cjson.encode({error = "Token expired"}))
return false
end
-- 缓存验证结果(Token 未过期前有效)
local ttl = 300 -- 默认缓存 5 分钟
if payload.exp then
ttl = math.min(ttl, payload.exp - ngx.time())
end
if ttl > 0 then
cache:set(token, cjson.encode(payload), ttl)
end
-- 设置用户信息到变量
ngx.var.user_id = payload.sub or ""
ngx.var.user_role = payload.role or "user"
-- 将用户信息传递给后端
ngx.req.set_header("X-User-ID", payload.sub or "")
ngx.req.set_header("X-User-Role", payload.role or "user")
ngx.req.set_header("X-User-Email", payload.email or "")
return true
end
-- 生成 JWT Token(用于登录接口)
function _M.generate_token(user_data, expires_in)
expires_in = expires_in or 7200 -- 默认 2 小时
local payload = {
sub = user_data.id,
email = user_data.email,
role = user_data.role or "user",
iat = ngx.time(),
exp = ngx.time() + expires_in,
iss = "openresty-gateway",
}
local token = jwt:sign(config.secret, {
header = {typ = "JWT", alg = config.algorithm},
payload = payload,
})
return token, payload
end
-- 刷新 Token
function _M.refresh_token(current_token)
local jwt_obj = jwt:verify(config.secret, current_token)
if not jwt_obj.verified then
return nil, "Invalid token"
end
local payload = jwt_obj.payload
-- 检查是否在刷新窗口内(过期后 30 分钟内可刷新)
if payload.exp and (ngx.time() - payload.exp) > 1800 then
return nil, "Token expired too long ago"
end
-- 生成新 Token
return _M.generate_token({
id = payload.sub,
email = payload.email,
role = payload.role,
})
end
return _M
7.2.3 Nginx 配置
http {
lua_shared_dict jwt_cache 20m;
server {
listen 8080;
# 通用变量
set $user_id "";
set $user_role "";
# 认证中间件
access_by_lua_block {
local jwt_auth = require "auth.jwt_auth"
if not jwt_auth.authenticate() then
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
}
# 登录接口(不需要认证,在白名单中)
location /api/auth/login {
content_by_lua_block {
local jwt_auth = require "auth.jwt_auth"
local cjson = require "cjson"
ngx.req.read_body()
local body = cjson.decode(ngx.req.get_body_data())
-- 验证用户名密码(示例)
if body.username == "admin" and body.password == "password" then
local token, payload = jwt_auth.generate_token({
id = "user_001",
email = "[email protected]",
role = "admin",
})
ngx.say(cjson.encode({
access_token = token,
token_type = "Bearer",
expires_in = 7200,
}))
else
ngx.status = 401
ngx.say(cjson.encode({error = "Invalid credentials"}))
end
}
}
# 受保护的 API
location /api/users {
proxy_pass http://user_backend;
}
}
}
7.3 API Key 认证
适合机器对机器(M2M)通信场景。
-- /usr/local/openresty/lua/auth/apikey_auth.lua
local _M = {}
local cjson = require "cjson"
-- API Key 存储(生产环境使用 Redis/数据库)
local api_keys = {
["ak_prod_1234567890"] = {
name = "Production Service",
rate_limit = 1000,
permissions = {"read", "write"},
enabled = true,
},
["ak_test_0987654321"] = {
name = "Test Service",
rate_limit = 100,
permissions = {"read"},
enabled = true,
},
}
-- 从请求中提取 API Key
local function extract_api_key()
-- 方式 1:Header
local key = ngx.req.get_headers()["X-API-Key"]
if key then return key end
-- 方式 2:查询参数
key = ngx.req.get_uri_args().api_key
if key then return key end
-- 方式 3:Authorization (ApiKey scheme)
local auth = ngx.req.get_headers()["Authorization"]
if auth then
key = auth:match("^ApiKey%s+(.+)$")
if key then return key end
end
return nil
end
function _M.authenticate()
local key = extract_api_key()
if not key then
ngx.status = 401
ngx.header["Content-Type"] = "application/json"
ngx.say(cjson.encode({
error = "Unauthorized",
message = "API key required. Send via X-API-Key header or api_key query parameter.",
}))
return false
end
-- 从缓存查找
local cache = ngx.shared.gateway_config
local cached = cache:get("apikey:" .. key)
local key_info
if cached then
key_info = cjson.decode(cached)
else
key_info = api_keys[key]
if key_info then
cache:set("apikey:" .. key, cjson.encode(key_info), 300)
end
end
if not key_info then
ngx.status = 401
ngx.say(cjson.encode({
error = "Unauthorized",
message = "Invalid API key",
}))
return false
end
if not key_info.enabled then
ngx.status = 403
ngx.say(cjson.encode({
error = "Forbidden",
message = "API key is disabled",
}))
return false
end
-- 设置请求上下文
ngx.var.user_id = key_info.name
ngx.var.user_role = "api"
-- 传递给后端
ngx.req.set_header("X-API-Key-Name", key_info.name)
ngx.req.set_header("X-API-Key-Permissions", cjson.encode(key_info.permissions))
return true
end
return _M
7.4 OAuth2 认证
OAuth2 用于第三方应用接入,常见授权模式:
| 模式 | 适用场景 | 安全性 |
|---|---|---|
| Authorization Code | Web 应用 | 高 |
| Client Credentials | 服务间通信 | 高 |
| Implicit | SPA(已不推荐) | 低 |
| Resource Owner Password | 受信任客户端 | 中 |
OAuth2 Token 验证
-- /usr/local/openresty/lua/auth/oauth2_auth.lua
local _M = {}
local http = require "resty.http"
local cjson = require "cjson"
local config = {
-- OAuth2 Provider 信息
introspect_url = "https://auth.example.com/oauth2/introspect",
client_id = "gateway-client",
client_secret = "gateway-secret",
-- 本地缓存
cache_ttl = 60,
}
-- Token 内省(Introspection)
local function introspect_token(token)
local httpc = http.new()
httpc:set_timeout(5000)
local res, err = httpc:request_uri(config.introspect_url, {
method = "POST",
body = "token=" .. token .. "&token_type_hint=access_token",
headers = {
["Content-Type"] = "application/x-www-form-urlencoded",
["Authorization"] = "Basic " .. ngx.encode_base64(
config.client_id .. ":" .. config.client_secret
),
},
})
if not res then
return nil, "introspection request failed: " .. err
end
if res.status ~= 200 then
return nil, "introspection returned status: " .. res.status
end
local result = cjson.decode(res.body)
if not result.active then
return nil, "token is not active"
end
return result
end
function _M.authenticate()
local auth_header = ngx.req.get_headers()["Authorization"]
if not auth_header then
ngx.status = 401
ngx.header["WWW-Authenticate"] = 'Bearer realm="api"'
ngx.say(cjson.encode({error = "Bearer token required"}))
return false
end
local token = auth_header:match("^Bearer%s+(.+)$")
if not token then
ngx.status = 401
ngx.say(cjson.encode({error = "Invalid authorization format"}))
return false
end
-- 检查缓存
local cache = ngx.shared.jwt_cache
local cached = cache:get("oauth2:" .. token)
if cached then
local info = cjson.decode(cached)
ngx.var.user_id = info.sub or ""
ngx.var.user_role = info.role or ""
return true
end
-- 内省 Token
local info, err = introspect_token(token)
if not info then
ngx.status = 401
ngx.say(cjson.encode({error = "Invalid token", message = err}))
return false
end
-- 缓存结果
cache:set("oauth2:" .. token, cjson.encode(info), config.cache_ttl)
-- 设置用户信息
ngx.var.user_id = info.sub or info.client_id or ""
ngx.var.user_role = info.role or ""
ngx.req.set_header("X-User-ID", info.sub or "")
ngx.req.set_header("X-Client-ID", info.client_id or "")
ngx.req.set_header("X-Token-Scope", info.scope or "")
return true
end
return _M
7.5 mTLS 双向认证
mTLS(Mutual TLS)用于高安全性场景,客户端和服务端互相验证证书。
server {
listen 8443 ssl;
# 服务端证书
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
# 客户端证书验证
ssl_client_certificate /etc/ssl/ca.crt;
ssl_verify_client on; # 强制要求客户端证书
ssl_verify_depth 2;
# CRL 检查(可选)
ssl_crl /etc/ssl/ca.crl;
location /api/ {
content_by_lua_block {
-- 获取客户端证书信息
local client_cert = ngx.var.ssl_client_s_dn
local client_serial = ngx.var.ssl_client_serial
local client_verify = ngx.var.ssl_client_verify
if client_verify ~= "SUCCESS" then
ngx.status = 403
ngx.say('{"error":"Client certificate verification failed"}')
return
end
-- 提取证书中的 CN 作为客户端标识
local client_cn = client_cert:match("CN=([^/,]+)")
ngx.req.set_header("X-Client-CN", client_cn)
ngx.req.set_header("X-Client-Serial", client_serial)
ngx.say('{"client":"' .. client_cn .. '","verified":true}')
}
}
}
7.6 RBAC 权限模型
Role-Based Access Control(基于角色的访问控制)是最常用的鉴权模型。
-- /usr/local/openresty/lua/auth/rbac.lua
local _M = {}
-- 角色权限配置
local roles = {
admin = {
permissions = {"*"}, -- 所有权限
},
editor = {
permissions = {
"users:read",
"users:write",
"posts:read",
"posts:write",
"posts:delete",
},
},
viewer = {
permissions = {
"users:read",
"posts:read",
},
},
api = {
permissions = {
"api:read",
"api:write",
},
},
}
-- 资源-操作 映射
local resource_actions = {
["GET:/api/users"] = "users:read",
["POST:/api/users"] = "users:write",
["PUT:/api/users"] = "users:write",
["DELETE:/api/users"] = "users:delete",
["GET:/api/posts"] = "posts:read",
["POST:/api/posts"] = "posts:write",
["DELETE:/api/posts"] = "posts:delete",
}
-- 检查权限
function _M.check_permission(user_role, method, uri)
local role_config = roles[user_role]
if not role_config then
return false, "Unknown role: " .. user_role
end
-- 超级权限
for _, perm in ipairs(role_config.permissions) do
if perm == "*" then
return true
end
end
-- 构建所需权限
local required = method .. ":" .. uri
-- 尝试匹配资源级权限
for pattern, permission in pairs(resource_actions) do
local p_method, p_path = pattern:match("^([^:]+):(.+)$")
if method == p_method and uri:match("^" .. p_path) then
required = permission
break
end
end
-- 检查角色是否拥有权限
for _, perm in ipairs(role_config.permissions) do
if perm == required then
return true
end
end
return false, "Permission denied: " .. required
end
-- 中间件:鉴权检查
function _M.authorize()
local user_role = ngx.var.user_role
local method = ngx.req.get_method()
local uri = ngx.var.uri
-- 白名单路径跳过鉴权
if uri:match("^/api/auth/") or uri == "/api/health" then
return true
end
local ok, err = _M.check_permission(user_role, method, uri)
if not ok then
ngx.status = 403
ngx.header["Content-Type"] = "application/json"
ngx.say('{"error":"Forbidden","message":"' .. err .. '"}')
return false
end
return true
end
return _M
nginx 配置:认证 + 鉴权
server {
listen 8080;
set $user_id "";
set $user_role "";
access_by_lua_block {
-- 步骤 1:认证
local jwt_auth = require "auth.jwt_auth"
if not jwt_auth.authenticate() then
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 步骤 2:鉴权
local rbac = require "auth.rbac"
if not rbac.authorize() then
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
}
location /api/users {
proxy_pass http://user_backend;
}
location /api/admin {
# 需要 admin 角色
access_by_lua_block {
local jwt_auth = require "auth.jwt_auth"
if not jwt_auth.authenticate() then
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
if ngx.var.user_role ~= "admin" then
ngx.status = 403
ngx.say('{"error":"Admin access required"}')
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
}
proxy_pass http://admin_backend;
}
}
7.7 多种认证方式并存
-- /usr/local/openresty/lua/auth/multi_auth.lua
local _M = {}
local auth_strategies = {
{
name = "jwt",
detect = function()
local auth = ngx.req.get_headers()["Authorization"]
return auth and auth:match("^Bearer%s+")
end,
handler = function()
local jwt = require "auth.jwt_auth"
return jwt.authenticate()
end,
},
{
name = "api_key",
detect = function()
return ngx.req.get_headers()["X-API-Key"]
end,
handler = function()
local apikey = require "auth.apikey_auth"
return apikey.authenticate()
end,
},
{
name = "oauth2",
detect = function()
local auth = ngx.req.get_headers()["Authorization"]
return auth and auth:match("^Bearer%s+") and true or false
end,
handler = function()
local oauth2 = require "auth.oauth2_auth"
return oauth2.authenticate()
end,
},
}
-- 尝试所有认证策略
function _M.authenticate()
for _, strategy in ipairs(auth_strategies) do
if strategy.detect() then
ngx.log(ngx.INFO, "Auth strategy detected: ", strategy.name)
return strategy.handler()
end
end
-- 无认证信息
ngx.status = 401
ngx.header["WWW-Authenticate"] = 'Bearer realm="api", ApiKey realm="api"'
ngx.say('{"error":"Authentication required"}')
return false
end
return _M
7.8 注意事项
Token 安全:JWT Secret 必须足够长(256 位以上),不要硬编码在代码中,使用环境变量或密钥管理服务。
Token 刷新:Access Token 有效期不宜过长(建议 2 小时),配合 Refresh Token 实现无感刷新。
缓存一致性:Token 验证结果缓存需要考虑 Token 吊销场景,使用短 TTL 或黑名单机制。
mTLS 性能:TLS 握手开销较大,适合内部服务间通信,不适合面向公网的 API。
上一章:← 第 06 章 - 限流与流控 下一章:第 08 章 - 反向代理与负载均衡 →