简介
在我们日常项目中,可能遇到这么一个需求“获取用户请求的真实IP”,比如说打印用户登录信息,用户ip白名单等。那该如何准确地获取用户请求真实IP呢?本文主要围绕着没有使用代理的服务和使用了代理的服务两种场景展开讨论。
温馨提示:本文的代理服务器使用Nginx,后端服务器使用tomcat。
没有使用代理的服务
如图所示,用户在浏览器发起一个请求,直接到tomcat服务器。因此可以通过
servletRequest.getRemoteAddr()
获得。
使用了代理的服务
如图所示,用户在浏览器发起一个请求,先经过Proxy1,接着经过Proxy2,再经过Proxy3,最后才到达tomcat服务器。
那此时,通过servletRequest.getRemoteAddr()
是否可以获得用户的真实IP呢?答案显然是不可以的。servletRequest.getRemoteAddr()
只能获取到tomcat服务器前面的Proxy3的ip。
那么我们应该如何获取用户真实ip呢?我们先来认识下X-Forwarded-For。
X-Forwarded-For
X-Forwarded-For(简称XFF)是一个 HTTP 扩展头部。HTTP/1.1(RFC 2616)协议并没有对它的定义,它最开始是由 Squid 这个缓存代理软件引入,用来表示 HTTP 请求端真实IP。如今它已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用,并被写入 RFC 7239(Forwarded HTTP Extension)标准之中。
参考上图,假设Proxy1的nginx配置为:
location / {
proxy_pass Proxy2;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
Proxy2的nginx配置为:
location / {
proxy_pass Proxy3;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
Proxy3的nginx配置为:
location / {
proxy_pass tomcat服务器;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
X-Forwarded-For数据格式:X-Forwarded-For: client_IP, proxy1_IP, proxy2_IP
- client_IP:客户端真实ip;
- proxy1_IP:第一个代理服务器IP;
- proxy2_IP:第二个代理服务器IP;
可能有人会有疑问,怎么没有proxy3_IP呢?首先了解下X-Forwarded-For数据生成的原理:
- 浏览器 -> Proxy1,Proxy1会将浏览器所在的IP写入到
X-Forwarded-For: client_IP
; - Proxy1 -> Proxy2,Proxy2会将Proxy1所在的IP追加到
X-Forwarded-For: client_IP, proxy1_IP
; - Proxy2 -> Proxy3,Proxy3会将Proxy2所在的IP追加到
X-Forwarded-For: client_IP, proxy1_IP, proxy2_IP
; - Proxy3 -> tomcat服务器,tomcat不需要追加;
那此时我们怎么获取到proxy3_IP呢?servletRequest.getRemoteAddr()
即是proxy3_IP。
因此,在使用了代理服务器的场景,后台服务器可以通过servletRequest.getHeader("X-Forwarded-For")
截取第一个ip得到用户请求真实ip。
问题延伸
如何获取用户真实协议、端口?
Proxy的nginx可以配置为:
location / {
proxy_pass XXXX;
# 协议传递给下一个代理(或者后台服务器)
proxy_set_header X-Forwarded-Proto $scheme;
# 端口号传递给下一个代理(或者后台服务器)
proxy_set_header X-Forwarded-Port $server_port;
}
注意,这里跟X-Forwarded-For有点区别,X-Forwarded-For是采用追加的方式,X-Forwarded-Proto和X-Forwarded-Port是直接传递过去,就是最终只有一个。
tomcat是如何处理X-Forwarded-For、X-Forwarded-Proto、X-Forwarded-Port?
添加配置
- server.xml方式:
- springboot方式:
server.tomcat.remote-ip-header=X-Forwarded-For
server.tomcat.protocol-header=X-Forwarded-Proto
server.tomcat.port-header=X-Forwarded-Port
# 2.2.0之前使用这个配置
server.use-forward-headers=true
# 2.2.0以上使用下面这个
# server.forward-headers-strategy=NATIVE
RemoteIpVavle
RemoteIpVavle意思是远程ip阀门,也就是tomcat用来处理X-Forwarded-For、X-Forwarded-Proto、X-Forwarded-Port的类。接下来我们来看这么一段源码(以tomcat-embed-core:9.0.31为例,不同版本可能代码会稍微有点差异):
/**
* 代码还是按照之前的例子:浏览器(https) -> Proxy1(http) -> Proxy2(http) -> Proxy3(http) -> tomcat服务器
* 每个nginx配置如下:
* location / {
* proxy_pass XXXX;
* # 协议传递给下一个代理(或者后台服务器)
* proxy_set_header X-Forwarded-Proto $scheme;
* # 端口号传递给下一个代理(或者后台服务器)
* proxy_set_header X-Forwarded-Port $server_port;
* # ip追加并传给下一个代理
* proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
* }
* {@inheritDoc}
*/
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
// Proxy3的IP
final String originalRemoteAddr = request.getRemoteAddr();
// Proxy3的主机
final String originalRemoteHost = request.getRemoteHost();
// tomcat的协议比如http
final String originalScheme = request.getScheme();
// tomcat的协议是否安全
final boolean originalSecure = request.isSecure();
// tomcat服务器名称(如果配置了proxy_set_header Host $http_host; 就会取最外层代理的服务器名称)
final String originalServerName = request.getServerName();
// tomcat服务器主机名
final String originalLocalName = request.getLocalName();
// tomcat的端口(如果配置了proxy_set_header Host $http_host; 就会取最外层代理的服务器端口)
final int originalServerPort = request.getServerPort();
// tomcat服务器端口
final int originalLocalPort = request.getLocalPort();
// X-Forwarded-By请求头数据
final String originalProxiesHeader = request.getHeader(proxiesHeader);
// X-Forwarded-For请求头数据
final String originalRemoteIpHeader = request.getHeader(remoteIpHeader);
/**
* internalProxies即内网代理IP,这些默认都是受信任的IP,
* 判断Proxy3服务器ip是否是内网ip
*
*/
boolean isInternal = internalProxies != null &&
internalProxies.matcher(originalRemoteAddr).matches();
// 如果Proxy3服务器ip是内网ip,或者是trustedProxies(自定义信任ip列表)的ip
if (isInternal || (trustedProxies != null &&
trustedProxies.matcher(originalRemoteAddr).matches())) {
String remoteIp = null;
// In java 6, proxiesHeaderValue should be declared as a java.util.Deque
LinkedList proxiesHeaderValue = new LinkedList<>();
StringBuilder concatRemoteIpHeaderValue = new StringBuilder();
// 获取X-Forwarded-For请求头ip列表
for (Enumeration e = request.getHeaders(remoteIpHeader); e.hasMoreElements();) {
if (concatRemoteIpHeaderValue.length() > 0) {
concatRemoteIpHeaderValue.append(", ");
}
concatRemoteIpHeaderValue.append(e.nextElement());
}
// 获取X-Forwarded-For请求头ip列表置换成数组,例如:[192.168.1.10, 192.168.2.10]
String[] remoteIpHeaderValue = commaDelimitedListToStringArray(concatRemoteIpHeaderValue.toString());
int idx;
// 如果是自定义信任ip处理,则将Proxy3_IP加入到proxiesHeaderValue链表
if (!isInternal) {
proxiesHeaderValue.addFirst(originalRemoteAddr);
}
// loop on remoteIpHeaderValue to find the first trusted remote ip and to build the proxies chain
// 从右向左循环X-Forwarded-For请求头ip列表
for (idx = remoteIpHeaderValue.length - 1; idx >= 0; idx--) {
String currentRemoteIp = remoteIpHeaderValue[idx];
remoteIp = currentRemoteIp;
// 如果是内网ip则不作处理
if (internalProxies !=null && internalProxies.matcher(currentRemoteIp).matches()) {
// do nothing, internalProxies IPs are not appended to the
// 如果是自定义信任ip则加入到proxiesHeaderValue链表中
} else if (trustedProxies != null &&
trustedProxies.matcher(currentRemoteIp).matches()) {
proxiesHeaderValue.addFirst(currentRemoteIp);
} else {
// 如果没有匹配上,终止循环;否则消耗一次idx
idx--; // decrement idx because break statement doesn't do it
break;
}
}
// continue to loop on remoteIpHeaderValue to build the new value of the remoteIpHeader
/*
* 如果之前idx没有被消耗完,则继续循环,加入newRemoteIpHeaderValue链表
* 假设X-Forwarded-For请求头ip列表[223.104.6.34, 223.104.7.45,
* 192.168.2.10],被消耗后,剩余[223.104.6.34]。
*/
LinkedList newRemoteIpHeaderValue = new LinkedList<>();
for (; idx >= 0; idx--) {
String currentRemoteIp = remoteIpHeaderValue[idx];
newRemoteIpHeaderValue.addFirst(currentRemoteIp);
}
// 客户端ip或者第一个非内网并且非自定义信任的ip
if (remoteIp != null) {
request.setRemoteAddr(remoteIp);
request.setRemoteHost(remoteIp);
// proxiesHeaderValue链表为空,即没有匹配到自定义信任ip,
// 则删除X-Forwarded-By请求头信息;否则设置该请求头信息
if (proxiesHeaderValue.size() == 0) {
request.getCoyoteRequest().getMimeHeaders().removeHeader(proxiesHeader);
} else {
String commaDelimitedListOfProxies = listToCommaDelimitedString(proxiesHeaderValue);
request.getCoyoteRequest().getMimeHeaders().setValue(proxiesHeader).setString(commaDelimitedListOfProxies);
}
// newRemoteIpHeaderValue链表为空,即X-Forwarded-For都匹配到内网ip,
// 则删除X-Forwarded-For请求头信息;否则设置该请求头信息
if (newRemoteIpHeaderValue.size() == 0) {
request.getCoyoteRequest().getMimeHeaders().removeHeader(remoteIpHeader);
} else {
String commaDelimitedRemoteIpHeaderValue = listToCommaDelimitedString(newRemoteIpHeaderValue);
request.getCoyoteRequest().getMimeHeaders().setValue(remoteIpHeader).setString(commaDelimitedRemoteIpHeaderValue);
}
}
// 判断tomcat是否配置了 X-Forwarded-Proto协议请求头
if (protocolHeader != null) {
String protocolHeaderValue = request.getHeader(protocolHeader);
if (protocolHeaderValue == null) {
// Don't modify the secure, scheme and serverPort attributes
// of the request
// 如果是https协议,则重新设置secure=true,scheme=https,端口
} else if (isForwardedProtoHeaderValueSecure(protocolHeaderValue)) {
request.setSecure(true);
request.getCoyoteRequest().scheme().setString("https");
setPorts(request, httpsServerPort);
} else {// 其他则重新设置secure=false,scheme=http,端口
request.setSecure(false);
request.getCoyoteRequest().scheme().setString("http");
setPorts(request, httpServerPort);
}
}
// 判断tomcat是否配置了X-Forwarded-Host,如果有则设置serverName、localName为Proxy1的主机
if (hostHeader != null) {
String hostHeaderValue = request.getHeader(hostHeader);
if (hostHeaderValue != null) {
try {
int portIndex = Host.parse(hostHeaderValue);
if (portIndex > -1) {
log.debug(sm.getString("remoteIpValve.invalidHostWithPort", hostHeaderValue, hostHeader));
hostHeaderValue = hostHeaderValue.substring(0, portIndex);
}
request.getCoyoteRequest().serverName().setString(hostHeaderValue);
if (isChangeLocalName()) {
request.getCoyoteRequest().localName().setString(hostHeaderValue);
}
} catch (IllegalArgumentException iae) {
log.debug(sm.getString("remoteIpValve.invalidHostHeader", hostHeaderValue, hostHeader));
}
}
}
request.setAttribute(Globals.REQUEST_FORWARDED_ATTRIBUTE, Boolean.TRUE);
if (log.isDebugEnabled()) {
log.debug("Incoming request " + request.getRequestURI() + " with originalRemoteAddr [" + originalRemoteAddr +
"], originalRemoteHost=[" + originalRemoteHost + "], originalSecure=[" + originalSecure +
"], originalScheme=[" + originalScheme + "], originalServerName=[" + originalServerName +
"], originalServerPort=[" + originalServerPort +
"] will be seen as newRemoteAddr=[" + request.getRemoteAddr() +
"], newRemoteHost=[" + request.getRemoteHost() + "], newSecure=[" + request.isSecure() +
"], newScheme=[" + request.getScheme() + "], newServerName=[" + request.getServerName() +
"], newServerPort=[" + request.getServerPort() + "]");
}
} else {
if (log.isDebugEnabled()) {
log.debug("Skip RemoteIpValve for request " + request.getRequestURI() + " with originalRemoteAddr '"
+ request.getRemoteAddr() + "'");
}
}
// tomcat accesslog相关参数赋值
if (requestAttributesEnabled) {
request.setAttribute(AccessLog.REMOTE_ADDR_ATTRIBUTE,
request.getRemoteAddr());
request.setAttribute(Globals.REMOTE_ADDR_ATTRIBUTE,
request.getRemoteAddr());
request.setAttribute(AccessLog.REMOTE_HOST_ATTRIBUTE,
request.getRemoteHost());
request.setAttribute(AccessLog.PROTOCOL_ATTRIBUTE,
request.getProtocol());
request.setAttribute(AccessLog.SERVER_NAME_ATTRIBUTE,
request.getServerName());
request.setAttribute(AccessLog.SERVER_PORT_ATTRIBUTE,
Integer.valueOf(request.getServerPort()));
}
try {
// 进入下一个阀门,最后进入controller进行业务处理
getNext().invoke(request, response);
} finally {
request.setRemoteAddr(originalRemoteAddr);
request.setRemoteHost(originalRemoteHost);
request.setSecure(originalSecure);
request.getCoyoteRequest().scheme().setString(originalScheme);
request.getCoyoteRequest().serverName().setString(originalServerName);
request.getCoyoteRequest().localName().setString(originalLocalName);
request.setServerPort(originalServerPort);
request.setLocalPort(originalLocalPort);
MimeHeaders headers = request.getCoyoteRequest().getMimeHeaders();
if (originalProxiesHeader == null || originalProxiesHeader.length() == 0) {
headers.removeHeader(proxiesHeader);
} else {
headers.setValue(proxiesHeader).setString(originalProxiesHeader);
}
if (originalRemoteIpHeader == null || originalRemoteIpHeader.length() == 0) {
headers.removeHeader(remoteIpHeader);
} else {
headers.setValue(remoteIpHeader).setString(originalRemoteIpHeader);
}
}
}
/*
* Considers the value to be secure if it exclusively holds forwards for
* {@link #protocolHeaderHttpsValue}.
*/
private boolean isForwardedProtoHeaderValueSecure(String protocolHeaderValue) {
if (!protocolHeaderValue.contains(",")) {
return protocolHeaderHttpsValue.equalsIgnoreCase(protocolHeaderValue);
}
String[] forwardedProtocols = commaDelimitedListToStringArray(protocolHeaderValue);
if (forwardedProtocols.length == 0) {
return false;
}
for (int i = 0; i < forwardedProtocols.length; i++) {
if (!protocolHeaderHttpsValue.equalsIgnoreCase(forwardedProtocols[i])) {
return false;
}
}
return true;
}
/**
* 端口设置
*
*/
private void setPorts(Request request, int defaultPort) {
int port = defaultPort;
// 判断tomcat是否配置了X-Forwarded-Port
if (portHeader != null) {
// 获取请求头X-Forwarded-Port值,即Proxy1的端口
String portHeaderValue = request.getHeader(portHeader);
if (portHeaderValue != null) {
try {
port = Integer.parseInt(portHeaderValue);
} catch (NumberFormatException nfe) {
if (log.isDebugEnabled()) {
log.debug(sm.getString(
"remoteIpValve.invalidPortHeader",
portHeaderValue, portHeader), nfe);
}
}
}
}
request.setServerPort(port);
if (changeLocalPort) {
request.setLocalPort(port);
}
}
因此,tomcat的RemoteIpVavle作用主要有以下几点:
通过内网IP默认列表internalProxies,去掉X-Forwarded-For中的内网IP,如果都是内网IP,X-Forwarded-For整个请求头会被删掉;
如果X-Forwarded-For中IP在用户自定义IP信任列表trustedProxies里,则加到X-Forwarded-By请求头,否则X-Forwarded-For整个请求头会被删掉;
-
如果tomcat配置X-Forwarded-Proto,则按照X-Forwarded-Proto(一般是最外层代理协议)请求头数据来设置,具体如下:
- X-Forwarded-Proto请求头=https,则重新设置secure=true、scheme="https"、端口(默认443);
- X-Forwarded-Proto请求头!=null && !=https,则重新设置secure=false、scheme="http"、端口(默认80);
- 如果tomcat配置X-Forwarded-Port,则按照X-Forwarded-Port(一般是最外层代理端口)请求头数据设置端口,否则使用默认端口;
如果tomcat配置了X-Forwarded-Host,则按照X-Forwarded-Host(一般是最外层代理主机)请求头数据设置serverName、localName;
tomcat accesslog相关参数赋值;