From 4306862eced89fa1f0059b0b7d8332299948fc85 Mon Sep 17 00:00:00 2001 From: lapuda <577732344@qq.com> Date: Tue, 14 Jul 2026 21:13:57 +0800 Subject: [PATCH] feat: Add RequestUrlUtils for building base URLs and refactor OutdoorFairController to use it --- ruoyi-bussiness/pom.xml | 7 +- .../app/AppOutdoorFairController.java | 8 +- .../controller/cms/OutdoorFairController.java | 10 +- .../cms/OutdoorFairControllerTest.java | 65 +++++++ .../ruoyi/common/utils/RequestUrlUtils.java | 161 ++++++++++++++++++ 5 files changed, 237 insertions(+), 14 deletions(-) create mode 100644 ruoyi-bussiness/src/test/java/com/ruoyi/cms/controller/cms/OutdoorFairControllerTest.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/RequestUrlUtils.java diff --git a/ruoyi-bussiness/pom.xml b/ruoyi-bussiness/pom.xml index fb4b41d..dcf1b6e 100644 --- a/ruoyi-bussiness/pom.xml +++ b/ruoyi-bussiness/pom.xml @@ -116,6 +116,11 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 + + org.springframework.boot + spring-boot-starter-test + test + - \ No newline at end of file + diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppOutdoorFairController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppOutdoorFairController.java index 0290a7f..12d7e32 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppOutdoorFairController.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppOutdoorFairController.java @@ -23,6 +23,7 @@ import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.entity.Company; import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.RequestUrlUtils; import com.ruoyi.common.utils.StringUtils; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -387,12 +388,7 @@ public class AppOutdoorFairController extends BaseController private String buildBaseUrl(HttpServletRequest request) { - String baseUrl = request.getScheme() + "://" + request.getServerName(); - int port = request.getServerPort(); - if (port != 80 && port != 443) { - baseUrl += ":" + port; - } - return baseUrl + (request.getContextPath() == null ? "" : request.getContextPath()); + return RequestUrlUtils.buildBaseUrl(request); } } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java index 201a78a..5e86109 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java @@ -49,6 +49,7 @@ import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.RequestUrlUtils; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; @@ -994,13 +995,8 @@ public class OutdoorFairController extends BaseController private String buildOutdoorFairCompanyH5Url(HttpServletRequest request, Long fairId, Long companyId) { - String baseUrl = request.getScheme() + "://" + request.getServerName(); - int port = request.getServerPort(); - if (port != 80 && port != 443) { - baseUrl += ":" + port; - } - String contextPath = request.getContextPath() == null ? "" : request.getContextPath(); - return baseUrl + contextPath + "/h5/outdoor-fair/company.html?fairId=" + fairId + "&companyId=" + companyId; + return RequestUrlUtils.buildBaseUrl(request) + + "/h5/outdoor-fair/company.html?fairId=" + fairId + "&companyId=" + companyId; } private Map buildCompanyQrCodeData(HttpServletRequest request, Long fairId, Long companyId) diff --git a/ruoyi-bussiness/src/test/java/com/ruoyi/cms/controller/cms/OutdoorFairControllerTest.java b/ruoyi-bussiness/src/test/java/com/ruoyi/cms/controller/cms/OutdoorFairControllerTest.java new file mode 100644 index 0000000..dae688b --- /dev/null +++ b/ruoyi-bussiness/src/test/java/com/ruoyi/cms/controller/cms/OutdoorFairControllerTest.java @@ -0,0 +1,65 @@ +package com.ruoyi.cms.controller.cms; + +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class OutdoorFairControllerTest +{ + @Test + void companyQrCodeUrlUsesPublicForwardedHost() throws Exception + { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setScheme("http"); + request.setServerName("127.0.0.1"); + request.setServerPort(9091); + request.addHeader("X-Forwarded-Proto", "http"); + request.addHeader("X-Forwarded-Host", "39.98.44.136"); + request.addHeader("X-Forwarded-Port", "6024"); + + assertEquals( + "http://39.98.44.136:6024/h5/outdoor-fair/company.html?fairId=12&companyId=34", + buildCompanyH5Url(request, 12L, 34L)); + } + + @Test + void companyQrCodeUrlUsesStandardForwardedHeader() throws Exception + { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setScheme("http"); + request.setServerName("127.0.0.1"); + request.setServerPort(9091); + request.addHeader("Forwarded", "for=192.0.2.10;proto=https;host=jobs.example.com:8443"); + + assertEquals( + "https://jobs.example.com:8443/h5/outdoor-fair/company.html?fairId=12&companyId=34", + buildCompanyH5Url(request, 12L, 34L)); + } + + @Test + void companyQrCodeUrlUsesHostHeaderWithoutForwardedHeaders() throws Exception + { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setScheme("https"); + request.setServerName("127.0.0.1"); + request.setServerPort(9091); + request.addHeader("Host", "jobs.example.com"); + + assertEquals( + "https://jobs.example.com/h5/outdoor-fair/company.html?fairId=12&companyId=34", + buildCompanyH5Url(request, 12L, 34L)); + } + + private String buildCompanyH5Url(MockHttpServletRequest request, Long fairId, Long companyId) + throws Exception + { + OutdoorFairController controller = new OutdoorFairController(); + Method method = OutdoorFairController.class.getDeclaredMethod( + "buildOutdoorFairCompanyH5Url", javax.servlet.http.HttpServletRequest.class, Long.class, Long.class); + method.setAccessible(true); + return (String) method.invoke(controller, request, fairId, companyId); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/RequestUrlUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/RequestUrlUtils.java new file mode 100644 index 0000000..8b629a2 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/RequestUrlUtils.java @@ -0,0 +1,161 @@ +package com.ruoyi.common.utils; + +import javax.servlet.http.HttpServletRequest; + +/** + * 请求公开地址工具。 + * + *

应用位于反向代理后时,Servlet 请求看到的 serverName/serverPort 可能是代理的内网地址, + * 因此优先使用代理传递的公开地址,再回退到 Host 和 Servlet 请求信息。

+ */ +public final class RequestUrlUtils +{ + private RequestUrlUtils() + { + } + + public static String buildBaseUrl(HttpServletRequest request) + { + if (request == null) { + return ""; + } + + String forwardedHeader = request.getHeader("Forwarded"); + String forwarded = firstForwardedValue(forwardedHeader, "host"); + String forwardedHost = firstHeaderValue(request.getHeader("X-Forwarded-Host")); + String hostHeader = request.getHeader("Host"); + boolean hasPublicHost = !isBlank(forwarded) || !isBlank(forwardedHost) || !isBlank(hostHeader); + String host = firstNonBlank( + forwarded, + forwardedHost, + hostHeader, + request.getServerName()); + host = normalizeHost(host); + + String scheme = firstNonBlank( + firstForwardedValue(request.getHeader("Forwarded"), "proto"), + firstHeaderValue(request.getHeader("X-Forwarded-Proto")), + request.getScheme()); + scheme = normalizeScheme(scheme, request.getScheme()); + + String forwardedPort = firstNonBlank( + extractPort(forwarded), + firstHeaderValue(request.getHeader("X-Forwarded-Port"))); + if (!hasPort(host) && shouldIncludePort(forwardedPort, scheme)) { + host += ":" + forwardedPort; + } + if (!hasPort(host) && isBlank(forwardedPort) && !hasPublicHost + && shouldIncludePort(String.valueOf(request.getServerPort()), scheme)) { + host += ":" + request.getServerPort(); + } + + String contextPath = request.getContextPath(); + return scheme + "://" + host + (contextPath == null ? "" : contextPath); + } + + private static String firstForwardedValue(String header, String name) + { + if (isBlank(header)) { + return null; + } + String firstElement = header.split(",", 2)[0]; + String[] parameters = firstElement.split(";"); + for (String parameter : parameters) { + String[] pair = parameter.split("=", 2); + if (pair.length == 2 && name.equalsIgnoreCase(pair[0].trim())) { + return stripQuotes(pair[1].trim()); + } + } + return null; + } + + private static String firstHeaderValue(String header) + { + if (isBlank(header)) { + return null; + } + return stripQuotes(header.split(",", 2)[0].trim()); + } + + private static String normalizeHost(String host) + { + if (isBlank(host)) { + return "localhost"; + } + String normalized = stripQuotes(host.trim()); + if (normalized.indexOf('\r') >= 0 || normalized.indexOf('\n') >= 0 + || normalized.indexOf('/') >= 0 || normalized.indexOf('?') >= 0 + || normalized.indexOf('#') >= 0) { + return "localhost"; + } + return normalized; + } + + private static String normalizeScheme(String scheme, String fallback) + { + if ("https".equalsIgnoreCase(scheme)) { + return "https"; + } + if ("http".equalsIgnoreCase(scheme)) { + return "http"; + } + return "https".equalsIgnoreCase(fallback) ? "https" : "http"; + } + + private static String extractPort(String host) + { + if (isBlank(host)) { + return null; + } + String normalized = host.trim(); + if (normalized.startsWith("[") && normalized.contains("]:")) { + return normalized.substring(normalized.lastIndexOf(':') + 1); + } + int colon = normalized.lastIndexOf(':'); + return colon > -1 && normalized.indexOf(':') == colon + ? normalized.substring(colon + 1) : null; + } + + private static boolean hasPort(String host) + { + return !isBlank(extractPort(host)); + } + + private static boolean shouldIncludePort(String port, String scheme) + { + if (isBlank(port)) { + return false; + } + try { + int portNumber = Integer.parseInt(port); + return portNumber > 0 && portNumber <= 65535 + && !(("http".equalsIgnoreCase(scheme) && portNumber == 80) + || ("https".equalsIgnoreCase(scheme) && portNumber == 443)); + } catch (NumberFormatException e) { + return false; + } + } + + private static String firstNonBlank(String... values) + { + for (String value : values) { + if (!isBlank(value)) { + return value.trim(); + } + } + return null; + } + + private static String stripQuotes(String value) + { + if (value != null && value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) { + return value.substring(1, value.length() - 1); + } + return value; + } + + private static boolean isBlank(String value) + { + return value == null || value.trim().isEmpty(); + } +}