via 值为: 下面是一些Demo
WTP/1.1 GDSZ-PS-GW010-WAP05.gd.chinamobile.com (Nokia WAP Gateway 4.0 CD3/ECD13_C/NWG4.0 CD3 ECD13_C 4.1.03)
下面是解释
列出从客户端到 OCS 或者相反方向的响应经过了哪些代理服务器,他们用什么协议(和版本)发送的请求。
当客户端请求到达第一个代理服务器时,该服务器会在自己发出的请求里面添加Via头部,并填上自己的相关信息,当下一个代理服务器收到第一个代理服务器的请求时,会在自己发出的请求里面复制前一个代理服务器的请求的Via头部,并把自己的相关信息加到后面,
以此类推,当 OCS 收到最后一个代理服务器的请求时,检查 Via 头部,就知道该请求所经过的路由。
例如:Via:1.0 236-81.D07071953.sina.com.cn:80 (squid/2.6.STABLE13)
public static boolean isMobileDevice(HttpServletRequest request) {
boolean pcFlag = false;
boolean mobileFlag = false;
String via = request.getHeader("Via");
String userAgent = request.getHeader("user-agent");
https://segmentfault.com/a/1190000000658255
http://www.aichengxu.com/view/10147673
server {
listen 80;
server_name www.jb51.net;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host:80;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Via "nginx";
}
}
- getRequestURI:/Test/test
- getRequestURL:http://127.0.0.1:8080/Test/test
getRequestURI
public String getRequestURI();
从HTTP请求的第一行返回请求的URL中定义被请求的资源的部分。如果有一个查询字符串存在,这个查询字符串将不包括在返回值当中。
例如,一个请求通过/catalog/books?id=1这样的URL路径访问,这个方法将返回/catalog/books。这个方法的返回值包括了Servlet路径和路径信息。
如果这个URL路径中的的一部分经过了URL编码,这个方法的返回值在返回之前必须经过解码。
一、没有使用代理服务器的情况:
REMOTE_ADDR = 您的 IP
HTTP_VIA = 没数值或不显示
HTTP_X_FORWARDED_FOR = 没数值或不显示
二、使用透明代理服务器的情况:Transparent Proxies
REMOTE_ADDR = 最后一个代理服务器 IP
HTTP_VIA = 代理服务器 IP
HTTP_X_FORWARDED_FOR = 您的真实 IP ,经过多个代理服务器时,这个值类似如下:203.98.182.163, 203.98.182.163, 203.129.72.215。
这类代理服务器还是将您的信息转发给您的访问对象,无法达到隐藏真实身份的目的。
三、使用普通匿名代理服务器的情况:Anonymous Proxies
REMOTE_ADDR = 最后一个代理服务器 IP
HTTP_VIA = 代理服务器 IP
HTTP_X_FORWARDED_FOR = 代理服务器 IP ,经过多个代理服务器时,这个值类似如下:203.98.182.163, 203.98.182.163, 203.129.72.215。
隐藏了您的真实IP,但是向访问对象透露了您是使用代理服务器访问他们的。
四、使用欺骗性代理服务器的情况:Distorting Proxies
REMOTE_ADDR = 代理服务器 IP
HTTP_VIA = 代理服务器 IP
HTTP_X_FORWARDED_FOR = 随机的 IP ,经过多个代理服务器时,这个值类似如下:203.98.182.163, 203.98.182.163, 203.129.72.215。
告诉了访问对象您使用了代理服务器,但编造了一个虚假的随机IP代替您的真实IP欺骗它。
五、使用高匿名代理服务器的情况:High Anonymity Proxies (Elite proxies)
REMOTE_ADDR = 代理服务器 IP
HTTP_VIA = 没数值或不显示
HTTP_X_FORWARDED_FOR = 没数值或不显示 ,经过多个代理服务器时,这个值类似如下:203.98.182.163, 203.98.182.163, 203.129.72.215。
完全用代理服务器的信息替代了您的所有信息,就象您就是完全使用那台代理服务器直接访问对象。
http://www.blogjava.net/Todd/archive/2009/10/09/297590.html
这一HTTP头一般格式如下:
X-Forwarded-For: client1, proxy1, proxy2
其中的值通过一个 逗号+空格 把多个IP地址区分开, 最左边(client1)是最原始客户端的IP地址, 代理服务器每成功收到一个请求,就把添加到右边。 在上面这个例子中,这个请求成功通过了三台代理服务器:proxy1, proxy2 及 proxy3。请求由client1发出,到达了proxy3(proxy3可能是请求的终点)。请求刚从client1中发出时,XFF是空的,请求被发往proxy1;通过proxy1的时候,client1被添加到XFF中,之后请求被发往proxy2;通过proxy2的时候,proxy1被添加到XFF中,之后请求被发往proxy3;通过proxy3时,proxy2被添加到XFF中,之后请求的的去向不明,如果proxy3不是请求终点,请求会被继续转发。
鉴于伪造这一字段非常容易,应该谨慎使用X-Forwarded-For字段。正常情况下XFF中最后一个IP地址是最后一个代理服务器的IP地址, 这通常是一个比较可靠的信息来源。
http://baike.so.com/doc/7045641-7268547.html
《TCP/IP详解卷》和《HTTP权威指南》都可以看下。一个是底层原理,一个是上层应用。
HTTP(HyperTextTransferProtocol)即超文本传输协议,目前网页传输的的通用协议。HTTP协议采用了请求/响应模型,浏览器或其他客户端发出请求,服务器给与响应。就整个网络资源传输而言,包括message-header和message-body两部分。首先传递message- header,即http header消息 。http header 消息通常被分为4个部分:general header, request header, response header, entity header。但是这种分法就理解而言,感觉界限不太明确。根据*对http header内容的组织形式,大体分为Request和Response两部分。
Requests部分
Header | 解释 | 示例 |
---|---|---|
Accept | 指定客户端能够接收的内容类型 | Accept: text/plain, text/html |
Accept-Charset | 浏览器可以接受的字符编码集。 | Accept-Charset: iso-8859-5 |
Accept-Encoding | 指定浏览器可以支持的web服务器返回内容压缩编码类型。 | Accept-Encoding: compress, gzip |
Accept-Language | 浏览器可接受的语言 | Accept-Language: en,zh |
Accept-Ranges | 可以请求网页实体的一个或者多个子范围字段 | Accept-Ranges: bytes |
Authorization | HTTP授权的授权证书 | Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== |
Cache-Control | 指定请求和响应遵循的缓存机制 | Cache-Control: no-cache |
Connection | 表示是否需要持久连接。(HTTP 1.1默认进行持久连接) | Connection: close |
Cookie | HTTP请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器。 | Cookie: $Version=1; Skin=new; |
Content-Length | 请求的内容长度 | Content-Length: 348 |
Content-Type | 请求的与实体对应的MIME信息 | Content-Type: application/x-www-form-urlencoded |
Date | 请求发送的日期和时间 | Date: Tue, 15 Nov 2010 08:12:31 GMT |
Expect | 请求的特定的服务器行为 | Expect: 100-continue |
From | 发出请求的用户的Email | From: user@email.com |
Host | 指定请求的服务器的域名和端口号 | Host: www.zcmhi.com |
If-Match | 只有请求内容与实体相匹配才有效 | If-Match: “737060cd8c284d8af7ad3082f209582d” |
If-Modified-Since | 如果请求的部分在指定时间之后被修改则请求成功,未被修改则返回304代码 | If-Modified-Since: Sat, 29 Oct 2010 19:43:31 GMT |
If-None-Match | 如果内容未改变返回304代码,参数为服务器先前发送的Etag,与服务器回应的Etag比较判断是否改变 | If-None-Match: “737060cd8c284d8af7ad3082f209582d” |
If-Range | 如果实体未改变,服务器发送客户端丢失的部分,否则发送整个实体。参数也为Etag | If-Range: “737060cd8c284d8af7ad3082f209582d” |
If-Unmodified-Since | 只在实体在指定时间之后未被修改才请求成功 | If-Unmodified-Since: Sat, 29 Oct 2010 19:43:31 GMT |
Max-Forwards | 限制信息通过代理和网关传送的时间 | Max-Forwards: 10 |
Pragma | 用来包含实现特定的指令 | Pragma: no-cache |
Proxy-Authorization | 连接到代理的授权证书 | Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== |
Range | 只请求实体的一部分,指定范围 | Range: bytes=500-999 |
Referer | 先前网页的地址,当前请求网页紧随其后,即来路 | Referer: http://www.zcmhi.com/archives/71.html |
TE | 客户端愿意接受的传输编码,并通知服务器接受接受尾加头信息 | TE: trailers,deflate;q=0.5 |
Upgrade | 向服务器指定某种传输协议以便服务器进行转换(如果支持) | Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 |
User-Agent | User-Agent的内容包含发出请求的用户信息 | User-Agent: Mozilla/5.0 (Linux; X11) |
Via | 通知中间网关或代理服务器地址,通信协议 | Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1) |
Warning | 关于消息实体的警告信息 | Warn: 199 Miscellaneous warning |
Responses 部分
Header | 解释 | 示例 |
---|---|---|
Accept-Ranges | 表明服务器是否支持指定范围请求及哪种类型的分段请求 | Accept-Ranges: bytes |
Age | 从原始服务器到代理缓存形成的估算时间(以秒计,非负) | Age: 12 |
Allow | 对某网络资源的有效的请求行为,不允许则返回405 | Allow: GET, HEAD |
Cache-Control | 告诉所有的缓存机制是否可以缓存及哪种类型 | Cache-Control: no-cache |
Content-Encoding | web服务器支持的返回内容压缩编码类型。 | Content-Encoding: gzip |
Content-Language | 响应体的语言 | Content-Language: en,zh |
Content-Length | 响应体的长度 | Content-Length: 348 |
Content-Location | 请求资源可替代的备用的另一地址 | Content-Location: /index.htm |
Content-MD5 | 返回资源的MD5校验值 | Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ== |
Content-Range | 在整个返回体中本部分的字节位置 | Content-Range: bytes 21010-47021/47022 |
Content-Type | 返回内容的MIME类型 | Content-Type: text/html; charset=utf-8 |
Date | 原始服务器消息发出的时间 | Date: Tue, 15 Nov 2010 08:12:31 GMT |
ETag | 请求变量的实体标签的当前值 | ETag: “737060cd8c284d8af7ad3082f209582d” |
Expires | 响应过期的日期和时间 | Expires: Thu, 01 Dec 2010 16:00:00 GMT |
Last-Modified | 请求资源的最后修改时间 | Last-Modified: Tue, 15 Nov 2010 12:45:26 GMT |
Location | 用来重定向接收方到非请求URL的位置来完成请求或标识新的资源 | Location: http://www.zcmhi.com/archives/94.html |
Pragma | 包括实现特定的指令,它可应用到响应链上的任何接收方 | Pragma: no-cache |
Proxy-Authenticate | 它指出认证方案和可应用到代理的该URL上的参数 | Proxy-Authenticate: Basic |
refresh | 应用于重定向或一个新的资源被创造,在5秒之后重定向(由网景提出,被大部分浏览器支持) |
Refresh: 5; url=
http://www.zcmhi.com/archives/94.html
|
Retry-After | 如果实体暂时不可取,通知客户端在指定时间之后再次尝试 | Retry-After: 120 |
Server | web服务器软件名称 | Server: Apache/1.3.27 (Unix) (Red-Hat/Linux) |
Set-Cookie | 设置Http Cookie | Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1 |
Trailer | 指出头域在分块传输编码的尾部存在 | Trailer: Max-Forwards |
Transfer-Encoding | 文件传输编码 | Transfer-Encoding:chunked |
Vary | 告诉下游代理是使用缓存响应还是从原始服务器请求 | Vary: * |
Via | 告知代理客户端响应是通过哪里发送的 | Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1) |
Warning | 警告实体可能存在的问题 | Warning: 199 Miscellaneous warning |
WWW-Authenticate | 表明客户端请求实体应该使用的授权方案 | WWW-Authenticate: Basic |
更多参见 w3c官网Header Field Definitions
http://kb.cnblogs.com/page/92320/
proxy_pass http://yourdomain.com;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
- 在默认情况下,Nginx 并不会对 XFF 头做任何处理
- 此时 Nginx 后面的 Resin/Apache/Tomcat 通过 request.getHeader("X-FORWARDED-FOR") 获得的ip仍然是原始ip。
- 当 Nginx 设置 X-Forwarded-For 等于 $proxy_add_x_forwarded_for 时:
- 如果从CDN过来的请求没有设置 XFF 头(通常这种事情不会发生),XFF 头为 CDN 的ip
- 此时相对于 Nginx 来说,客户端就是 CDN
- 如果 CDN 设置了 XFF 头,我们这里又设置了一次,且值为$proxy_add_x_forwarded_for 的话:
- XFF 头为“客户端IP,Nginx负载均衡服务器IP”,这样取第一个值即可
- 这也就是大家所常见的场景!
Client -> Apache WebServer + Weblogic http plugin -> Weblogic Instances
proxy_set_header HTTP_CLIENT_IP $remote_addr;
实际上,REMOTE_ADDR 是客户端跟服务器“握手”时的IP,但如果使用了“匿名代理”,REMOTE_ADDR 将显示代理服务器的ip,或者最后一个代理服务器的ip。请参考附录B。
内网IP:172.16.100.10
客户端IP:123.123.123.123
测试页面: test.jsp
<%
out.println("x-forwarded-for: " + request.getHeader("x-forwarded-for"));
out.println("remote hosts: " + request.getRemoteAddr());
%>
nginx 配置一proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;wget测试wget -O aa --header="X-Forwarded-For:192.168.0.1" "http://test.com/test.jsp"页面返回结果:x-forwarded-for: 192.168.0.1, 123.123.123.123remote hosts: 172.16.100.10curl测试curl -H "X-Forwarded-For:192.168.0.1" "http://test.com/test.jsp"x-forwarded-for: 192.168.0.1, 123.123.123.123remote hosts: 172.16.100.10
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
wget测试:
wget -O aa --header="X-Forwarded-For:192.168.0.1" "http://test.com/test.jsp"
页面返回结果:
x-forwarded-for: 123.123.123.123
remote hosts: 172.16.100.10
curl测试
curl -H "X-Forwarded-For:192.168.0.1" "http://test.com/test.jsp"
x-forwarded-for: 123.123.123.123
remote hosts: 172.16.100.10
测试结果:
1、配置
增加了一个真实ip X-Forwarded-For,并且顺序是增加到了“后面”。
2、配置
清空了客户端伪造传入的X-Forwarded-For,
保证了使用 request.getHeader("x-forwarded-for") 获取的ip为真实ip,
或者用“,”分隔,截取 X-Forwarded-For 最后的值。
http://www.cnblogs.com/zhengyun_ustc/archive/2012/09/19/getremoteaddr.html
在Servlet里,获取客户端的IP地址的方法是:request.getRemoteAddr(),这种方法在大部分情况下都是有效的。但是在通过了Apache,Squid,Nginx等反向代理软件就不能获取到客户端的真实IP地址了。
如果使用了反向代理软件,例如将http://192.168.101.88:80/ 的URL反向代理为http://pay.kedou.com/ 的URL时,用request.getRemoteAddr()方法获取的IP地址是:127.0.0.1 或192.168.101.88,而并不是客户端的真实IP。
如下图,原来是client端直接请求服务端,走A路线请求,这时候通过request.getRemoteAddr()方法可以准备的获取客户端的IP。但是做了代理之后呢,client端不是直接请求服务端,而是走B线路请求代理服务器,由代理器去请求服务端,这时候服务端通过request.getRemoteAddr()方法拿到的理所当然是代理服务器的地址了。
经过代理以后,由于在客户端和服务之间增加了中间层,因此服务器无法直接拿到客户端的IP,服务器端应用也无法直接通过转发请求的地址返回给客户端。但是在转发请求的HTTP头信息中,增加了X-FORWARDED-FOR信息。用以跟踪原有的客户端IP地址和原来客户端请求的服务器地址。当我们访问http://www.xxx.com/index.jsp/ 时,其实并不是我们浏览器真正访问到了服务器上的index.jsp文件,而是先由代理服务器去访问http://192.168.1.110:2046/index.jsp ,代理服务器再将访问到的结果返回给我们的浏览器,因为是代理服务器去访问index.jsp的,所以index.jsp中通过request.getRemoteAddr()的方法获取的IP实际上是代理服务器的地址,并不是客户端的IP地址。
下面的获取真实IP的Java类:
/**
* 常用获取客户端信息的工具
*
*/
public final class NetworkUtil {
/**
* Logger for this class
*/
private static Logger logger = Logger.getLogger(RandomCodeUtils.class); /**
* 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址;
*
* @param request
* @return
* @throws IOException
*/
public final static String getIpAddress(HttpServletRequest request) throws IOException {
// 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址 String ip = request.getHeader("X-Forwarded-For");
if (logger.isInfoEnabled()) {
logger.info("getIpAddress(HttpServletRequest) - X-Forwarded-For - String ip=" + ip);
} if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
if (logger.isInfoEnabled()) {
logger.info("getIpAddress(HttpServletRequest) - Proxy-Client-IP - String ip=" + ip);
}
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
if (logger.isInfoEnabled()) {
logger.info("getIpAddress(HttpServletRequest) - WL-Proxy-Client-IP - String ip=" + ip);
}
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
if (logger.isInfoEnabled()) {
logger.info("getIpAddress(HttpServletRequest) - HTTP_CLIENT_IP - String ip=" + ip);
}
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
if (logger.isInfoEnabled()) {
logger.info("getIpAddress(HttpServletRequest) - HTTP_X_FORWARDED_FOR - String ip=" + ip);
}
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if (logger.isInfoEnabled()) {
logger.info("getIpAddress(HttpServletRequest) - getRemoteAddr - String ip=" + ip);
}
}
} else if (ip.length() > 15) {
String[] ips = ip.split(",");
for (int index = 0; index < ips.length; index++) {
String strIp = (String) ips[index];
if (!("unknown".equalsIgnoreCase(strIp))) {
ip = strIp;
break;
}
}
}
return ip;
}
}
http://blog.csdn.net/yin_jw/article/details/24470131
request方法客户端IP: request.getRemoteAddr() 输出:192.168.0.106
客户端主机名:request.getRemoteHost()输出:abc
request.getHeader("Host") 输出:192.168.0.1:8080
Web服务器名字:request.getServerName()输出:192.168.0.1
服务器监听的端口:request.getServerPort()输出:8080
在JSP里,获取客户端的IP地址的方法是:request.getRemoteAddr(),这种方法在大部分情况下都是有效的。
但是在通过了 Apache,Squid等反向代理软件就不能获取到客户端的真实IP地址了。
如果使用了反向代理软件,用 request.getRemoteAddr()方法获取的IP地址是:127.0.0.1或 192.168.1.110,而并不是客户端的真实IP。
经过代理以后,由于在客户端和服务之间增加了中间层,因此服务器无法直接拿到客户端的 IP,服务器端应用也无法直接通过转发请求的地址返回给客户端。
但是在转发请求的HTTP头信息中,增加了X-FORWARDED-FOR信息。用以跟踪 原有的客户端IP地址和原来客户端请求的服务器地址。
当我们访问index.jsp/时,其实并不是我们浏览器真正访问到了服务器上的index.jsp 文件,而是先由代理服务器去访问index.jsp ,
代理服务器再将访问到的结果返回给我们的浏览器,因为是代理服务器去访问index.jsp的,
所以index.jsp中通过 request.getRemoteAddr()的方法获取的IP实际上是代理服务器的地址,并不是客户端的IP地址。
于是可得出获得客户端真实IP地址 的方法一:
public String getRemortIP(HttpServletRequest request) {
if (request.getHeader("x-forwarded-for") == null) {
return request.getRemoteAddr();
}
return request.getHeader("x-forwarded-for");
}
获得客户端真实IP地址的方法二:
public String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,
究竟哪个才是真正的用户端的真实IP呢?答案是取 X-Forwarded-For中第一个非unknown的有效IP字符串。
如: X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130, 192.168.1.100 用户真实IP为: 192.168.1.110
http://my.oschina.net/fuweiwei/blog/299316?p=1
今天要说的是 HTTP 请求头中的 X-Forwarded-For(XFF)。
背景
通过名字就知道,X-Forwarded-For 是一个 HTTP 扩展头部。HTTP/1.1(RFC 2616)协议并没有对它的定义,它最开始是由 Squid 这个缓存代理软件引入,用来表示 HTTP 请求端真实 IP。如今它已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用,并被写入 RFC 7239(Forwarded HTTP Extension)标准之中。
X-Forwarded-For 请求头格式非常简单,就这样:
X-Forwarded-For: client, proxy1, proxy2
可以看到,XFF 的内容由「英文逗号 + 空格」隔开的多个部分组成,最开始的是离服务端最远的设备 IP,然后是每一级代理设备的 IP。
如果一个 HTTP 请求到达服务器之前,经过了三个代理 Proxy1、Proxy2、Proxy3,IP 分别为 IP1、IP2、IP3,用户真实 IP 为 IP0,那么按照 XFF 标准,服务端最终会收到以下信息:
X-Forwarded-For: IP0, IP1, IP2
Proxy3 直连服务器,它会给 XFF 追加 IP2,表示它是在帮 Proxy2 转发请求。列表中并没有 IP3,IP3 可以在服务端通过 Remote Address 字段获得。我们知道 HTTP 连接基于 TCP 连接,HTTP 协议中没有 IP 的概念,Remote Address 来自 TCP 连接,表示与服务端建立 TCP 连接的设备 IP,在这个例子里就是 IP3。
Remote Address 无法伪造,因为建立 TCP 连接需要三次握手,如果伪造了源 IP,无法建立 TCP 连接,更不会有后面的 HTTP 请求。不同语言获取 Remote Address 的方式不一样,例如 php 是 $_SERVER["REMOTE_ADDR"]
,Node.js 是req.connection.remoteAddress
,但原理都一样。
问题
有了上面的背景知识,开始说问题。我用 Node.js 写了一个最简单的 Web Server 用于测试。HTTP 协议跟语言无关,这里用 Node.js 只是为了方便演示,换成任何其他语言都可以得到相同结论。另外本文用 Nginx 也是一样的道理,如果有兴趣,换成 Apache 或其他 Web Server 也一样。
下面这段代码会监听 9009
端口,并在收到 HTTP 请求后,输出一些信息:
JSvar http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write('remoteAddress: ' + req.connection.remoteAddress + '\n');
res.write('x-forwarded-for: ' + req.headers['x-forwarded-for'] + '\n');
res.write('x-real-ip: ' + req.headers['x-real-ip'] + '\n');
res.end();
}).listen(9009, '0.0.0.0');
这段代码除了前面介绍过的 Remote Address 和 X-Forwarded-For
,还有一个 X-Real-IP
,这又是一个自定义头部字段。X-Real-IP
通常被 HTTP 代理用来表示与它产生 TCP 连接的设备 IP,这个设备可能是其他代理,也可能是真正的请求端。需要注意的是,X-Real-IP
目前并不属于任何标准,代理和 Web 应用之间可以约定用任何自定义头来传递这个信息。
现在可以用域名 + 端口号直接访问这个 Node.js 服务,再配一个 Nginx 反向代理:
NGINXlocation / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://127.0.0.1:9009/;
proxy_redirect off;
}
我的 Nginx 监听 80
端口,所以不带端口就可以访问 Nginx 转发过的服务。
测试直接访问 Node 服务:
BASHcurl http://t1.imququ.com:9009/
remoteAddress: 114.248.238.236
x-forwarded-for: undefined
x-real-ip: undefined
由于我的电脑直接连接了 Node.js 服务,Remote Address 就是我的 IP。同时我并未指定额外的自定义头,所以后两个字段都是 undefined。
再来访问 Nginx 转发过的服务:
BASHcurl http://t1.imququ.com/
remoteAddress: 127.0.0.1
x-forwarded-for: 114.248.238.236
x-real-ip: 114.248.238.236
这一次,我的电脑是通过 Nginx 访问 Node.js 服务,得到的 Remote Address 实际上是 Nginx 的本地 IP。而前面 Nginx 配置中的这两行起作用了,为请求额外增加了两个自定义头:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
实际上,在生产环境中部署 Web 应用,一般都采用上面第二种方式,有很多好处。但这就引入一个隐患:很多 Web 应用为了获取用户真正的 IP,从 HTTP 请求头中获取 IP。
HTTP 请求头可以随意构造,我们通过 curl 的 -H
参数构造 X-Forwarded-For
和 X-Real-IP
,再来测试一把。
直接访问 Node.js 服务:
BASHcurl http://t1.imququ.com:9009/ -H 'X-Forwarded-For: 1.1.1.1' -H 'X-Real-IP: 2.2.2.2'
remoteAddress: 114.248.238.236
x-forwarded-for: 1.1.1.1
x-real-ip: 2.2.2.2
对于 Web 应用来说,X-Forwarded-For
和 X-Real-IP
就是两个普通的请求头,自然就不做任何处理原样输出了。这说明,对于直连部署方式,除了从 TCP 连接中得到的 Remote Address 之外,请求头中携带的 IP 信息都不能信。
访问 Nginx 转发过的服务:
BASHcurl http://t1.imququ.com/ -H 'X-Forwarded-For: 1.1.1.1' -H 'X-Real-IP: 2.2.2.2'
remoteAddress: 127.0.0.1
x-forwarded-for: 1.1.1.1, 114.248.238.236
x-real-ip: 114.248.238.236
这一次,Nginx 会在 X-Forwarded-For
后追加我的 IP;并用我的 IP 覆盖 X-Real-IP
请求头。
这说明,有了 Nginx 的加工,X-Forwarded-For
最后一节以及 X-Real-IP
整个内容无法构造,可以用于获取用户 IP。
用户 IP 往往会被使用与跟 Web 安全有关的场景上,例如检查用户登录地区,基于 IP 做访问频率控制等等。这种场景下,确保 IP 无法构造更重要。经过前面的测试和分析,对于直接面向用户部署的 Web 应用,必须使用从 TCP 连接中得到的 Remote Address;对于部署了 Nginx 这样反向代理的 Web 应用,在正确配置了 Set Header 行为后,可以使用 Nginx 传过来的 X-Real-IP
或 X-Forwarded-For
最后一节(实际上它们一定等价)。
那么,Web 应用自身如何判断请求是直接过来,还是由可控的代理转发来的呢?在代理转发时增加额外的请求头是一个办法,但是不怎么保险,因为请求头太容易构造了。如果一定要这么用,这个自定义头要够长够罕见,还要保管好不能泄露出去。
判断 Remote Address 是不是本地 IP 也是一种办法,不过也不完善,因为在 Nginx 所处服务器*问,无论直连还是走 Nginx 代理,Remote Address 都是 127.0.0.1。这个问题还好通常可以忽略,更麻烦的是,反向代理服务器和实际的 Web 应用不一定部署在同一台服务器上。所以更合理的做法是收集所有代理服务器 IP 列表,Web 应用拿到 Remote Address 后逐一比对来判断是以何种方式访问。
通常,为了简化逻辑,生产环境会封掉通过带端口直接访问 Web 应用的形式,只允许通过 Nginx 来访问。那是不是这样就没问题了呢?也不见得。
首先,如果用户真的是通过代理访问 Nginx,X-Forwarded-For
最后一节以及 X-Real-IP
得到的是代理的 IP,安全相关的场景只能用这个,但有些场景如根据 IP 显示所在地天气,就需要尽可能获得用户真实 IP,这时候 X-Forwarded-For
中第一个 IP 就可以排上用场了。这时候需要注意一个问题,还是拿之前的例子做测试:
BASHcurl http://t1.imququ.com/ -H 'X-Forwarded-For: unknown, <>"1.1.1.1'
remoteAddress: 127.0.0.1
x-forwarded-for: unknown, <>"1.1.1.1, 114.248.238.236
x-real-ip: 114.248.238.236
X-Forwarded-For
最后一节是 Nginx 追加上去的,但之前部分都来自于 Nginx 收到的请求头,这部分用户输入内容完全不可信。使用时需要格外小心,符合 IP 格式才能使用,不然容易引发 SQL 注入或 XSS 等安全漏洞。
结论
- 直接对外提供服务的 Web 应用,在进行与安全有关的操作时,只能通过 Remote Address 获取 IP,不能相信任何请求头;
- 使用 Nginx 等 Web Server 进行反向代理的 Web 应用,在配置正确的前提下,要用
X-Forwarded-For
最后一节 或X-Real-IP
来获取 IP(因为 Remote Address 得到的是 Nginx 所在服务器的内网 IP);同时还应该禁止 Web 应用直接对外提供服务; - 在与安全无关的场景,例如通过 IP 显示所在地天气,可以从
X-Forwarded-For
靠前的位置获取 IP,但是需要校验 IP 格式合法性;
PS:网上有些文章建议这样配置 Nginx,其实并不合理:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
这样配置之后,安全性确实提高了,但是也导致请求到达 Nginx 之前的所有代理信息都被抹掉,无法为真正使用代理的用户提供更好的服务。还是应该弄明白这中间的原理,具体场景具体分析。
本文链接:https://imququ.com/post/x-forwarded-for-header-in-http.html,参与评论 »
https://imququ.com/series.html#toc-7
/**
*
* getRequestURL:(将HttpServletRequest转成URL参数字符串 ). <br/>
*
*/
public static String getRequestURL(HttpServletRequest request) { StringBuilder sb = new StringBuilder();
boolean isFirst = true;
@SuppressWarnings("unchecked")
Enumeration<String> er = request.getParameterNames();
while (er.hasMoreElements()) {
String name = (String) er.nextElement();
String value = request.getParameter(name);
if (isFirst) {
sb.append(name + "=" + value);
isFirst = false;
}
else {
if (value != null) {
sb.append("&" + name + "=" + value);
}
else {
sb.append("&" + name + "=");
}
}
}
return sb.toString();
}
import com.util.JsonUtils;
import com.util.LoggerUtils;
import com.util.StringUtil;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger; import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern; public class WebUtils {
protected final static transient Logger dbLogger = LoggerUtils.getLogger(WebUtils.class);
private static final List<String> DEFAULT_SENSITIVE = Arrays.asList("mobile", "pass", "sign", "encode", "token", "check", "card");
private static final List<String> IGNORE_KEYS = Arrays.asList("mobileType");
/**
* 查询串提取
*
* @param queryStr
* @param encode
* @return
*/
private static Pattern QUERY_MAP_PATTERN = Pattern.compile("&?([^=&]+)="); /**
* 获取用户真实的IP,如果是系统之间内部API调用,则通过特殊的 header 加入用户真实IP
*
* @param request
* @return
*/
public static final String getClientIp(HttpServletRequest request) {
String clientIp = getRemoteIp(request);
return clientIp;
} public static final String getRemoteIp(HttpServletRequest request) {
String xfwd = request.getHeader("X-Forwarded-For");
String result = getRemoteIpFromXfwd(xfwd);
if (StringUtils.isNotBlank(result)) {
return result;
}
return request.getRemoteAddr();
} private static String getRemoteIpFromXfwd(String xfwd) {
String tmpip = null;
if (StringUtils.isNotBlank(xfwd)) {
String[] ipList = xfwd.split(",");
for (int i = ipList.length - 1; i >= 0; i--) {
String ip = ipList[i];
ip = StringUtils.trim(ip);
return ip;
}
}
return tmpip;
} public static final void writeJsonResponse(HttpServletResponse res, boolean success, String retval) {
res.setContentType("application/json;charset=utf-8");
res.setCharacterEncoding("utf-8");
try {
PrintWriter writer = res.getWriter();
Map jsonMap = new HashMap();
jsonMap.put("success", success);
if (!success) {
jsonMap.put("msg", retval);
} else {
jsonMap.put("retval", retval);
}
writer.write("var data=" + JsonUtils.writeObjectToJson(jsonMap));
res.flushBuffer();
} catch (IOException e) {
}
} public static final String getAttributeStr(HttpServletRequest request, String spliter) {
String paramsStr = "";
String tmpname;
Enumeration params = request.getAttributeNames();
while (params.hasMoreElements()) {
tmpname = (String) params.nextElement();
paramsStr += tmpname + "=" + request.getAttribute(tmpname) + spliter;
}
return paramsStr;
} public static final String getHeaderStr(HttpServletRequest request) {
return "" + getHeaderMap(request);
} public static final Map<String, String> getRequestMap(HttpServletRequest request) {
Map<String, String> result = new HashMap<>();
Enumeration<String> it = request.getParameterNames();
String key = null;
while (it.hasMoreElements()) {
key = it.nextElement();
result.put(key, request.getParameter(key));
}
return result;
} public static final Map<String, String> getHeaderMap(HttpServletRequest request) {
Map<String, String> result = new HashMap<>();
Enumeration<String> it = request.getHeaderNames();
String key = null;
while (it.hasMoreElements()) {
key = it.nextElement();
String value = request.getHeader(key);
//禁止cookie日志打印
if (StringUtils.containsIgnoreCase(key, "cookie")) {
value = "*******";
}
result.put(key, value);
}
return result;
} /**
* 返回Map,但key=“head4”+originalKey
*
* @param request
* @return
*/
public static final Map<String, String> getHeaderMapWidthPreKey(HttpServletRequest request) {
Map<String, String> result = new HashMap<>();
Enumeration<String> it = request.getHeaderNames();
String key = null;
while (it.hasMoreElements()) {
key = it.nextElement();
String value = request.getHeader(key);
//禁止cookie日志打印
if (StringUtils.containsIgnoreCase(key, "cookie")) {
value = "*******";
}
result.put("head4" + StringUtils.lowerCase(key), value);
}
return result; } public static final void clearCookie(HttpServletResponse response, String path, String cookieName) {
Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0);
cookie.setPath(path);
response.addCookie(cookie);
} public static final boolean isRobot(String userAgent) {
return StringUtils.containsIgnoreCase(userAgent, "spider") ||
StringUtils.containsIgnoreCase(userAgent, "Googlebot") ||
StringUtils.containsIgnoreCase(userAgent, "robot");
} public static final boolean isAjaxRequest(HttpServletRequest request) {
boolean result = StringUtils.isNotBlank(request.getHeader("X-Requested-With"));
return result;
} public static final void addCookie(HttpServletResponse response, String cookiename, String cookievalue, String path, int maxSecond) {
addCookie(response, cookiename, cookievalue, path, maxSecond, true);
} public static final void addCookie(HttpServletResponse response, String cookiename, String cookievalue, String path, int maxSecond, boolean httpOnly) {
Cookie cookie = new Cookie(cookiename, cookievalue);
cookie.setPath(path);
cookie.setMaxAge(maxSecond);// 24 hour
cookie.setHttpOnly(httpOnly);
response.addCookie(cookie);
} public static final Cookie getCookie(HttpServletRequest request, String cookiename) {
Cookie cookies[] = request.getCookies();
if (cookies == null)
return null;
for (Cookie cookie : cookies) {
if (cookiename.equals(cookie.getName())) {
return cookie;
}
}
return null;
} public static final String getCookieValue(HttpServletRequest request, String cookiename) {
Cookie cookie = getCookie(request, cookiename);
if (cookie == null)
return null;
return cookie.getValue();
} public static final String joinParams(Map params, boolean ignoreBlank) {
StringBuilder content = new StringBuilder();
List<String> keys = new ArrayList(params.keySet());
Collections.sort(keys);
for (String key : keys) {
Object value = params.get(key);
if (!ignoreBlank || value != null && StringUtils.isNotBlank("" + value))
content.append(key).append("=").append(value).append("&");
}
if (content.length() > 0)
content.deleteCharAt(content.length() - 1);
return content.toString();
} public static final boolean checkString(String str) {
if (StringUtils.isBlank(str))
return false;
if (StringUtils.contains(StringUtils.lowerCase(str), "<script"))
return true;// 验证JS
//// 验证iframe
return StringUtils.contains(StringUtils.lowerCase(str), "<iframe");
} public static final boolean checkPropertyAll(Object entity) {
try {
Map result = PropertyUtils.describe(entity);
for (Object key : result.keySet()) {
if (result.get(key) instanceof String) {
if (checkString(result.get(key) + ""))
return true;
}
}
} catch (Exception ex) {
}
return false;
} public static final Map<String, String> getRequestParams(HttpServletRequest request, String... pnames) {
Map<String, String> result = new TreeMap<>();
if (pnames != null) {
for (String pn : pnames) {
String pv = request.getParameter(pn);
if (StringUtils.isNotBlank(pv)) result.put(pn, pv);
}
}
return result;
} /**
* 判断用户浏览器信息
*/
public static final String getBrowerInfo(String userAgent) {
String browserInfo = "UNKNOWN";
String info = StringUtils.lowerCase(userAgent);
try {
String[] strInfo = info.substring(info.indexOf("(") + 1, info.indexOf(")") - 1).split(";");
if ((info.indexOf("msie")) > -1) {
return strInfo[1].trim();
} else {
String[] str = info.split(" ");
if (!info.contains("navigator") && info.contains("firefox")) {
return str[str.length - 1].trim();
} else if ((info.indexOf("opera")) > -1) {
return str[0].trim();
} else if (!info.contains("chrome") && info.contains("safari")) {
return str[str.length - 1].trim();
} else if (info.contains("chrome")) {
return str[str.length - 2].trim();
} else if (info.contains("navigator")) {
return str[str.length - 1].trim();
}
}
} catch (Exception e) {
}
return browserInfo;
} /**
* @param queryString encoded queryString
* queryString is already encoded (e.g %20 and & may be present)
* @param encode
* @return
*/
public static final Map<String, String> parseQueryStr(String queryString, String encode) {
Map<String, String> map = new LinkedHashMap<>();
if (StringUtils.isBlank(queryString)) return map;
Matcher matcher = QUERY_MAP_PATTERN.matcher(queryString);
String key = null, value;
int end = 0;
while (matcher.find()) {
if (key != null) {
try {
value = queryString.substring(end, matcher.start());
if (StringUtils.isNotBlank(value)) {
value = URLDecoder.decode(value, encode);
map.put(key, value);
}
} catch (UnsupportedEncodingException e) {
dbLogger.error(LoggerUtils.getExceptionTrace(e));
}
}
key = matcher.group(1);
end = matcher.end();
}
if (key != null) {
try {
value = queryString.substring(end);
if (StringUtils.isNotBlank(value)) {
value = URLDecoder.decode(value, encode);
map.put(key, value);
}
} catch (UnsupportedEncodingException e) {
dbLogger.error(LoggerUtils.getExceptionTrace(e));
}
}
return map;
} public static final String getQueryStr(HttpServletRequest request, String encode) {
return getQueryStr(flatRequestMap(request.getParameterMap(), ","), encode);
} public static final Map<String, String> flatRequestMap(Map<String, String[]> reqMap, String joinChar) {
Map<String, String> flatMap = new HashMap<>();
for (String key : reqMap.keySet()) {
flatMap.put(key, StringUtils.join(reqMap.get(key), joinChar));
}
return flatMap;
} /**
* 多值用“,”号隔开
*
* @param requestMap
* @param encode
* @return
*/
public static final String getQueryStr(Map<String, String> requestMap, String encode) {
if (requestMap == null || requestMap.isEmpty()) return "";
String result = "";
for (String name : requestMap.keySet()) {
try {
result += name + "=" + URLEncoder.encode(requestMap.get(name), encode) + "&";
} catch (UnsupportedEncodingException e) {
}
}
return result.substring(0, result.length() - 1);
} public static final String encodeParam(String params, String encode) {
Map<String, String> paramMap = parseQueryStr(params, encode);
String result = "";
for (String key : paramMap.keySet()) {
try {
result += "&" + key + "=" + URLEncoder.encode(paramMap.get(key), encode);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
if (StringUtils.isNotBlank(result)) return result.substring(1);
return "";
} public static final String getContextPath(HttpServletRequest request) {
String contextPath = request.getContextPath();
if (!StringUtils.endsWith(contextPath, "/")) contextPath += "/";
return contextPath;
} public static final String getParamStr(HttpServletRequest request, boolean removeSensitive, String... sensitiveKeys) {
Map<String, String> requestMap = getRequestMap(request);
if (removeSensitive) {
removeSensitiveInfo(requestMap, sensitiveKeys);
}
return "" + requestMap;
} public static final void removeSensitiveInfo(Map<String, String> params, String... keys) {
List<String> keyList = null;
if (keys != null) {
keyList = new ArrayList(DEFAULT_SENSITIVE);
keyList.addAll(Arrays.asList(keys));
} else {
keyList = DEFAULT_SENSITIVE;
} for (String pname : new ArrayList<>(params.keySet())) {
int valueLen = StringUtils.length(params.get(pname));
if (valueLen > 1000) {
params.put(pname, StringUtils.substring(params.get(pname), 1000) + "->LEN:" + valueLen);
}
if (!IGNORE_KEYS.contains(pname)) {
for (String key : keyList) {
if (StringUtils.containsIgnoreCase(pname, key) && StringUtils.isNotBlank(params.get(pname))) {
params.put(pname, "MG" + StringUtil.md5("kcj3STidSC" + params.get(pname)));
}
}
}
}
} public static final String getRemotePort(HttpServletRequest request) {//获取请求端口号
String port = request.getHeader("x-client-port");
if (StringUtils.isBlank(port)) {
return "" + request.getRemotePort();
}
return port;
} public static final void writeRedirect(HttpServletResponse response, String url) {
try {
response.setContentType("text/html;charset=UTF-8");
Writer writer = response.getWriter();
writer.write("<!DOCTYPE html>\n");
writer.write("<html><body><script>window.location.href=\"" + url + "\";</script></body></html>");
writer.flush();
} catch (IOException e) {
dbLogger.warn(LoggerUtils.getExceptionTrace(e, 10));
}
} public static String getPostBody(HttpServletRequest request) {
String encode = "utf-8";
BufferedReader in = null;
String result = "";
try {
in = new BufferedReader(new InputStreamReader(request.getInputStream(), encode));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
dbLogger.error(LoggerUtils.getExceptionTrace(e, 5));
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
dbLogger.error(LoggerUtils.getExceptionTrace(e, 5));
}
}
}
return result;
} public String checkScript(HttpServletRequest request) {
String match = "onclick|onfocus|onblur|onload|onerror";
for (String[] v : request.getParameterMap().values()) {
for (String value : v) {
String script = StringUtil.findFirstByRegex(value, match);
if (StringUtils.isNotBlank(script)) {
return script;
}
}
}
return "";
}
}