在分布式系统和微服务架构中,准确获取服务部署地址是一个常见需求。特别是在 Nginx 反向代理环境下,传统的获取方式可能无法正确反映客户端实际访问的地址。本文将详细介绍如何在 Java 中实现这一功能,并确保在反向代理环境下也能正常工作。

背景与挑战

当服务部署在 Nginx 反向代理之后时,服务端通过常规方法获取的地址信息实际上是反向代理与后端服务之间的通信信息,而非客户端实际访问的地址。这会导致以下问题:

  1. 生成的链接可能使用内部协议(如 HTTP 而非 HTTPS)

  2. 端口号可能不正确(显示后端服务端口而非客户端访问端口)

  3. 域名可能显示为内部服务名而非对外域名

完整实现方案

基础工具类实现

import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 服务器地址信息工具类
 * 支持反向代理环境下的地址获取
 */
public class ServerAddressUtils {
    
    private static final Logger logger = LoggerFactory.getLogger(ServerAddressUtils.class);
    
    // 常用头部名称常量
    private static final String X_FORWARDED_PROTO = "X-Forwarded-Proto";
    private static final String X_FORWARDED_HOST = "X-Forwarded-Host";
    private static final String X_FORWARDED_PORT = "X-Forwarded-Port";
    private static final String X_FORWARDED_FOR = "X-Forwarded-For";
    
    /**
     * 获取完整的服务器基础地址
     * @param request HttpServletRequest 对象
     * @return 完整的服务器基础URL(包含协议、域名和端口)
     */
    public static String getFullServerAddress(HttpServletRequest request) {
        String protocol = resolveProtocol(request);
        String domain = resolveDomain(request);
        String port = resolvePort(request, protocol);
        
        StringBuilder baseUrl = new StringBuilder();
        baseUrl.append(protocol).append("://").append(domain);
        
        if (shouldAppendPort(protocol, port)) {
            baseUrl.append(":").append(port);
        }
        
        logger.debug("Constructed server base URL: {}", baseUrl.toString());
        return baseUrl.toString();
    }
    
    /**
     * 解析协议类型
     */
    private static String resolveProtocol(HttpServletRequest request) {
        String protocol = request.getHeader(X_FORWARDED_PROTO);
        if (StringUtils.isBlank(protocol)) {
            protocol = request.getScheme();
            logger.debug("Using default protocol: {}", protocol);
        } else {
            logger.debug("Using forwarded protocol: {}", protocol);
        }
        return protocol.toLowerCase();
    }
    
    /**
     * 解析域名
     */
    private static String resolveDomain(HttpServletRequest request) {
        String domain = request.getHeader(X_FORWARDED_HOST);
        if (StringUtils.isBlank(domain)) {
            domain = request.getServerName();
            logger.debug("Using default server name: {}", domain);
        } else {
            // 处理可能包含多个域名的场景(如X-Forwarded-Host: example.com,example.org)
            domain = domain.split(",")[0].trim();
            logger.debug("Using forwarded host: {}", domain);
        }
        return domain;
    }
    
    /**
     * 解析端口号
     */
    private static String resolvePort(HttpServletRequest request, String protocol) {
        String port = request.getHeader(X_FORWARDED_PORT);
        if (StringUtils.isBlank(port)) {
            port = String.valueOf(request.getServerPort());
            logger.debug("Using default server port: {}", port);
        } else {
            logger.debug("Using forwarded port: {}", port);
        }
        return port;
    }
    
    /**
     * 判断是否需要附加端口号
     */
    private static boolean shouldAppendPort(String protocol, String port) {
        int portNum = Integer.parseInt(port);
        return !((protocol.equals("http") && portNum == 80) || 
                (protocol.equals("https") && portNum == 443));
    }
    
    /**
     * 获取客户端真实IP(考虑反向代理情况)
     */
    public static String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader(X_FORWARDED_FOR);
        if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        } else {
            // 可能包含多个IP(如X-Forwarded-For: client,proxy1,proxy2)
            ip = ip.split(",")[0].trim();
        }
        return ip;
    }
}

