feat: Add RequestUrlUtils for building base URLs and refactor OutdoorFairController to use it

This commit is contained in:
2026-07-14 21:13:57 +08:00
parent 80caa84a59
commit 4306862ece
5 changed files with 237 additions and 14 deletions

View File

@@ -116,6 +116,11 @@
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
</project>

View File

@@ -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);
}
}

View File

@@ -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<String, Object> buildCompanyQrCodeData(HttpServletRequest request, Long fairId, Long companyId)

View File

@@ -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);
}
}

View File

@@ -0,0 +1,161 @@
package com.ruoyi.common.utils;
import javax.servlet.http.HttpServletRequest;
/**
* 请求公开地址工具。
*
* <p>应用位于反向代理后时Servlet 请求看到的 serverName/serverPort 可能是代理的内网地址,
* 因此优先使用代理传递的公开地址,再回退到 Host 和 Servlet 请求信息。</p>
*/
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();
}
}