在web开发中,获取客户端的真实ip地址是一个常见需求。由于客户端可能经过代理、负载均衡或cdn,request.getremoteaddr() 往往只能拿到代理服务器的ip。因此,需要从特定的http头(如 x-forwarded-for、proxy-client-ip、wl-proxy-client-ip)中解析出原始ip。
下面分别给出经典写法(基于工具类/条件判断)和lambda写法(基于函数式接口 + 流式处理)的实现,并进行对比。
经典写法(传统工具类)
使用 if-else 链和循环逐层解析,逻辑清晰,易于理解和调试。
import javax.servlet.http.httpservletrequest;
public class iputils {
/**
* 获取客户端真实ip地址
* @param request httpservletrequest
* @return 真实ip,如果无法获取则返回 "unknown"
*/
public static string getclientip(httpservletrequest request) {
string ip = request.getheader("x-forwarded-for");
if (ip == null || ip.isempty() || "unknown".equalsignorecase(ip)) {
ip = request.getheader("proxy-client-ip");
}
if (ip == null || ip.isempty() || "unknown".equalsignorecase(ip)) {
ip = request.getheader("wl-proxy-client-ip");
}
if (ip == null || ip.isempty() || "unknown".equalsignorecase(ip)) {
ip = request.getheader("http_client_ip");
}
if (ip == null || ip.isempty() || "unknown".equalsignorecase(ip)) {
ip = request.getheader("http_x_forwarded_for");
}
if (ip == null || ip.isempty() || "unknown".equalsignorecase(ip)) {
ip = request.getremoteaddr();
}
// 对于通过多级代理的情况,取第一个非 unknown 的ip
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}lambda写法(java 8+)
利用 stream 和 optional 对头名称列表进行链式处理,代码更紧凑、函数式风格更明显。
import javax.servlet.http.httpservletrequest;
import java.util.arrays;
import java.util.optional;
public class iputilslambda {
private static final string[] headers_to_try = {
"x-forwarded-for",
"proxy-client-ip",
"wl-proxy-client-ip",
"http_client_ip",
"http_x_forwarded_for"
};
/**
* 获取客户端真实ip地址(lambda风格)
* @param request httpservletrequest
* @return 真实ip,默认返回 request.getremoteaddr()
*/
public static string getclientip(httpservletrequest request) {
string ip = arrays.stream(headers_to_try)
.map(request::getheader)
.filter(header -> header != null && !header.isempty() && !"unknown".equalsignorecase(header))
.findfirst()
.orelseget(request::getremoteaddr);
// 处理多级代理情况
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}或者更进一步,将分割逻辑也融入流中:
public static string getclientipadvanced(httpservletrequest request) {
return arrays.stream(headers_to_try)
.map(request::getheader)
.filter(header -> header != null && !header.isempty() && !"unknown".equalsignorecase(header))
.map(header -> header.contains(",") ? header.split(",")[0].trim() : header)
.findfirst()
.orelseget(request::getremoteaddr);
}两种写法对比
| 对比维度 | 经典写法 | lambda写法 |
|---|---|---|
| 可读性 | 直观,每个条件分支清晰可见,适合所有java开发者 | 代码紧凑,但对不熟悉函数式编程的开发者有一定阅读门槛 |
| 代码行数 | 较多(约20行) | 较少(约10行) |
| 扩展性 | 添加新头需要增加一个 if 块 | 只需在 headers_to_try 数组中增加一个元素 |
| 性能 | 几乎无差异,都是 o(n) 遍历,且只在请求生命周期内执行一次 | 同样 o(n),但 stream 会引入少量额外开销(微乎其微) |
| 调试便利性 | 可在任意 if 处打断点,逐行跟踪 | 流内调试相对困难,需借助 peek() 或 ide 的流调试插件 |
| 错误处理 | 可针对每个头单独处理异常或日志 | 需要额外在 map 或 filter 中处理,不够直观 |
| 团队接受度 | 高,任何java程序员都能立即理解 | 依赖于团队对函数式编程的熟悉程度 |
注意事项与最佳实践
- 代理信任:
x-forwarded-for头可以被伪造,如果应用直接暴露在公网,不应完全信任该头。通常需要在反向代理(如nginx)层面配置proxy_set_header x-forwarded-for $remote_addr,并限制内部网络才能传递该头。 - ip格式:ipv4和ipv6均可能出现在头中,无需额外处理,直接取字符串即可。
- 多级代理:
x-forwarded-for的值格式为client, proxy1, proxy2,通常取第一个(最左侧)为真实客户端ip。 - 空值处理:许多代理会返回
unknown字符串,需要过滤掉。 - 性能考虑:获取ip的操作发生在每个请求中,但遍历几个头字符串的性能开销可以忽略不计。
- spring boot集成:如果使用spring boot,也可以直接使用内置工具类
requestutils.getremoteaddress()或servletwebrequest.getheadervalues(),但原理与上述相同。
方法补充
下面对比了两种java获取客户端ip的实现方式。经典写法使用for循环遍历ip头信息数组,逐个检查并返回第一个有效ip;lambda写法则利用stream api,通过链式操作完成相同的逻辑,代码更简洁。两种方法都考虑了多种代理头信息(x-forwarded-for等)和ip格式处理(分割、去空、trim),最终回退到request.getremoteaddr()。lambda写法展现了函数式编程的优势,但两者在功能上完全等效。
经典写法
@autowired
httpservletrequest request;
string[] ip_headers = {"x-forwarded-for", "x-real-ip", "proxy-client-ip", "wl-proxy-client-ip", "http_x_forwarded_for", "http_x_forwarded", "http_x_cluster_client_ip", "http_client_ip", "http_forwarded_for", "http_forwarded", "http_via", "remote_addr"};
private string getclientipclassic()
{
log.info("##### classic");
for (string header : ip_headers)
{
string ip = request.getheader(header);
if (stringutils.isnotblank(ip) && !stringutils.equalsignorecase("unknown", ip))
{
log.info("##### ip header: {}", header);
string[] ips = ip.split(",");
return ips[0].trim();
}
}
log.info("##### request.getremoteaddr");
return request.getremoteaddr();
}
lambda写法
private string getclientiplambda()
{
log.info("##### lambda");
return arrays.stream(ip_headers)
.peek(log::info)
.map(request::getheader)
.filter(ips -> stringutils.isnotblank(ips) && !"unknown".equalsignorecase(ips))
.flatmap(ips -> arrays.stream(ips.split(",")))
.filter(stringutils::isnotblank)
.map(string::trim)
.findfirst()
.orelse(request.getremoteaddr());
}
总结
- 经典写法:适合大多数项目,尤其是维护性优先、团队成员技术水平不一的场景。它直观、易于调试和扩展,没有额外的学习成本。
- lambda写法:适合追求代码简洁、熟悉函数式编程、且头名称固定不变的场景。它减少了样板代码,让意图更聚焦于“从一系列候选头中找出第一个非空的有效值”。
实际项目中,两种写法均可正确工作。如果项目已经使用 java 8+ 并普遍采用 stream api,lambda写法可以提升代码的表达力;否则,经典写法更加稳妥。
到此这篇关于java获取客户端真实ip地址经典写法详解的文章就介绍到这了,更多相关java获取客户端ip地址内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论