增强功能实现

import java.net.URI;
import java.net.URISyntaxException;

/**
 * 服务器地址增强功能类
 */
public class ServerAddressEnhancer {
    
    /**
     * 构建完整的请求URL(包含路径和查询参数)
     */
    public static String buildFullRequestUrl(HttpServletRequest request) {
        String baseUrl = ServerAddressUtils.getFullServerAddress(request);
        String requestUri = request.getRequestURI();
        String queryString = request.getQueryString();
        
        StringBuilder fullUrl = new StringBuilder(baseUrl);
        fullUrl.append(requestUri);
        
        if (queryString != null && !queryString.isEmpty()) {
            fullUrl.append("?").append(queryString);
        }
        
        return fullUrl.toString();
    }
    
    /**
     * 安全地构建URI对象
     */
    public static URI buildUri(HttpServletRequest request) throws URISyntaxException {
        String url = buildFullRequestUrl(request);
        return new URI(url);
    }
    
    /**
     * 获取上下文路径的完整URL
     */
    public static String getContextPathUrl(HttpServletRequest request) {
        String baseUrl = ServerAddressUtils.getFullServerAddress(request);
        return baseUrl + request.getContextPath();
    }
}

Nginx 最佳配置实践

为确保系统在各种反向代理场景下都能正常工作,建议使用以下 Nginx 配置:

server {
    listen 443 ssl;
    server_name example.com;
    
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    
    location / {
        proxy_pass http://backend-server;
        
        # 基本代理头部
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        
        # 支持多级代理的头部传递
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_set_header X-Forwarded-Prefix $request_uri;
        
        # 连接优化参数
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_read_timeout 300s;
        
        # 缓冲设置
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 16k;
        proxy_busy_buffers_size 32k;
    }
    
    # 其他优化配置
    client_max_body_size 20M;
    keepalive_timeout 30;
}

完整使用示例

Spring Boot 控制器示例

@RestController
@RequestMapping("/api/system")
public class SystemInfoController {
    
    private static final Logger logger = LoggerFactory.getLogger(SystemInfoController.class);
    
    @GetMapping("/info")
    public ResponseEntity<Map<String, Object>> getSystemInfo(HttpServletRequest request) {
        Map<String, Object> info = new LinkedHashMap<>();
        
        // 获取基础地址信息
        info.put("serverBaseUrl", ServerAddressUtils.getFullServerAddress(request));
        info.put("clientIp", ServerAddressUtils.getClientIp(request));
        
        // 获取当前请求信息
        info.put("currentRequestUrl", ServerAddressEnhancer.buildFullRequestUrl(request));
        info.put("requestMethod", request.getMethod());
        info.put("userAgent", request.getHeader("User-Agent"));
        
        // 获取部署环境信息
        try {
            info.put("contextPath", request.getContextPath());
            info.put("servletPath", request.getServletPath());
            info.put("serverInfo", request.getServletContext().getServerInfo());
        } catch (Exception e) {
            logger.warn("Failed to get some server info", e);
        }
        
        return ResponseEntity.ok()
                .header("X-Server-Address", ServerAddressUtils.getFullServerAddress(request))
                .body(info);
    }
    
    @GetMapping("/generate-link")
    public ResponseEntity<Map<String, String>> generateLinks(HttpServletRequest request) {
        Map<String, String> links = new LinkedHashMap<>();
        String baseUrl = ServerAddressUtils.getFullServerAddress(request);
        
        links.put("apiDocs", baseUrl + "/swagger-ui.html");
        links.put("healthCheck", baseUrl + "/actuator/health");
        links.put("metrics", baseUrl + "/actuator/metrics");
        
        return ResponseEntity.ok(links);
    }
}

测试用例

import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;

import static org.junit.jupiter.api.Assertions.*;

class ServerAddressUtilsTest {
    
