jwt-auth.lua 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. -- /usr/local/openresty/nginx/conf.d/jwt-auth.lua
  2. -- JWT 认证服务 - Access Token 验证脚本(修正版)
  3. -- 功能:
  4. -- 1. 验证 JWT Token 的合法性(签名、过期时间)
  5. -- 2. 提取用户信息并传递给后端服务
  6. -- 注意:
  7. -- - 所有时间使用北京时间(UTC+8)
  8. -- - 需在 nginx.conf 中配置 jwt_secret 变量
  9. -- - Token 格式:Authorization: Bearer <token>
  10. local jwt = require "resty.jwt"
  11. local cjson = require "cjson"
  12. -- ============================================================
  13. -- 1. 配置参数(从 Nginx 变量中读取)
  14. -- ============================================================
  15. local jwt_secret = ngx.var.jwt_secret
  16. local jwt_algorithm = ngx.var.jwt_algorithm or "HS256"
  17. -- 检查密钥是否配置(避免空密钥导致验证失效)
  18. if not jwt_secret or jwt_secret == "" then
  19. ngx.log(ngx.ERR, "[JWT Auth] JWT secret not configured (empty or missing)")
  20. ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR
  21. ngx.header["Content-Type"] = "application/json; charset=utf-8"
  22. ngx.say(cjson.encode({
  23. detail = "JWT 配置错误:未设置有效的密钥(jwt_secret)"
  24. }))
  25. return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
  26. end
  27. -- ============================================================
  28. -- 2. 获取并验证 Authorization 请求头
  29. -- ============================================================
  30. local auth_header = ngx.var.http_Authorization
  31. if not auth_header or auth_header == "" then
  32. ngx.log(ngx.WARN, "[JWT Auth] Missing Authorization header from IP: ", ngx.var.remote_addr)
  33. ngx.status = ngx.HTTP_UNAUTHORIZED
  34. ngx.header["Content-Type"] = "application/json; charset=utf-8"
  35. ngx.say(cjson.encode({
  36. detail = "缺少 Authorization 请求头,请按格式传递:Bearer <token>"
  37. }))
  38. return ngx.exit(ngx.HTTP_UNAUTHORIZED)
  39. end
  40. -- 提取 Bearer Token(兼容大小写和空格格式:"bearer token" 或 "Bearer token")
  41. local token = string.match(auth_header, "^[Bb][Ee][Aa][Rr][Ee][Rr]%s+(.+)$")
  42. if not token then
  43. ngx.log(ngx.WARN, "[JWT Auth] Invalid Authorization format from IP: ", ngx.var.remote_addr)
  44. ngx.status = ngx.HTTP_UNAUTHORIZED
  45. ngx.header["Content-Type"] = "application/json; charset=utf-8"
  46. ngx.say(cjson.encode({
  47. detail = "Authorization 格式错误,正确格式:Authorization: Bearer <你的Token>"
  48. }))
  49. return ngx.exit(ngx.HTTP_UNAUTHORIZED)
  50. end
  51. -- ============================================================
  52. -- 3. 验证 JWT Token 核心合法性(签名 + 过期时间)
  53. -- ============================================================
  54. -- 仅验证签名和标准声明(exp/iat 等),不强制 Token 类型
  55. local jwt_obj = jwt:verify(jwt_secret, token)
  56. -- 检查验证结果是否为空(极端异常情况)
  57. if not jwt_obj then
  58. ngx.log(ngx.ERR, "[JWT Auth] JWT verify failed: jwt_obj is nil (IP: ", ngx.var.remote_addr, ")")
  59. ngx.status = ngx.HTTP_UNAUTHORIZED
  60. ngx.header["Content-Type"] = "application/json; charset=utf-8"
  61. ngx.say(cjson.encode({
  62. detail = "Token 验证失败:无效的 Token 格式"
  63. }))
  64. return ngx.exit(ngx.HTTP_UNAUTHORIZED)
  65. end
  66. -- 【安全关键】验证签名是否通过
  67. if jwt_obj.verified == false then
  68. ngx.log(ngx.WARN, "[JWT Auth] JWT signature verification failed (IP: ", ngx.var.remote_addr, ")")
  69. ngx.status = ngx.HTTP_UNAUTHORIZED
  70. ngx.header["Content-Type"] = "application/json; charset=utf-8"
  71. ngx.say(cjson.encode({
  72. detail = "Token 签名验证失败:可能是密钥不匹配或 Token 被篡改"
  73. }))
  74. return ngx.exit(ngx.HTTP_UNAUTHORIZED)
  75. end
  76. -- 验证 Token 是否有效(包含过期时间 exp 校验)
  77. if not jwt_obj.valid then
  78. local reason = jwt_obj.reason or "未知原因"
  79. ngx.log(ngx.WARN, "[JWT Auth] Invalid JWT from IP: ", ngx.var.remote_addr, ", Reason: ", reason)
  80. ngx.status = ngx.HTTP_UNAUTHORIZED
  81. ngx.header["Content-Type"] = "application/json; charset=utf-8"
  82. -- 根据错误原因返回友好提示
  83. local error_msg = "Token 无效"
  84. if reason == "exp" or reason == "expiry" then
  85. error_msg = "Token 已过期,请重新获取新 Token"
  86. elseif reason == "signature" then
  87. error_msg = "Token 签名验证失败:密钥不匹配或 Token 被篡改"
  88. elseif reason == "iat" then
  89. error_msg = "Token 签发时间异常(iat 字段无效)"
  90. end
  91. ngx.say(cjson.encode({
  92. detail = error_msg
  93. }))
  94. return ngx.exit(ngx.HTTP_UNAUTHORIZED)
  95. end
  96. -- ============================================================
  97. -- 4. 提取用户信息并传递给后端(通过 Nginx 变量)
  98. -- ============================================================
  99. local payload = jwt_obj.payload
  100. if not payload then
  101. ngx.log(ngx.ERR, "[JWT Auth] JWT payload is nil (IP: ", ngx.var.remote_addr, ")")
  102. ngx.status = ngx.HTTP_UNAUTHORIZED
  103. ngx.header["Content-Type"] = "application/json; charset=utf-8"
  104. ngx.say(cjson.encode({
  105. detail = "Token 无效:缺少 payload 数据(用户信息字段)"
  106. }))
  107. return ngx.exit(ngx.HTTP_UNAUTHORIZED)
  108. end
  109. -- 提取 Token 中的用户信息(字段需与 Token 生成时的 payload 对应)
  110. local accountID = payload.accountID or ""
  111. local name = payload.name or ""
  112. local userCode = payload.userCode or ""
  113. local contactNumber = payload.contactNumber or ""
  114. local jti = payload.jti or ""
  115. -- 设置 Nginx 变量(后端可通过请求头 X-User-XXX 获取这些信息)
  116. ngx.var.user_accountID = accountID
  117. ngx.var.user_name = name
  118. ngx.var.user_userCode = userCode
  119. ngx.var.user_contactNumber = contactNumber
  120. ngx.var.user_jti = jti
  121. -- 【可选】调试日志:验证通过时记录用户信息(生产环境可注释,避免敏感信息泄露)
  122. -- ngx.log(ngx.INFO, "[JWT Auth] User authenticated: accountID=", accountID, ", userCode=", userCode, " (IP: ", ngx.var.remote_addr, ")")
  123. -- ============================================================
  124. -- 5. 验证通过,放行请求
  125. -- ============================================================
  126. -- 脚本执行完毕后,Nginx 会继续执行后续指令(如 proxy_pass 转发到后端服务)