ASP.NET 没有提供一个类似的方法来激活对查询字符串的保护。很多时候,用户是否可以看到或者修改它并不重要,但有时,查询字符串包含了应当对用户隐藏的数据。
有必要设计一个增强安全的简单方式,在将数据放到查询字符串之前使其不规则化。
1. 包装查询字符串
创建一个 EncryptedQueryString 类,用于接收一个基于字符串的信息集合,并允许在另一个页面获取它。在后台,EncryptedQueryString 类在数据放入查询字符串之前进行加密,并最终对之解密。
public class EncryptedQueryString : System.Collections.Specialized.StringDictionary
{
public EncryptedQueryString() { }
public EncryptedQueryString(string encryptedData) { }
public override string ToString() { }
}
注意,它从 StringDictionary 类继承。StringDictionary 代表一个通过字符串索引的字符串集合,因此,可以像使用普通的字符串集合那样使用 EncryptedQueryString 类,而不用编写任何额外代码。例如下面:
encryptedQueryString["value1"] = "Sample Value";
那么,如何将这些信息放到查询字符串中呢?这里的设想是覆盖它的 ToString() 方法,检查所有的集合数据并将其合并为一个单独的加密过的字符串。查询字符串使用 = 将值和名称隔开,使用 & 将每一个名/值对隔开。你需要保证集合中的名称和值不包含这些特殊字符。为了解决这个问题,使用 HttpServerUtility.UrlEncode() 方法对字符串进行转义。下面是 ToString() 方法的一部分:
public override string ToString()
{
StringBuilder content = new StringBuilder();
// Go through the contents and build a typical query string
foreach (string key in base.Keys)
{
content.Append(HttpUtility.UrlEncode(key)).Append("=");
content.Append(HttpUtility.UrlEncode(base[key])).Append("&");
}
content.Remove(content.Length - 1, 1);
......
}
下一步是使用 ProtectedData 类来加密数据。这个类使用 DPAPI 来加密信息,使用它的 Protect() 返回一个字节数组,因此你需要额外的步骤把字节数组转化为一个字符串形式,即符合查询字符串的形式。
Convert.ToBase64String() 方法看上去似乎很合理,它会创建使用 Base64 编码的字符串。但是,Base64 字符串会包含查询字符串中不允许的符号(=)。即使你对其再进行 URL 编码,这也使得解密阶段更加复杂。而且 ToBase64String() 也可能会引入一系列的字符,它们看上去特别象经过 URL 编码的字符序列。当你解码字符串时,这些字符序列可能会被错误的替换掉。
一个更为简单的方法就是使用不同的编码方式。本例使用了十六进制编码,将每一个字符替换为一个字母数字编码。下面是这个辅助类的简单实现:
/// <summary>
/// 十六进制转换辅助类
/// </summary>
public static class HexEncoding
{
/// <summary>
/// 将每一个字符都转换为十六进制表现
/// </summary>
/// <param name="data">字节数组</param>
/// <returns>十六进制表现的字符串</returns>
public static string GetString(byte[] data)
{
StringBuilder results = new StringBuilder();
foreach (byte b in data)
{
results.Append(b.ToString("X2"));
}
return results.ToString();
}
/// <summary>
/// 将十六进制表现的字符串转换为 byte[]
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public static byte[] GetBytes(string data)
{
// GetString encodes the hex numbers with two digits
byte[] results = new byte[data.Length / 2];
for (int i = 0; i < data.Length; i += 2)
{
results[i / 2] = Convert.ToByte(data.Substring(i, 2), 16);
}
return results;
}
}
继续完成 EncryptedQueryString.ToString() 方法:
// Now encrypt the contents using DPAPI
byte[] encryptData = ProtectedData.Protect(Encoding.UTF8.GetBytes(content.ToString()),
null, DataProtectionScope.LocalMachine);
return HexEncoding.GetString(encryptData);
收到查询字符串的目标页面需要一个方法来反序列化并解密这个字符串。创建一个新的 EncryptedQueryString 对象,并提供已加密的数据:
public EncryptedQueryString(string encryptedData)
{
// Decrypt data passed in using DPAPI
byte[] rawData = HexEncoding.GetBytes(encryptedData);
byte[] clearRawData = ProtectedData.Unprotect(rawData,
null, DataProtectionScope.LocalMachine);
string stringData = Encoding.UTF8.GetString(clearRawData);
// Split the data and add the contents
int index;
string[] splittedData = stringData.Split('&');
foreach (string singleData in splittedData)
{
index = singleData.IndexOf('=');
base.Add(
HttpUtility.UrlDecode(singleData.Substring(0, index)),
HttpUtility.UrlDecode(singleData.Substring(index + 1))
);
}
}
这个构造函数首先将闯入的十六进制信息解码,然后使用 DPAPI 解密,再用 Encoding.UTF8.GetString() 还原回原来的查询字符串,最后将字符串分割回原来的部分,并将键值对添加到基本的 StringDictionary 中。
下面是 EncryptedQueryString 类的完整代码,并附上中文注释:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Security.Cryptography;
namespace EncryptionUtility
{
/// <summary>
/// 加密查询字符串
/// </summary>
public class EncryptedQueryString : System.Collections.Specialized.StringDictionary
{
public EncryptedQueryString() { }
/// <summary>
/// 在这里解密一个已加密的字符串
/// </summary>
/// <param name="encryptedData"></param>
public EncryptedQueryString(string encryptedData)
{
// Decrypt data passed in using DPAPI
byte[] rawData = HexEncoding.GetBytes(encryptedData);
byte[] clearRawData = ProtectedData.Unprotect(rawData,
null, DataProtectionScope.LocalMachine);
string stringData = Encoding.UTF8.GetString(clearRawData);
// Split the data and add the contents
int index;
string[] splittedData = stringData.Split('&');
foreach (string singleData in splittedData)
{
index = singleData.IndexOf('=');
base.Add(
HttpUtility.UrlDecode(singleData.Substring(0, index)),
HttpUtility.UrlDecode(singleData.Substring(index + 1))
);
}
}
/// <summary>
/// 加密查询字符串
/// </summary>
/// <returns>as Hex-encoded string</returns>
public override string ToString()
{
StringBuilder contents = new StringBuilder();
// Go through the contents and build a typical query string
foreach (string key in base.Keys)
{
contents.Append(HttpUtility.UrlEncode(key)).Append("=");
contents.Append(HttpUtility.UrlEncode(base[key])).Append("&");
}
contents.Remove(contents.Length - 1, 1);
// Now encrypt the contents using DPAPI
byte[] encryptData = ProtectedData.Protect(Encoding.UTF8.GetBytes(contents.ToString()),
null, DataProtectionScope.LocalMachine);
return HexEncoding.GetString(encryptData);
}
}
}
2. 创建一个测试页面
我们创建 2 个页面来测试 EncryptedQueryString 类。第一个页面包含一个用户输入信息的文本框:
<form id="form1" runat="server">
<div>
Enter some data here:
<asp:TextBox runat="server" ID="txtData" />
<br />
<br />
<asp:Button runat="server" Text="Send Info" ID="btnSend" OnClick="btnSend_Click" />
</div>
</form>
protected void btnSend_Click(object sender, EventArgs e)
{
EncryptedQueryString queryString = new EncryptedQueryString();
queryString.Add("MyData", txtData.Text);
queryString.Add("MyTime", DateTime.Now.ToLongTimeString());
queryString.Add("MyDate", DateTime.Now.ToLongDateString());
Response.Redirect("Reciever.aspx?data=" + queryString.ToString());
}
第二个页面就作一下取值的显示:
protected void Page_Load(object sender, EventArgs e)
{
// Deserialize the encrypted query string
EncryptedQueryString queryString = new EncryptedQueryString(Request.QueryString["data"]);
// Write information to the screen
StringBuilder info = new StringBuilder();
foreach (string key in queryString.Keys)
{
info.AppendFormat("{0} = {1}<br>", key, queryString[key]);
}
lblQueryString.Text = info.ToString();
}
页面测试的效果如下:
加密流程解释:
- 创建 EncryptedQueryString 实例,Add() 添加字符串索引信息的键值对,调用 ToString() 加密
- 对信息进行 HttpUtility.UrlEncode() 编码,过滤 URL 中非法字符,将键值对连接成字符串
- 调用 Encoding.UTF8.GetBytes() 获得该字符串的 byte[],进行 DPAPI 保护
- 调用辅助类 HexEncoding.GetString() 将保护后的 byte[] 转换为 16 进制形式的字符串
- 将 16 进制字符串作为查询字符串,跳转到接收页面
解密流程解释:
- 获取 16 进制字符串,用来构造 EncryptedQueryString 实例,准备解密查询字符串
- 将 16 进制字符串形式转换为 byte[],取出 DPAPI 保护
- 调用 Encoding.UTF8.GetString() 将 byte[] 还原回查询字符串
- 将查询字符串分解为名值对,添加到 EncryptedQueryString 集合中
- 接收页面作出显示