    @Test
    void testGetFullServerAddressWithProxy() {
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.addHeader("X-Forwarded-Proto", "https");
        request.addHeader("X-Forwarded-Host", "api.example.com");
        request.addHeader("X-Forwarded-Port", "443");
        
        String result = ServerAddressUtils.getFullServerAddress(request);
        assertEquals("https://api.example.com", result);
    }
    
    @Test
    void testGetFullServerAddressWithoutProxy() {
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setScheme("http");
        request.setServerName("localhost");
        request.setServerPort(8080);
        
        String result = ServerAddressUtils.getFullServerAddress(request);
        assertEquals("http://localhost:8080", result);
    }
    
    @Test
    void testGetClientIpWithProxy() {
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.addHeader("X-Forwarded-For", "203.0.113.45, 198.51.100.22");
        
        String result = ServerAddressUtils.getClientIp(request);
        assertEquals("203.0.113.45", result);
    }
}

高级应用场景

1. 负载均衡环境下的处理

在多级代理和负载均衡环境下,可能需要处理更复杂的头部信息:

public static String getLoadBalancedServerAddress(HttpServletRequest request) {
    // 获取所有转发信息
    String forwarded = request.getHeader("Forwarded");
    if (StringUtils.isNotBlank(forwarded)) {
        // 解析 Forwarded 头部(RFC 7239 标准格式)
        // 示例:Forwarded: for=192.0.2.60;proto=https;host=example.com
        Map<String, String> forwardedParams = parseForwardedHeader(forwarded);
        String proto = forwardedParams.get("proto");
        String host = forwardedParams.get("host");
        String forIp = forwardedParams.get("for");
        
        if (proto != null && host != null) {
            return proto + "://" + host;
        }
    }
    
    // 回退到标准处理
    return getFullServerAddress(request);
}

2. 支持 WebSocket 地址生成

public static String getWebSocketUrl(HttpServletRequest request) {
    String baseUrl = getFullServerAddress(request);
    if (baseUrl.startsWith("https://")) {
        return "wss://" + baseUrl.substring(8);
    } else {
        return "ws://" + baseUrl.substring(7);
    }
}

3. 多租户 SaaS 应用支持

public static String getTenantAwareUrl(HttpServletRequest request, String tenantId) {
    String baseUrl = getFullServerAddress(request);
    String host = request.getHeader("X-Forwarded-Host");
    
    if (StringUtils.isNotBlank(host)) {
        // 如果是自定义域名模式
        return baseUrl;
    } else {
        // 如果是路径模式或子域名模式
        return baseUrl + "/" + tenantId;
    }
}

安全注意事项

  1. 头部验证:应验证 X-Forwarded-* 头部的值,防止头部注入攻击

  2. 代理信任:配置只接受来自可信代理的头部信息

  3. 日志脱敏:记录日志时应考虑对敏感信息进行脱敏处理

  4. HTTPS 强制:生产环境应强制使用 HTTPS

public static String getSecureServerAddress(HttpServletRequest request) {
    String address = getFullServerAddress(request);
    if (address.startsWith("http://")) {
        address = "https://" + address.substring(7);
    }
    return address;
}

性能优化建议

  1. 缓存常用地址信息,避免重复计算

  2. 使用线程局部变量存储当前请求的地址信息

  3. 对于高频访问的地址信息,考虑使用静态变量存储基础部分

  4. 实现懒加载机制,只在第一次访问时计算完整地址

public class ServerAddressCache {
    private static final ThreadLocal<String> currentRequestAddress = new ThreadLocal<>();
    
    public static String getCachedAddress(HttpServletRequest request) {
        String address = currentRequestAddress.get();
        if (address == null) {
            address = ServerAddressUtils.getFullServerAddress(request);
            currentRequestAddress.set(address);
        }
        return address;
    }
    
    public static void clearCache() {
        currentRequestAddress.remove();
    }
}

总结

本文提供了在 Java 中获取服务器部署地址的完整解决方案,特别是在 Nginx 反向代理环境下的实现细节。通过正确处理各种转发头部,可以确保应用程序在各种部署环境下都能生成正确的地址信息。实现时应注意安全性、性能和可维护性,根据实际需求选择合适的实现方式。