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

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

*/ public final class RequestUrlUtils { /** 反向代理未透传公开地址时使用的默认访问地址(不带末尾斜杠,便于拼接路径)。 */ private static final String DEFAULT_PUBLIC_BASE_URL = "http://39.98.44.136:6024/shihezi"; 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()); String forwardedPublicHost = firstNonBlank(forwarded, forwardedHost); if (isInternalHost(requestHost) && isInternalHost(forwardedPublicHost)) { String contextPath = request.getContextPath(); return DEFAULT_PUBLIC_BASE_URL + (contextPath == null ? "" : contextPath); } 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); } /** * 将历史数据中的回环地址改为当前请求的公开 origin,同时保留资源路径。 * 这样浏览器不会把 127.0.0.1/localhost 解释为访问者自己的电脑。 */ public static String normalizePublicUrl(HttpServletRequest request, String value) { if (isBlank(value) || request == null) { return value; } try { URI uri = new URI(value.trim()); String host = uri.getHost(); if (host == null || !isInternalHost(host)) { return value; } URI base = new URI(buildBaseUrl(request)); String authority = base.getRawAuthority(); if (isBlank(authority)) { return value; } return base.getScheme() + "://" + authority + (uri.getRawPath() == null ? "" : uri.getRawPath()) + (uri.getRawQuery() == null ? "" : "?" + uri.getRawQuery()) + (uri.getRawFragment() == null ? "" : "#" + uri.getRawFragment()); } catch (URISyntaxException e) { return value; } } /** 回环资源地址不能写入公开招聘会图片字段。相对路径不受此校验影响。 */ public static boolean isLoopbackUrl(String value) { if (isBlank(value)) { return false; } try { URI uri = new URI(value.trim()); return uri.getHost() != null && isInternalHost(uri.getHost()); } catch (URISyntaxException e) { return false; } } 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 { int portSeparator = normalized.lastIndexOf(':'); if (portSeparator > -1 && normalized.indexOf(':') == portSeparator) { normalized = normalized.substring(0, portSeparator); } } 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(); } }