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