|
@@ -0,0 +1,142 @@
|
|
|
|
|
+-- /usr/local/openresty/nginx/conf.d/jwt-auth.lua
|
|
|
|
|
+-- JWT 认证服务 - Access Token 验证脚本(修正版)
|
|
|
|
|
+-- 功能:
|
|
|
|
|
+-- 1. 验证 JWT Token 的合法性(签名、过期时间)
|
|
|
|
|
+-- 2. 提取用户信息并传递给后端服务
|
|
|
|
|
+-- 注意:
|
|
|
|
|
+-- - 所有时间使用北京时间(UTC+8)
|
|
|
|
|
+-- - 需在 nginx.conf 中配置 jwt_secret 变量
|
|
|
|
|
+-- - Token 格式:Authorization: Bearer <token>
|
|
|
|
|
+
|
|
|
|
|
+local jwt = require "resty.jwt"
|
|
|
|
|
+local cjson = require "cjson"
|
|
|
|
|
+
|
|
|
|
|
+-- ============================================================
|
|
|
|
|
+-- 1. 配置参数(从 Nginx 变量中读取)
|
|
|
|
|
+-- ============================================================
|
|
|
|
|
+local jwt_secret = ngx.var.jwt_secret
|
|
|
|
|
+local jwt_algorithm = ngx.var.jwt_algorithm or "HS256"
|
|
|
|
|
+
|
|
|
|
|
+-- 检查密钥是否配置(避免空密钥导致验证失效)
|
|
|
|
|
+if not jwt_secret or jwt_secret == "" then
|
|
|
|
|
+ ngx.log(ngx.ERR, "[JWT Auth] JWT secret not configured (empty or missing)")
|
|
|
|
|
+ ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR
|
|
|
|
|
+ ngx.header["Content-Type"] = "application/json; charset=utf-8"
|
|
|
|
|
+ ngx.say(cjson.encode({
|
|
|
|
|
+ detail = "JWT 配置错误:未设置有效的密钥(jwt_secret)"
|
|
|
|
|
+ }))
|
|
|
|
|
+ return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+-- ============================================================
|
|
|
|
|
+-- 2. 获取并验证 Authorization 请求头
|
|
|
|
|
+-- ============================================================
|
|
|
|
|
+local auth_header = ngx.var.http_Authorization
|
|
|
|
|
+if not auth_header or auth_header == "" then
|
|
|
|
|
+ ngx.log(ngx.WARN, "[JWT Auth] Missing Authorization header from IP: ", ngx.var.remote_addr)
|
|
|
|
|
+ ngx.status = ngx.HTTP_UNAUTHORIZED
|
|
|
|
|
+ ngx.header["Content-Type"] = "application/json; charset=utf-8"
|
|
|
|
|
+ ngx.say(cjson.encode({
|
|
|
|
|
+ detail = "缺少 Authorization 请求头,请按格式传递:Bearer <token>"
|
|
|
|
|
+ }))
|
|
|
|
|
+ return ngx.exit(ngx.HTTP_UNAUTHORIZED)
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+-- 提取 Bearer Token(兼容大小写和空格格式:"bearer token" 或 "Bearer token")
|
|
|
|
|
+local token = string.match(auth_header, "^[Bb][Ee][Aa][Rr][Ee][Rr]%s+(.+)$")
|
|
|
|
|
+if not token then
|
|
|
|
|
+ ngx.log(ngx.WARN, "[JWT Auth] Invalid Authorization format from IP: ", ngx.var.remote_addr)
|
|
|
|
|
+ ngx.status = ngx.HTTP_UNAUTHORIZED
|
|
|
|
|
+ ngx.header["Content-Type"] = "application/json; charset=utf-8"
|
|
|
|
|
+ ngx.say(cjson.encode({
|
|
|
|
|
+ detail = "Authorization 格式错误,正确格式:Authorization: Bearer <你的Token>"
|
|
|
|
|
+ }))
|
|
|
|
|
+ return ngx.exit(ngx.HTTP_UNAUTHORIZED)
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+-- ============================================================
|
|
|
|
|
+-- 3. 验证 JWT Token 核心合法性(签名 + 过期时间)
|
|
|
|
|
+-- ============================================================
|
|
|
|
|
+-- 仅验证签名和标准声明(exp/iat 等),不强制 Token 类型
|
|
|
|
|
+local jwt_obj = jwt:verify(jwt_secret, token)
|
|
|
|
|
+
|
|
|
|
|
+-- 检查验证结果是否为空(极端异常情况)
|
|
|
|
|
+if not jwt_obj then
|
|
|
|
|
+ ngx.log(ngx.ERR, "[JWT Auth] JWT verify failed: jwt_obj is nil (IP: ", ngx.var.remote_addr, ")")
|
|
|
|
|
+ ngx.status = ngx.HTTP_UNAUTHORIZED
|
|
|
|
|
+ ngx.header["Content-Type"] = "application/json; charset=utf-8"
|
|
|
|
|
+ ngx.say(cjson.encode({
|
|
|
|
|
+ detail = "Token 验证失败:无效的 Token 格式"
|
|
|
|
|
+ }))
|
|
|
|
|
+ return ngx.exit(ngx.HTTP_UNAUTHORIZED)
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+-- 【安全关键】验证签名是否通过
|
|
|
|
|
+if jwt_obj.verified == false then
|
|
|
|
|
+ ngx.log(ngx.WARN, "[JWT Auth] JWT signature verification failed (IP: ", ngx.var.remote_addr, ")")
|
|
|
|
|
+ ngx.status = ngx.HTTP_UNAUTHORIZED
|
|
|
|
|
+ ngx.header["Content-Type"] = "application/json; charset=utf-8"
|
|
|
|
|
+ ngx.say(cjson.encode({
|
|
|
|
|
+ detail = "Token 签名验证失败:可能是密钥不匹配或 Token 被篡改"
|
|
|
|
|
+ }))
|
|
|
|
|
+ return ngx.exit(ngx.HTTP_UNAUTHORIZED)
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+-- 验证 Token 是否有效(包含过期时间 exp 校验)
|
|
|
|
|
+if not jwt_obj.valid then
|
|
|
|
|
+ local reason = jwt_obj.reason or "未知原因"
|
|
|
|
|
+ ngx.log(ngx.WARN, "[JWT Auth] Invalid JWT from IP: ", ngx.var.remote_addr, ", Reason: ", reason)
|
|
|
|
|
+ ngx.status = ngx.HTTP_UNAUTHORIZED
|
|
|
|
|
+ ngx.header["Content-Type"] = "application/json; charset=utf-8"
|
|
|
|
|
+
|
|
|
|
|
+ -- 根据错误原因返回友好提示
|
|
|
|
|
+ local error_msg = "Token 无效"
|
|
|
|
|
+ if reason == "exp" or reason == "expiry" then
|
|
|
|
|
+ error_msg = "Token 已过期,请重新获取新 Token"
|
|
|
|
|
+ elseif reason == "signature" then
|
|
|
|
|
+ error_msg = "Token 签名验证失败:密钥不匹配或 Token 被篡改"
|
|
|
|
|
+ elseif reason == "iat" then
|
|
|
|
|
+ error_msg = "Token 签发时间异常(iat 字段无效)"
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ ngx.say(cjson.encode({
|
|
|
|
|
+ detail = error_msg
|
|
|
|
|
+ }))
|
|
|
|
|
+ return ngx.exit(ngx.HTTP_UNAUTHORIZED)
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+-- ============================================================
|
|
|
|
|
+-- 4. 提取用户信息并传递给后端(通过 Nginx 变量)
|
|
|
|
|
+-- ============================================================
|
|
|
|
|
+local payload = jwt_obj.payload
|
|
|
|
|
+if not payload then
|
|
|
|
|
+ ngx.log(ngx.ERR, "[JWT Auth] JWT payload is nil (IP: ", ngx.var.remote_addr, ")")
|
|
|
|
|
+ ngx.status = ngx.HTTP_UNAUTHORIZED
|
|
|
|
|
+ ngx.header["Content-Type"] = "application/json; charset=utf-8"
|
|
|
|
|
+ ngx.say(cjson.encode({
|
|
|
|
|
+ detail = "Token 无效:缺少 payload 数据(用户信息字段)"
|
|
|
|
|
+ }))
|
|
|
|
|
+ return ngx.exit(ngx.HTTP_UNAUTHORIZED)
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+-- 提取 Token 中的用户信息(字段需与 Token 生成时的 payload 对应)
|
|
|
|
|
+local accountID = payload.accountID or ""
|
|
|
|
|
+local name = payload.name or ""
|
|
|
|
|
+local userCode = payload.userCode or ""
|
|
|
|
|
+local contactNumber = payload.contactNumber or ""
|
|
|
|
|
+local jti = payload.jti or ""
|
|
|
|
|
+
|
|
|
|
|
+-- 设置 Nginx 变量(后端可通过请求头 X-User-XXX 获取这些信息)
|
|
|
|
|
+ngx.var.user_accountID = accountID
|
|
|
|
|
+ngx.var.user_name = name
|
|
|
|
|
+ngx.var.user_userCode = userCode
|
|
|
|
|
+ngx.var.user_contactNumber = contactNumber
|
|
|
|
|
+ngx.var.user_jti = jti
|
|
|
|
|
+
|
|
|
|
|
+-- 【可选】调试日志:验证通过时记录用户信息(生产环境可注释,避免敏感信息泄露)
|
|
|
|
|
+-- ngx.log(ngx.INFO, "[JWT Auth] User authenticated: accountID=", accountID, ", userCode=", userCode, " (IP: ", ngx.var.remote_addr, ")")
|
|
|
|
|
+
|
|
|
|
|
+-- ============================================================
|
|
|
|
|
+-- 5. 验证通过,放行请求
|
|
|
|
|
+-- ============================================================
|
|
|
|
|
+-- 脚本执行完毕后,Nginx 会继续执行后续指令(如 proxy_pass 转发到后端服务)
|