惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

TaoSecurity Blog
TaoSecurity Blog
L
LINUX DO - 最新话题
Help Net Security
Help Net Security
N
News | PayPal Newsroom
www.infosecurity-magazine.com
www.infosecurity-magazine.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Last Watchdog
The Last Watchdog
S
Security @ Cisco Blogs
W
WeLiveSecurity
C
CXSECURITY Database RSS Feed - CXSecurity.com
Webroot Blog
Webroot Blog
T
Troy Hunt's Blog
V
Vulnerabilities – Threatpost
Google Online Security Blog
Google Online Security Blog
N
News and Events Feed by Topic
T
Threat Research - Cisco Blogs
Security Archives - TechRepublic
Security Archives - TechRepublic
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tor Project blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
D
Darknet – Hacking Tools, Hacker News & Cyber Security
PCI Perspectives
PCI Perspectives
Google DeepMind News
Google DeepMind News
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
S
SegmentFault 最新的问题
J
Java Code Geeks
P
Privacy & Cybersecurity Law Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园 - 【当耐特】
博客园_首页
H
Hacker News: Front Page
T
Threatpost
Jina AI
Jina AI
博客园 - Franky
月光博客
月光博客
L
LINUX DO - 热门话题
The Cloudflare Blog
H
Heimdal Security Blog
博客园 - 司徒正美
酷 壳 – CoolShell
酷 壳 – CoolShell
Cloudbric
Cloudbric
雷峰网
雷峰网
Hugging Face - Blog
Hugging Face - Blog
S
Secure Thoughts
T
Tenable Blog
I
Intezer
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻

宏尘极客

halo-theme-dream2.0-plus 主题-新版朋友圈配置说明 halo-theme-dream2.0-plus 主题 1.20.2 版本-留言板配置说明 微信红包封面-马到功成 Windows11彻底关闭自动更新指南 WSL2 Docker 资源配置指南 绿联Pro 安装Dos游戏中文版webui,一个可以浏览器内玩dos游戏的容器 绿联Pro 安装 Stirling-PDF-V2 - 强大的本地托管基于 Web 的 PDF 操作工具 绿联Pro 部署在线游戏机 绿联Pro 安装OpenList一个支持多种存储的文件列表程序 绿联Pro 部署Memos - 轻量级自托管备忘录中心 绿联Pro 安装 Stirling-PDF - 强大的本地托管基于 Web 的 PDF 操作工具 绿联Pro 部署Upage - 基于大模型的可视化网页构建平台,Lovable 开源替代 不蒜子数据同步工具使用指南 绿联Pro 部署 PostgreSQL 数据库 绿联Pro 自建不蒜子统计服务,替代Dream2.0 Plus主题页脚统计数据 1panel使用编排模板部署不蒜子服务,自建不蒜子统计服务,替代Dream2.0 Plus主题页脚统计数据 绿联Pro 安装Photopea,轻量在线PS工具 绿联Pro 安装禅道(Zentao),一款功能强大、全面的敏捷项目管理软件 绿联Pro 安装MeterSphere,新一代的测试管理和接口测试工具 Docker Compose 标签介绍与示例指南 绿联Pro 安装Meilisearch 搜索引擎,为halo博客提供增强搜索引擎 解析 <meta> 标签的全局 Referrer 策略设置 电线粗细选择与家用大功率电器布线指南 Nginx 防盗链配置介绍 Nginx 反向代理简介 绿联Pro 部署meting-api服务并使用lukcy反向代理,自建音乐API服务 1panel使用编排模板部署meting-api服务,自建音乐API服务 Git revert 撤销已推送到服务器的提交详解 绿联Pro 部署MediaGo,跨平台视频提取工具 C# 自定义事件与委托 绿联Pro 安装Umami,使用MySQL数据库,为你的网站添加网站监控
java开发 - 通过客户端访问接口获取接口服务器实际部署地址
宏尘 · 2025-07-01 · via 宏尘极客

在分布式系统和微服务架构中,准确获取服务部署地址是一个常见需求。特别是在 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 反向代理环境下的实现细节。通过正确处理各种转发头部,可以确保应用程序在各种部署环境下都能生成正确的地址信息。实现时应注意安全性、性能和可维护性,根据实际需求选择合适的实现方式。