当您已经有一个要用于 Limyee 电商平台的登录系统,并且您希望您的用户能够登录一次就登录到您的所有网站时,应使用Cookie身份验证,而不必单独重新验证每个网站。例如,您的主网站可能已经有一个登录系统,并且您希望用户在登录到您的主网站时自动登录到 Limyee 电商平台(反之亦然)。
启用 Cookie SSO 后,会发生以下情况:
Cookie SSO 通过 HTTP Cookie 将有关登录用户的安全消息从您的外部系统传递到 Limyee 电商平台。 此消息包含有关用户会话的加密信息,以及 Limyee 电商平台可用于验证消息的真实性和完整性的其他数据。
当用户登录到您的主单点登录系统时,它将创建一个 Cookie,其中包含有关用户会话的数据。 当用户访问您的 Limyee 电商平台时,Limyee 电商平台将读取此 Cookie 会话数据。 如果存在给定用户名的用户,则登录该用户。 如果该用户不存在,则使用指定的会话数据创建一个新用户,然后登录该用户。
要使 Cookie SSO 正常工作,Cookie 需要在外部身份验证系统和平台之间共享。 由于浏览器允许共享cookie的方式,这意味着您的平台和登录页面必须使用相同的域名。 例如,login.example.com 和 mobile.example.com 使用相同的域:example.com,因此可以与Cookie SSO一起使用。
在使用 Cookie SSO 之前,您需要确定 Cookie 加密模式,生成您配置的 Cookie 单点登录插件。
有两种模式可用于生成 Cookie
这两种模式都为 Cookie 提供机密性、身份验证和完整性保证。
选择身份验证方法后,需要生成要使用的加密密钥:
然后,应在 Cookie 单点登录插件中设置这些值。
单点登录 Cookie 中的会话数据由一组键值对组成,这些键值对描述了要登录的用户的会话。
|
名字 |
键 |
类型 |
描述 |
必填 |
|
用户名 |
用户名 |
字符串 |
要登录或创建的用户的用户名。此用户名必须是唯一的,并且不应是电子邮件地址或包含任何敏感信息(有关更多详细信息,请参阅"隐私"部分) |
是 |
|
电子邮件 |
电子邮件 |
字符串 |
用户的电子邮件地址。此电子邮件地址必须是唯一的。 |
是 |
|
有效期 |
过期日期 |
日期时间 |
身份验证 Cookie 失效的日期。(有关更多详细信息,请参阅下面的到期日期与到期) |
是 |
|
角色 |
角色 |
字符串 [] |
要将用户添加到的角色的逗号分隔列表。用户将始终被额外添加"Everyone"和"Registered Users"角色。 |
否 |
|
显示名称 |
通用名 |
字符串 |
可能显示的友好名称,而不是用户名。与用户名不同,"显示名称"可能是重复的。 |
否 |
会话数据需要序列化为多值 Cookie(根据 RFC 2109)以生成纯文本会话数据。
username=johnsmith&emailAddress=john.smith@example.com
对生成的每个 Cookie,会生成一个唯一的初始化向量 (IV)。如果是 AES-GCM,这应该是一个 96 位值。如果是 AES-HMAC,它必须与您使用的加密密钥长度相同。将其与您选择的加密算法一起使用,以加密生成密文的编码会话数据。
private byte[] EncryptSessionData(byte[] encryptionKey, byte[] iv, string serializedSessionData)
{
using (var aes = new AesCryptoServiceProvider())
{
aes.Mode = CipherMode.CBS;
using (var encryptor = aes.CreateEncryptor(encryptionKey, iv))
{
byte[] plainTextBytes = Encoding.UTF8.GetBytes(serializedSessionData);
using (var memoryStream = new MemoryStream())
{
using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
{
cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
cryptoStream.FlushFinalBlock();
}
return memoryStream.ToArray();
}
}
}
}
4. 生成身份验证数据
在 GCM 模式下使用 AES 时,这是由 AES GCM 算法生成的身份验证标记。在CBC模式下使用AES时,MAC是IV字节数组串联的SHA256加密哈希,后跟密码文本字节数组。Limyee 电商平台在解码 Cookie 以验证 Cookie 时使用此值。
5. 创建 Cookie
通过将 base64 单独加密每个值,然后将这些值串联起来,然后将这些值(由"$"分隔)将 IV、MAC 和 CipherText 编码为单个 Cookie
{Base64Encode(IV)}${Base64Encode(MAC)}${Base64Encode(CipherText)}
6. 将 Cookie 发送给客户端
Set-Cookie: AuthenticatedUser=6oX6iPtc7K0t6rxqj/smOQ==$caVgfxncWPSWynh/+ODlLlkBLGR7neFs5zJT3VMfxYk=$RosxFm0ZaVG3tMoV2zDfjEoxnjuOIyVc+ymrennvfJxUbJ7PwVwvMjOOV4JR96Y70HEZPSs+nboOOBEzVNWF/g==; HttpOnly
以下值是如何构建 Cookie 的示例。 这些值可用于测试您编写的任何 Cookie 生成代码。(任何二进制数据都显示为 Base64 编码字符串)
AES CBC + HMAC:
AES GCM:
C# 示例代码
AES HMAC
using System;
using System.Collections.Specialized;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public sealed class AesHmacCookieGenerator : IDisposable
{
private AesCryptoServiceProvider _aes;
private readonly byte[] _hmacKey;
public AesHmacCookieGenerator(byte[] encryptionKey, byte[] hmacKey)
{
_hmacKey = hmacKey;
_aes = new AesCryptoServiceProvider
{
Mode = CipherMode.CBC,
Key = encryptionKey
};
}
public string GenerateCookieContent(NameValueCollection sessionData, byte[] iv)
{
var encodedSessionData = EncodeSessionData(sessionData);
byte[] cipherText = EncryptSessionData(encodedSessionData, iv);
byte[] mac = ComputeHMAC(cipherText, iv);
return EncodeCookie(iv, mac, cipherText);
}
private static string EncodeSessionData(NameValueCollection sessionData)
{
var builder = new StringBuilder();
for (int i = 0; i < sessionData.Count; i++)
{
if (i > 0)
builder.Append('&');
builder.Append(sessionData.Keys[i]);
builder.Append('=');
builder.Append(sessionData[i]);
}
return builder.ToString();
}
private static string EncodeCookie(byte[] iv, byte[] mac, byte[] cipherText)
{
return Convert.ToBase64String(iv)
+ "$" + Convert.ToBase64String(mac)
+ "$" + Convert.ToBase64String(cipherText);
}
private byte[] EncryptSessionData(string encodedSessionData, byte[] iv)
{
byte[] plainTextBytes = Encoding.UTF8.GetBytes(encodedSessionData);
using (MemoryStream memoryStream = new MemoryStream())
{
using (ICryptoTransform encryptor = _aes.CreateEncryptor(_aes.Key, iv))
{
using (CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
{
cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
}
return memoryStream.ToArray();
}
}
}
private byte[] ComputeHMAC(byte[] ciphertext, byte[] iv)
{
byte[] messageToMac = new byte[iv.Length + ciphertext.Length];
Array.Copy(iv, 0, messageToMac, 0, iv.Length);
Array.Copy(ciphertext, 0, messageToMac, iv.Length, ciphertext.Length);
using (HMACSHA256 hmacsha256 = new HMACSHA256(_hmacKey))
{
return hmacsha256.ComputeHash(messageToMac);
}
}
public void Dispose()
{
if (_aes != null)
{
_aes.Dispose();
_aes = null;
}
}
}
AES GCM
using System;
using System.Collections.Specialized;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using Security.Cryptography;
public sealed class AesGcmCookieGenerator : IDisposable
{
private AuthenticatedAesCng _aes;
public AesGcmCookieGenerator(byte[] encryptionKey)
{
_aes = new AuthenticatedAesCng();
_aes.Key = encryptionKey;
}
public string GenerateCookieContent(NameValueCollection sessionData, byte[] iv)
{
var encodedSessionData = EncodeSessionData(sessionData);
byte[] cipherText;
byte[] mac;
EncryptAndGenerateMac(encodedSessionData, iv, out cipherText, out mac);
return EncodeCookie(iv, mac, cipherText);
}
private static string EncodeSessionData(NameValueCollection sessionData)
{
var builder = new StringBuilder();
for (int i = 0; i < sessionData.Count; i++)
{
if (i > 0)
builder.Append('&');
builder.Append(sessionData.Keys[i]);
builder.Append('=');
builder.Append((sessionData[i]));
}
return builder.ToString();
}
private static string EncodeCookie(byte[] iv, byte[] mac, byte[] cipherText)
{
return Convert.ToBase64String(iv)
+ "$" + Convert.ToBase64String(mac)
+ "$" + Convert.ToBase64String(cipherText);
}
private void EncryptAndGenerateMac(string encodedSessionData, byte[] iv, out byte[] cipherText, out byte[] mac)
{
byte[] plainTextBytes = Encoding.UTF8.GetBytes(encodedSessionData);
using (MemoryStream memoryStream = new MemoryStream())
{
using (IAuthenticatedCryptoTransform encryptor = _aes.CreateAuthenticatedEncryptor(_aes.Key, iv))
{
using (CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
{
cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
}
mac = encryptor.GetTag();
cipherText = memoryStream.ToArray();
}
}
}
public void Dispose()
{
if (_aes != null)
{
_aes.Dispose();
_aes = null;
}
}
}
如果 Limyee 电商平台无法处理 SSO cookie,它将记录错误并删除该 Cookie,以防止进一步的验证失败。若要查看这些错误,请转到管理>监测>异常。将异常类型筛选为"验证错误(Validation Error)"。
如果您无法访问控制面板(例如,由于 Cookie SSO 配置问题而无法再登录),则可以使用紧急访问页面返回您的平台。您必须在服务器上访问此页面。此页面可以在 ~/controlpanel/localaccess.aspx 找到,它将允许您在启用 Cookie SSO 插件之前使用凭据登录。
您不应使用电子邮件地址作为用户名。用户名用作平台中用户的可见唯一标识符,因此,如果使用电子邮件地址代替用户名将导致用户的电子邮件地址公开。如果您的现有系统使用电子邮件地址作为用户名,那么您应该考虑使用其他内容作为用户名,例如整数,UUID标识符,或者让用户在首次登录平台时创建其用户名。
为确保 Cookie 的安全性,您必须始终设置"到期日期"的会话数据值。如果您希望您的身份验证 Cookie 在用户关闭其浏览器后持续使用,那么您还可以将"吊销 cookie" 属性设置为与到期日期相同的值。不应设置 "吊销 cookie"属性来代替"到期日期"会话数据值。"吊销 Cookie" 属性仅指示浏览器在达到过期时间时删除 Cookie,如果由于任何原因浏览器忽略了过期时间,或者攻击者设法窃取了 Cookie,那么他们可以在过期时间之后继续使用 Cookie。到期日期会话数据值与此相反, 如果收到的 Cookie 已超过其到期日期,则该 cookie 被视为无效。
在 GCM 模式下使用 AES 时,您必须非常仔细地注意如何生成 IV - 特别是确保使用的每个 IV 都是唯一的。根据生日悖论,随机生成IV并不能提供足够强的唯一保证。有关该问题的更多详细信息以及生成IV的建议方法,请参阅 NIST Special Publication 800-38D 的第8,8.2节和附录A。
创建 Cookie 时,您应考虑在 Cookie 上设置以下属性,以最大限度地提高 Cookie 的安全性