在不使用 Cookie 的情况下识别用户计算机






2.64/5 (6投票s)
在不使用 Cookie 的情况下识别用户计算机
引言
几天前,我接到一项任务,要求找到一种不使用 cookie 或 IP 地址即可识别用户计算机的特殊方法。当用户首次访问 Web 应用程序时,用户会收到“不,您以前未连接过,是新用户”的消息,否则,如果不是第一次访问,用户将收到“是,您以前连接过”的消息。因此,我研究了 HTTP/1.0 协议以获得一些线索,并根据以下缓存信息提出了识别计算机的思路。
背景
Web 浏览器(HTTP 客户端应用程序)与 Web 服务器(HTTP 服务器应用程序)之间的通信以请求和响应的形式进行。请求从 Web 浏览器发送到 Web 服务器,响应从 Web 服务器返回到 Web 浏览器。每个请求或响应都包含两个部分:标头和数据。标头提供有关请求或响应的额外信息和设置。数据部分包含实际需要的数据。
大多数浏览器会缓存它们从 Web 服务器接收到的 Internet 数据。因此,当用户再次访问之前访问过的内容时,Web 浏览器会询问 Web 服务器自上次接收以来内容是否已更改,并将上次访问的时间戳与“内容请求”一起发送到 Web 服务器,这称为条件 GET。只有当内容自浏览器接收到时间戳以来发生修改时,Web 服务器才会返回内容。通过这种方式,浏览速度得以加快,因为如果缓存的数据未修改,则无需下载。在将内容传递给浏览器的过程中,Web 服务器可能会告知 Web 浏览器内容的最后修改日期,如果这样做,Web 浏览器(如 MSIE 和 Mozilla Firefox 等著名浏览器)会将相同的时间戳返回给 Web 服务器,以查询自该时间以来内容是否已修改。
内容的最后修改日期通过 Last-Modified 标头从 Web 服务器发送到 Web 浏览器。在第二次访问时,Web 浏览器会通过在 If-Modified-Since 标头中指定最后修改时间戳来请求内容。
Using the Code
这是一个包含 `static` 函数的小程序。代码将在下面简要解释。以下系统命名空间被使用
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Net;
using System.IO;
`System.Collections.Generic` 命名空间用于包含 `Dictionary` 对象。`System.Net` 和 `System.Net.Socket` 用于实现 TCP/IP 网络功能。`System.IO` 用于包含 `TextReader` 和 `TextWriter` 功能。
主过程描述如下
public static Dictionary<string, string> headers = null;
static void Main(string[] args)
{
TcpListener listener = null;
try
{
IPAddress address = Dns.GetHostEntry( Dns.GetHostName() ).AddressList[0];
Console.WriteLine("Info: server start at IP: " + address + " Port: 80");
listener = new TcpListener(address, 80);
listener.Start();
while (true)
{
try
{
Socket conn = listener.AcceptSocket();
Console.WriteLine("*************************************");
Console.WriteLine("Info: Connection established,
Connected to IP: " + ((IPEndPoint)conn.RemoteEndPoint).Address +
" Port: " + ((IPEndPoint)conn.RemoteEndPoint).Port);
Console.WriteLine("*************************************");
NetworkStream stream = new NetworkStream(conn);
TextReader reader = new StreamReader(stream);
TextWriter writer = new StreamWriter(stream);
if (ParseRequest(reader))
{
if ("GET" == method)
{
headers = new Dictionary<string, string>();
while (ReadNParseHeader(reader)) ;
if ("/" == resource)
{
SendHTMLIdentifyUser(writer);
}
else
{
Console.WriteLine("Warning: Invalid resource: \""
+ resource + "\" requested");
}
}
else
{
Console.WriteLine
("Warning: Only GET method supported, Closing connection");
}
}
Console.WriteLine("Info: Closing Connection Successfully");
Console.WriteLine("-------------------------------------");
writer.Close();
reader.Close();
stream.Close();
conn.Close();
}
catch (Exception exception)
{
Console.WriteLine("Warning: " + exception.Message);
}
}
}
catch (Exception exception)
{
Console.WriteLine("ERROR: " + exception.Message);
}
finally
{
if (null != listener)
{
Console.WriteLine("Info: Stopping listener");
listener.Stop();
listener = null;
}
}
Console.WriteLine("Program Ended, Press ENTER to exit");
Console.ReadLine();
}
`'header'` 对象是 `Dictionary` 类型,其键和值都是 `string` 类型;它将在程序稍后使用。
首先,获取 IP 地址并与端口一起打印到控制台,以便用户知道服务器正在监听哪个套接字(以避免混淆,以防计算机分配了多个 IP 地址)。然后,在套接字上创建并启动监听器。然后获取远程套接字以服务它。然后创建 `TextReader` 和 `TextWriter` 对象,以便使用文本流进行网络通信;因为大多数 HTTP 通信通常是纯文本。
第一个元素是来自 HTTP 客户端(Web 浏览器)的基本请求。这包括方法、内容标识符和位置,以及 HTTP 版本。`ParseRequest` 过程解析此基本请求,并将方法、资源和协议分别放入 `method`、`resource` 和 `httpProtocol` `static string` 对象中。
HTTP 服务器有三种基本类型的请求:`GET`、`HEAD` 和 `PUT`。这里,简单的程序仅支持 `GET` 请求。
在服务远程套接字时,首先使用 `ReadNParseHeader` 读取和解析请求标头,它将每个标头的标题和值放入 header 字典对象中。“/”资源指定了这里唯一支持的默认内容。然后使用 `SendHTMLIdentifyUser` 过程将 HTML 发送给客户端。
下面描述 `ParseRequest` 过程
public static string method,
resourceLoc,
resource,
queryString,
httpProtocol; // HTTP/1.0, always assuming it
private static bool ParseRequest(TextReader reader)
{
string request = ReadUntilCRLF(reader);
Console.WriteLine("Info: Request received: \"" + request + "\"");
string[] tokens = request.Split(new string[] { " " },
StringSplitOptions.RemoveEmptyEntries);
if (3 != tokens.Length)
{
Console.WriteLine("Warning: Request must split in 3 tokens");
return false;
}
// method
method = tokens[0].ToUpper();
// query string
queryString = "";
int indexEnd = tokens[1].IndexOf('?');
if (indexEnd < 0)
{
indexEnd = tokens[1].Length;
}
else
{
queryString = tokens[1].Substring(indexEnd, tokens[1].Length - indexEnd);
}
// resource
int indexLastSeperator = tokens[1].LastIndexOf('/');
int resLen = indexEnd - indexLastSeperator;
resource = tokens[1].Substring(indexLastSeperator, resLen);
// resourceLocation
if (0 == tokens[1].ToLower().IndexOf("http://")) // absolute path in request
{
int indexSeperator = tokens[1].IndexOf('/', 7); // http:// are 7 chars
resourceLoc = tokens[1].Substring(indexLastSeperator,
indexEnd - indexLastSeperator - resLen);
}
else
{
resourceLoc = tokens[1].Substring(0, indexEnd - resLen);
}
// protocol
httpProtocol = tokens[2].ToUpper();
Console.WriteLine("Info: Method: " + method);
Console.WriteLine("Info: Resource Location: " + resourceLoc);
Console.WriteLine("Info: Resource: " + resource);
Console.WriteLine("Info: Query String: " + queryString);
Console.WriteLine("Info: Protocol: " + httpProtocol);
return true;
}
正如我之前所说,Web 浏览器发送给 Web 服务器的第一个实体是基本请求,它由请求方法、资源标识符和 HTTP 版本组成。查询字符串中的其他内容可能包括资源位置和查询字符串。资源位置可以是相对的或绝对的。`ParseRequest` 函数仅分离此信息并将其存储在相应的 `static string` 对象中,即 `method`、`resourceLoc`、`resource`、`queryString` 和 `httpProtocol`。
下面描述 `ReadNParseHeader` 过程
private static bool ReadNParseHeader(TextReader reader)
{
string header = ReadUntilCRLF(reader);
if (header.Length > 0)
{
Console.WriteLine("Info: Header received: \"" + header + "\"");
string[] tokens = header.Split(new string[] { ": " },
StringSplitOptions.RemoveEmptyEntries);
if (tokens.Length == 2)
{
headers.Add(tokens[0].ToUpper(), tokens[1]);
}
else
{
Console.WriteLine("Warning: Cannot Parse header");
}
return true; // headers follow
}
else
{
return false; // end of headers
}
}
HTTP 请求和响应标头遵循严格的格式。每个标头由标头标题后跟冒号 `:` 然后是空格,最后是该标头的值组成。每个标头都以回车符和换行符 '\r\n' 终止。标头部分的结束由最后一个标头后的额外回车符和换行符指定。`ReadNParseHeader` 解析每个标头并将标头标题和标头值存储在字典中。如果存在更多标头,此过程将返回 `true`,否则返回 `false`。
`ParseRequest` 和 `ReadNParseHeader` 过程使用下面描述的 `ReadUntilCRLF` 过程
private static string ReadUntilCRLF(TextReader reader)
{
string strLine = "";
char prevChar = '\0',
currChar = (char)reader.Read();
while (!('\r' == prevChar && '\n' == currChar))
{
strLine += currChar;
prevChar = currChar;
currChar = (char)reader.Read();
}
strLine = strLine.Substring(0, strLine.Length - 1); // remove prevChar = "\r"
return strLine;
}
此函数逐个字符地读取文本流,直到找到回车符和换行符。它返回直到遇到的分隔符 ('\r\n') 之前的所有字符串。
实际导致识别计算机的函数是 `SendHTMLIdentifyUser`;它在下面描述
private static void SendHTMLIdentifyUser(TextWriter writer)
{
string html = "<HTML><BODY>Hello! How are you? ";
bool userNew = true;
string keyIfModifiedSince = "IF-MODIFIED-SINCE";
foreach (string key in headers.Keys)
{
if (key == keyIfModifiedSince)
{
userNew = false;
break;
}
}
int currentId = -1;
string lastModified = DateTime.Now.ToString("R");
writer.Write("HTTP/1.0 200 OK\r\n");
writer.Write("Content-Type: text/HTML\r\n");
writer.Write("Last-Modified: " + lastModified + "\r\n");
string strIden = "";
if (userNew)
{
currentId = machineId++;
strIden = "No, you have not connected before and are a new user";
}
else
{
string lastDate = headers[keyIfModifiedSince];
int indexSep = lastDate.IndexOf(';');
if (indexSep < 0)
{
indexSep = lastDate.Length;
}
lastDate = lastDate.Substring(0, indexSep);
try
{
currentId = machineIdentification[lastDate];
machineIdentification.Remove(lastDate);
strIden = "Yes, you have connected before";
}
catch (Exception)
{
currentId = machineId++;
strIden = "No, you have not connected before and are a new user";
}
}
html += strIden + "</BODY></HTML>";
machineIdentification.Add(lastModified, currentId);
writer.Write("Content-Length: " + html.Length + "\r\n");
writer.Write("\r\n");
writer.Write(html);
Console.WriteLine("Info: Machine Id: " + currentId);
}
上述函数中应用的技巧是,首先,在标头字段中搜索 IF-MODIFIED-SINCE 标头,它的存在意味着用户之前已经访问过;另一方面,它的不存在意味着用户是第一次访问。如果用户已经访问过该网站,IF-MODIFIED-SINCE 请求标头中的时间戳将帮助我们获取用户配置文件(在本例中为用户 ID);否则,如果用户是第一次访问,我们就必须为用户创建一个新配置文件(在本例中为新用户 ID)。获取当前时间戳,并将用户配置文件与此时间戳进行映射,然后将时间戳作为 Last-Modified 标头发送给用户;以及用户请求的资源。这样就可以借助 IF-MODIFIED-SINCE 请求标头和 Last-Modified 响应标头来识别用户。
关注点
值得注意的是,MSIE 还会将收到的最后一个内容长度连同时间戳一起发送给 Web 服务器。
历史
- 2007 年 9 月 2 日:首次发布