package com.ruoyi.common.utils; import javax.servlet.http.HttpServletRequest; import java.net.URI; import java.net.URISyntaxException; /** * 请求公开地址工具。 * *

应用位于反向代理后时,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"); String requestHost = firstNonBlank(hostHeader, request.getServerName()); URI browserOrigin = isInternalHost(requestHost) ? firstBrowserOrigin(request) : null; String browserHost = browserOrigin == null ? null : getAuthority(browserOrigin); boolean hasPublicHost = !isBlank(forwarded) || !isBlank(forwardedHost) || !isBlank(hostHeader) || browserOrigin != null; String host = firstNonBlank( forwarded, forwardedHost, browserHost, hostHeader, request.getServerName()); host = normalizeHost(host); String scheme = firstNonBlank( firstForwardedValue(request.getHeader("Forwarded"), "proto"), firstHeaderValue(request.getHeader("X-Forwarded-Proto")), browserOrigin == null ? null : browserOrigin.getScheme(), request.getScheme()); scheme = normalizeScheme(scheme, request.getScheme()); String forwardedPort = firstNonBlank( extractPort(forwarded), firstHeaderValue(request.getHeader("X-Forwarded-Port")), browserOrigin == null ? null : String.valueOf(browserOrigin.getPort())); if ("-1".equals(forwardedPort)) { forwardedPort = null; } 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); } /** * 反向代理未透传 Host 时,Servlet 可能只能看到 127.0.0.1:9091。 * 对浏览器请求,Origin/Referer 仍然包含用户实际访问的公开地址,作为兜底来源。 */ private static URI firstBrowserOrigin(HttpServletRequest request) { URI origin = parseHttpUri(firstHeaderValue(request.getHeader("Origin"))); return origin != null ? origin : parseHttpUri(firstHeaderValue(request.getHeader("Referer"))); } private static URI parseHttpUri(String value) { if (isBlank(value)) { return null; } try { URI uri = new URI(value.trim()); String scheme = uri.getScheme(); return uri.getHost() != null && ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme)) ? uri : null; } catch (URISyntaxException e) { return null; } } private static String getAuthority(URI uri) { String host = uri.getHost(); if (host == null) { return null; } if (host.indexOf(':') >= 0 && !host.startsWith("[")) { host = "[" + host + "]"; } return uri.getPort() > 0 ? host + ":" + uri.getPort() : host; } private static boolean isInternalHost(String host) { if (isBlank(host)) { return true; } String normalized = host.trim().toLowerCase(); if (normalized.startsWith("[") && normalized.contains("]")) { normalized = normalized.substring(1, normalized.indexOf(']')); } else if (normalized.indexOf(':') == normalized.lastIndexOf(':')) { normalized = normalized.substring(0, normalized.indexOf(':')); } return "localhost".equals(normalized) || "127.0.0.1".equals(normalized) || "0.0.0.0".equals(normalized) || "::1".equals(normalized); } 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(); } }