前置環境:
- 在微信小程序中嵌入H5頁面(智能客服),需要讓h5頁面在https的域名服務器上。即通過 nginx 部署成web服務,還得配置域名和端口443訪問。
- 電信的第三方deepseek服務 ,只接收http請求,暫未支持https請求。
- 所以我們要使用https的話 就需要在智文網關xxx:31170前面 自行加一個網關進行處理(https -> http)。
下面配置nginx 代理,及調試。主要麻煩在了我方與第三方簽名驗證不通過上 (postJsonWithAuth)。
1. 構建請求頭的方法
1.1
public class SignUtils 工具類
// public static final String PARAM_HEADER_DATE = "Date";public static final String PARAM_HEADER_X_DATE = "x-date";private static final List<String> SIGNED_HEADERS = new ArrayList<>();private static final String algorithm = "HmacSHA256";private static final String hmacAlgorithm = "hmac-sha256";static {SIGNED_HEADERS.add("x-tenantid");SIGNED_HEADERS.add("x-userid");SIGNED_HEADERS.add("x-source");}
1.2
private static Map<String, String> buildHeaders() {Map<String, String> headers = new HashMap<>();headers.put("Content-Type", "application/json");headers.put("x-userid", RequestParam.getUserId());headers.put("x-tenantid", RequestParam.getTenantId());headers.put("x-source", RequestParam.getSource());LocalDateTime utcTime = LocalDateTime.now(ZoneOffset.UTC);// 定義日期時間格式DateTimeFormatter formatter = DateTimeFormatter.ofPattern("E, dd MMM yyyy HH:mm:ss 'GMT'", Locale.ENGLISH);// 格式化時間并輸出String formattedUtcTime = utcTime.format(formatter);headers.put(SignUtils.PARAM_HEADER_X_DATE, formattedUtcTime);return headers;}
1.3
/**
API 常量類
**/
public class ApiConstants
// 對話接口: 智文-SSE對話接口public static final String CHAT_OPENAPI = "/ais/bot/openapi/dcc/sseDialog";// 智文-同步對話接口public static final String CHAT_OPENAPI_SYNC = "/ais/bot/openapi/dcc/dialog";// 數科官網本地https 轉發public static final String API_DIANXIN_PROXY = "/apidx";public static final String CHAT_OPENAPI_SYNC_PROXY = API_DIANXIN_PROXY + CHAT_OPENAPI_SYNC;
主要問題就忽略了 我們簽名時,參數uri 加了"/api" 前綴,通過nginx 代理轉發后,過濾了 /api 前綴,到達第三方后,對方用的原始 uri驗簽,所以總是不通過。
過程中 在ds這臺服務器上裝了 tcpdump 抓包工具,比對本地debug(不代理)成功的報文,和訪問https生產服務器 nginx代理失敗的報文
/*** @param api 第三方接口uri* @param reqBody post請求體參數*/public static String postJsonWithAuth(String api, String reqBody) throws Exception {
// String url = RequestParam.getHost() + api;// 通過nginx 代理轉發時,加上前綴 String url = RequestParam.getHost() + ApiConstants.API_DIANXIN_PROXY + api;long startTime = System.currentTimeMillis();try {Map<String, String> headers = buildHeaders();// 簽名時,與第三方保持一致,使用原始api不變 ,不使用代理后的apiString sign = SignUtils.generateAuth(RequestParam.getAccessKey(), RequestParam.getSecretKey(),"POST", api, headers, new HashMap<>(), reqBody);headers.put("Authorization", sign);// 請求開始監控log.info(">>>> HTTP請求開始 [URL: {},API: {}]", url, api);log.info(">>>> HTTP請求頭: {}", headers);log.info(">>>> HTTP請求參數 [Body: {}]", reqBody);RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), reqBody);Request request = new Request.Builder().url(url).headers(Headers.of(headers)).post(requestBody).build();try (Response response = httpClient.newCall(request).execute()) {String responseBody = response.body().string();log.info("<<<< 原始響應結果:[狀態碼: {}, 內容: {}]", response.code(), response);if (response.isSuccessful()) {return responseBody;} else {log.error("詳細錯誤響應: {}", responseBody);throw new IOException("HTTP請求失敗: " + response.code() + " - " + response.message());}}} catch (Exception e) {long cost = System.currentTimeMillis() - startTime;log.error("<<<< 請求異常 [耗時: {} ms, URL: {}, 錯誤: {}]", cost, url, e.getMessage(), e);throw e;} finally {long totalCost = System.currentTimeMillis() - startTime;log.info("==== 請求結束 [總耗時: {} ms] ====", totalCost);}}
測試方法:
@Test(timeOut = 60000)public void testSSEChat() throws Exception {String question="石家莊有什么好玩的";
// String agentCode = queryAgentCode();String agentCode = "agent1048486107377569792";String messageId = UUID.randomUUID().toString();String sessionId = UUID.randomUUID().toString();String userId = RequestParam.getUserId();MessageRequest messageRequest = new MessageRequest();messageRequest.setMessageId(messageId);messageRequest.setSessionId(sessionId);messageRequest.setMsgType("TEXT");messageRequest.setUserId(userId);messageRequest.setContent(question);messageRequest.setQuery(question);messageRequest.setAgentCode(agentCode);messageRequest.setTest(1);messageRequest.setChatType("chat");messageRequest.setRequestTime(System.currentTimeMillis());messageRequest.setEntry("default");// 添加extraData參數Map<String, Object> extraData = new HashMap<>();Map<String, Object> filterMap = new HashMap<>();filterMap.put("knowledgeFilters", Collections.singletonList(Collections.singletonMap("knowledgeBaseCode", "1050656046175358976")));extraData.put("filter", JsonUtil.toJSONString(filterMap));extraData.put("range", "all");messageRequest.setExtraData(extraData);CountDownLatch latch = new CountDownLatch(5);AtomicBoolean received = new AtomicBoolean(false);String resp = OkHttpUtils.postJsonWithAuth(ApiConstants.CHAT_OPENAPI_SYNC, JsonUtil.toJSONString(messageRequest));log.info("同步結果: {}", resp);Thread.sleep(10000); }
下面是Nginx配置:
# 添加詳細日志記錄
log_format proxy_debug '$remote_addr - $remote_user [$time_local] ''"$request" $status $body_bytes_sent ''"$http_referer" "$http_user_agent" ''Authorization: "$http_authorization" ''x-source: "$http_x_source" ''x-userid: "$http_x_userid" ''x-tenantid: "$http_x_tenantid" ''x-date: "$http_x_date" ''content-type: "$http_content_type" ''cookie_header: "$http_cookie" ''host_header: "$host"';server {listen 443 ssl;charset utf-8;server_name stdai.sjzwltszkj.com ;ssl_certificate /usr/local/nginx/conf/cert/cert.pem;ssl_certificate_key /usr/local/nginx/conf/cert/cert.key;ssl_session_cache shared:SSL:1m;ssl_session_timeout 5m;#charset koi8-r;#access_log logs/host.access.log main;# location / {location ~* \.txt$ {root /data/std/authentication;}location /ai/ {alias /www/wwwroot/static.ltkj.com/std_applet/dist/build/h5/ ;#root /www/wwwroot/static.ltkj.com/std_applet/h5/dist/;try_files $uri $uri/ /ai/index.html;index index.html index.htm;}location /static/ {alias /www/wwwroot/static.ltkj.com/std_applet/dist/build/h5/static/;expires 1y;add_header Cache-Control "public";}# API代理 關鍵配置location /apidx/ {# 1. 保持原始HTTP版本和連接行為proxy_http_version 1.1;proxy_set_header Connection "";# 2. 保持原始Host頭proxy_set_header Host $proxy_host;proxy_set_header x-forwarded-host $host;# 傳遞所有頭并保持原始順序proxy_pass_request_headers on;# 3. 禁用不必要的頭傳遞proxy_pass_header Server;proxy_pass_header Date;proxy_hide_header 'Access-Control-Allow-Origin';proxy_hide_header 'Access-Control-Allow-Methods';proxy_hide_header 'Access-Control-Allow-Headers';# 4. 精確傳遞鑒權相關頭proxy_set_header Authorization $http_authorization;proxy_set_header x-source $http_x_source;proxy_set_header x-userid $http_x_userid;proxy_set_header x-tenantid $http_x_tenantid;proxy_set_header x-date $http_x_date;# 5. 代理到后端服務器 https -> http. 過濾掉/api/ 后保持原始請求路徑proxy_pass http://222.223.xxx.xxx:xxx70/;# 超時設置proxy_connect_timeout 60s;proxy_send_timeout 60s;proxy_read_timeout 60s;# 禁用緩沖proxy_buffering off;# CORS配置add_header 'Access-Control-Allow-Origin' 'https://stdai.sjzwltszkj.com' always;add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With, X-CSRF-Token, x-source, x-userid, x-tenantid, x-date' always;add_header 'Access-Control-Allow-Credentials' 'true' always;add_header 'Access-Control-Expose-Headers' 'Authorization' always;# 處理OPTIONS預檢請求if ($request_method = 'OPTIONS') {add_header 'Access-Control-Allow-Origin' 'https://stdai.sjzwltszkj.com' always;add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,X-CSRF-Token,x-source,x-userid,x-tenantid,x-date' always;add_header 'Access-Control-Max-Age' 1728000 always;add_header 'Content-Type' 'text/plain; charset=utf-8' always;add_header 'Content-Type' 'text/plain; charset=utf-8' always;add_header 'Content-Length' 0 always;return 204;}}error_page 500 502 503 504 /50x.html;location = /50x.html {root html;}access_log /usr/local/nginx/logs/access.log proxy_debug ;}