当您已经有一个要用于 Limyee 电商平台的登录系统,并且您希望您的用户能够登录一次就登录到您的所有网站时,应使用Cookie身份验证,而不必单独重新验证每个网站。例如,您的主网站可能已经有一个登录系统,并且您希望用户在登录到您的主网站时自动登录到 Limyee 电商平台(反之亦然)。
启用 Cookie SSO 后,会发生以下情况:
- 平台的登录和注册 URL 被重定向到指向外部身份验证系统
- 当外部登录系统登录用户时,将设置一个 cookie,允许Limyee 电商平台登录此用户,如果不存在帐户时将创建其新帐户。
Cookie SSO 的工作原理
Cookie SSO 通过 HTTP Cookie 将有关登录用户的安全消息从您的外部系统传递到 Limyee 电商平台。 此消息包含有关用户会话的加密信息,以及 Limyee 电商平台可用于验证消息的真实性和完整性的其他数据。
当用户登录到您的主单点登录系统时,它将创建一个 Cookie,其中包含有关用户会话的数据。 当用户访问您的 Limyee 电商平台时,Limyee 电商平台将读取此 Cookie 会话数据。 如果存在给定用户名的用户,则登录该用户。 如果该用户不存在,则使用指定的会话数据创建一个新用户,然后登录该用户。
Cookie SSO 的限制
要使 Cookie SSO 正常工作,Cookie 需要在外部身份验证系统和平台之间共享。 由于浏览器允许共享cookie的方式,这意味着您的平台和登录页面必须使用相同的域名。 例如,login.example.com 和 mobile.example.com 使用相同的域:example.com,因此可以与Cookie SSO一起使用。
初始设置
在使用 Cookie SSO 之前,您需要确定 Cookie 加密模式,生成您配置的 Cookie 单点登录插件。
有两种模式可用于生成 Cookie
- AES-HMAC - 此模式在 CBC 模式下使用 AES 加密会话数据,然后计算 IV 的 HMAC-SHA256 哈希和加密的会话数据以验证消息的完整性。
- AES-GCM - 此模式在 GCM 模式下使用经过身份验证的 AES,该模式既加密会话数据,又在单个操作中生成身份验证标记。
这两种模式都为 Cookie 提供机密性、身份验证和完整性保证。
选择身份验证方法后,需要生成要使用的加密密钥:
- AES-HMAC - 密钥必须是 128、192 或 256 位,HMAC 密钥必须是 256 位。
- AES-GCM - 必须使用 256 位加密密钥。不需要 HMAC 密钥。
然后,应在 Cookie 单点登录插件中设置这些值。
生成 SSO 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:
- Encryption Key (base64): FFhrYY4xw9Y/xRKE7eS4jV/2YaPbpt7ryvjJ1E8SwV0=
- HMAC Key (base64): NNeWjU+i4/V9lkVhIRoWY3CfxBy7nmU3okSD/9fBqnScP8DbdY7elgow0xi3LDyQWMd795gnL+2v+ZHpYUJlMg==
- Plaintext (string): username=example&emailAddress=example@example.org
- Initialisation Vector (base64): 6oX6iPtc7K0t6rxqj/smOQ==
- CipherText: RosxFm0ZaVG3tMoV2zDfjEoxnjuOIyVc+ymrennvfJxUbJ7PwVwvMjOOV4JR96Y70HEZPSs+nboOOBEzVNWF/g==
- HMAC (Base64): caVgfxncWPSWynh/+ODlLlkBLGR7neFs5zJT3VMfxYk=
- Cookie Content (string): 6oX6iPtc7K0t6rxqj/smOQ==$caVgfxncWPSWynh/+ODlLlkBLGR7neFs5zJT3VMfxYk=$RosxFm0ZaVG3tMoV2zDfjEoxnjuOIyVc+ymrennvfJxUbJ7PwVwvMjOOV4JR96Y70HEZPSs+nboOOBEzVNWF/g==
AES GCM:
- Encryption Key (base64): FFhrYY4xw9Y/xRKE7eS4jV/2YaPbpt7ryvjJ1E8SwV0=
- HMAC Key: (Not Used)
- Plaintext (string): username=example&emailAddress=example@example.org
- Initialisation Vector (base64): yEKcjquPkAF+7GeQ
- CipherText (base64): +BW+eTnnzezORFMZAwPVdmzDlWl1A8i1Ak+tfv3iMM+NCyPTZViowjF17DaBdcCdVQ==
- MAC (base64): aahmltkpzeIQRytPxDO7ZA==
- Cookie Content (string): EKcjquPkAF+7GeQ$aahmltkpzeIQRytPxDO7ZA==$+BW+eTnnzezORFMZAwPVdmzDlWl1A8i1Ak+tfv3iMM+NCyPTZViowjF17DaBdcCdVQ==
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 插件之前使用凭据登录。
使用 Cookie SSO 的安全和隐私注意事项
使用电子邮件地址作为用户名
您不应使用电子邮件地址作为用户名。用户名用作平台中用户的可见唯一标识符,因此,如果使用电子邮件地址代替用户名将导致用户的电子邮件地址公开。如果您的现有系统使用电子邮件地址作为用户名,那么您应该考虑使用其他内容作为用户名,例如整数,UUID标识符,或者让用户在首次登录平台时创建其用户名。
到期日期与吊销
为确保 Cookie 的安全性,您必须始终设置"到期日期"的会话数据值。如果您希望您的身份验证 Cookie 在用户关闭其浏览器后持续使用,那么您还可以将"吊销 cookie" 属性设置为与到期日期相同的值。不应设置 "吊销 cookie"属性来代替"到期日期"会话数据值。"吊销 Cookie" 属性仅指示浏览器在达到过期时间时删除 Cookie,如果由于任何原因浏览器忽略了过期时间,或者攻击者设法窃取了 Cookie,那么他们可以在过期时间之后继续使用 Cookie。到期日期会话数据值与此相反, 如果收到的 Cookie 已超过其到期日期,则该 cookie 被视为无效。
在 GCM 模式下生成初始化向量
在 GCM 模式下使用 AES 时,您必须非常仔细地注意如何生成 IV - 特别是确保使用的每个 IV 都是唯一的。根据生日悖论,随机生成IV并不能提供足够强的唯一保证。有关该问题的更多详细信息以及生成IV的建议方法,请参阅 NIST Special Publication 800-38D 的第8,8.2节和附录A。
Cookie 属性
创建 Cookie 时,您应考虑在 Cookie 上设置以下属性,以最大限度地提高 Cookie 的安全性
- HttpOnly - 您应将 Cookie 标记为 HttpOnly,以提高身份验证 Cookie 的安全性。此属性指示浏览器阻止从 JavaScript 读取 Cookie。有关更多详细信息,请参阅:https://www.owasp.org/index.php/HttpOnly。
- Secure - 如果您的登录页面和平台只能通过 SSL 访问,或者您只想通过 SSL 发送 Cookie,请将 Cookie 标记为安全,以便永远不会通过非 SSL 连接发送。有关更多详细信息,请参阅:https://www.owasp.org/index.php/SecureFlag。
- Domain - 当单点登录页面和 Limyee 电商平台位于不同的域上时,必须设置 cookie 域以确保 Cookie 可以发送到 Limyee 电商平台。此域应设置为两个域共享的最接近的公共域名。(例如,如果您的单点登录页面位于 example.com,而平台位于 my.example.com,则 Cookie 域应设置为"example.com")。在多个子域之间共享 Cookie 时,请确保不要在域名前面放置点(例如,".example.com")。在域前面有一个点将导致 Cookie 仅在该特定域上可见,而不是其子域。
- Expires - 指定浏览器应将 Cookie 视为无效并将其删除的时间。如果未设置过期,则浏览器将保留 Cookie,直到浏览器会话关闭。
- SameSite - 这是一个实验属性,目前在Chrome中实现,仅用于防止在跨域请求中发送 cookie,这可以防止某些形式的CSRF攻击 。有关更多详细信息,请参阅:https://tools.ietf.org/html/draft-west-first-party-cookies-07。