需求與安全基線
- 用戶勾選「記住密碼」→ 下次自動填充用戶名、密文密碼。
- 絕不存儲明文密碼:Cookie 僅存 MD5/SHA256 雜值,驗證時比對雜值。
- 防範 XSS/CSRF:Cookie 設置
HttpOnly=true、Secure=true(HTTPS)、SameSite=Lax。
- 定期輪換:建議 30 天過期,敏感系統縮短至 7-14 天。
核心實現類
using System;
using System.Web;
using System.Web.Security;
public class RememberMeHelper
{
private const string UserNameCookieName = "RememberMe_User";
private const string PasswordHashCookieName = "RememberMe_PwdHash";
private const int ExpireDays = 30;
private const string PlaceholderPassword = "◆◆◆◆◆◆◆◆◆◆"; // 10 字符佔位符
private readonly HttpContext _ctx;
public RememberMeHelper(HttpContext ctx) { _ctx = ctx; }
///
/// 登錄成功後調用:根據勾選狀態寫入/清除 Cookie
///
public void ProcessLogin(string userName, string password, bool rememberMe)
{
if (rememberMe)
{
SaveCookie(UserNameCookieName, userName);
// 存儲密碼雜值(MD5 或更強 SHA256)
string pwdHash = FormsAuthentication.HashPasswordForStoringInConfigFile(password, "SHA256");
SaveCookie(PasswordHashCookieName, pwdHash);
}
else
{
ClearCookie(UserNameCookieName);
ClearCookie(PasswordHashCookieName);
}
}
///
/// 頁面加載時調用:自動填充用戶名、密碼框佔位符
///
public void AutoFill(ref string userNameBox, ref string passwordBox, ref bool rememberChecked)
{
var userCookie = _ctx.Request.Cookies[UserNameCookieName];
var pwdCookie = _ctx.Request.Cookies[PasswordHashCookieName];
if (userCookie != null)
{
rememberChecked = true;
userNameBox = userCookie.Value;
if (pwdCookie != null)
{
// 密碼框設置佔位符,前端顯示圓點,實際值不回填明文
passwordBox = PlaceholderPassword;
}
}
}
///
/// 登錄驗證時調用:返回用於比對的密碼雜值
///
public string GetPasswordHashForValidation(string inputPassword, bool rememberChecked)
{
if (rememberChecked)
{
var pwdCookie = _ctx.Request.Cookies[PasswordHashCookieName];
if (pwdCookie != null) return pwdCookie.Value; // 直接用 Cookie 雜值比對
}
// 未勾選或無 Cookie:現場計算輸入密碼雜值
return FormsAuthentication.HashPasswordForStoringInConfigFile(inputPassword, "SHA256");
}
private void SaveCookie(string name, string value)
{
var cookie = new HttpCookie(name, value)
{
Expires = DateTime.Now.AddDays(ExpireDays),
HttpOnly = true,
Secure = _ctx.Request.IsSecureConnection, // 僅 HTTPS 傳輸
SameSite = SameSiteMode.Lax
};
_ctx.Response.Cookies.Set(cookie);
}
private void ClearCookie(string name)
{
var cookie = new HttpCookie(name)
{
Expires = DateTime.Now.AddDays(-1),
HttpOnly = true,
Secure = _ctx.Request.IsSecureConnection,
SameSite = SameSiteMode.Lax
};
_ctx.Response.Cookies.Set(cookie);
}
}
前端頁面關鍵點
<asp:TextBox ID="txtUserName" runat="server" />
<asp:TextBox ID="txtPassword" runat="server" TextMode="Password" autocomplete="off" />
<asp:CheckBox ID="chkRemember" runat="server" Text="記住密碼" />
<asp:Button ID="btnLogin" runat="server" Text="登錄" OnClick="btnLogin_Click" />
後端調用流程
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
string user = "", pwd = "";
bool remember = false;
new RememberMeHelper(Context).AutoFill(ref user, ref pwd, ref remember);
txtUserName.Text = user;
txtPassword.Text = pwd; // 佔位符
chkRemember.Checked = remember;
}
}
protected void btnLogin_Click(object sender, EventArgs e)
{
string inputUser = txtUserName.Text.Trim();
string inputPwd = txtPassword.Text;
bool remember = chkRemember.Checked;
// 關鍵:若密碼框是佔位符,說明用戶未修改密碼,直接用 Cookie 雜值驗證
bool useCookieHash = (inputPwd == RememberMeHelper.PlaceholderPassword);
var helper = new RememberMeHelper(Context);
string hashToValidate = useCookieHash
? helper.GetPasswordHashForValidation(null, true) // 內部會讀 Cookie
: FormsAuthentication.HashPasswordForStoringInConfigFile(inputPwd, "SHA256");
// 數據庫驗證:SELECT * FROM Users WHERE UserName=@u AND PasswordHash=@h
bool valid = ValidateUser(inputUser, hashToValidate);
if (valid)
{
helper.ProcessLogin(inputUser, inputPwd, remember);
Response.Redirect("Default.aspx");
}
else
{
lblMsg.Text = "用戶名或密碼錯誤";
// 登錄失敗時清除佔位符,避免下次誤判
txtPassword.Text = "";
}
}
安全增強清單
| 措施 | 實現 |
| 升級雜值算法 | 替換 MD5 → SHA256 / PBKDF2 / Argon2(需引入庫) |
| 綁定設備指紋 | Cookie 值 = Hash(UserId + PasswordHash + UserAgent + IP段) |
| 異常登錄告警 | 新設備/地點登錄發送郵件/短信確認 |
| Token 化改造 | 引入 Refresh Token + Access Token,替換 Cookie 方案 |