Jelajahi Sumber

Update优化

XieXing 3 bulan lalu
induk
melakukan
40d09b21ea
2 mengubah file dengan 145 tambahan dan 3 penghapusan
  1. 142 0
      jwt-auth.lua
  2. 3 3
      shudao-vue-frontend/src/utils/api.js

+ 142 - 0
jwt-auth.lua

@@ -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 转发到后端服务)

+ 3 - 3
shudao-vue-frontend/src/utils/api.js

@@ -3,7 +3,7 @@
  * 统一管理所有API请求
  */
 import request from './authRequest'
-import { buildApiUrl } from './apiConfig'
+import { buildApiUrl, REPORT_API_PREFIX } from './apiConfig'
 
 /**
  * 用户登录
@@ -80,7 +80,7 @@ export function getLoginLogs(params = {}) {
  */
 export async function stopSSEStream(userId, aiConversationId) {
   try {
-    const response = await fetch(buildApiUrl('/sse/stop'), {
+    const response = await fetch(buildApiUrl('/sse/stop', REPORT_API_PREFIX), {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json'
@@ -139,7 +139,7 @@ export async function stopSSEStream(userId, aiConversationId) {
  */
 export async function updateAIMessageContent(aiMessageId, content) {
   try {
-    const response = await fetch(buildApiUrl('/report/update-ai-message'), {
+    const response = await fetch(buildApiUrl('/report/update-ai-message', REPORT_API_PREFIX), {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json'