应用位于反向代理后时,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(); + } +}