ASP.NET C#中如何创建安全的唯一用户会话ID
会话安全改造:用不可猜测的会话ID替代账号ID
当前问题
现有会话逻辑直接将account-id存入Cookie,账号ID易被猜测,存在身份伪造风险。需要替换为不可猜测的唯一值(如GUID),同时实现服务器端的会话关联校验。
当前代码实现
创建会话代码
public static void NewSession(Account account) { var redirectUrl = "Login.aspx"; if (account == null) { var sessionCookie = HttpContext.Current.Request.Cookies["test-app-session"]; if (sessionCookie != null) ExpireCookie(sessionCookie); } else { var sessionCookie = new HttpCookie("test-app-session"); sessionCookie.Values["account-id"] = account.Id.ToString(); sessionCookie.Expires = DateTime.Now.AddHours(12); HttpContext.Current.Response.Cookies.Add(sessionCookie); var redirectCookie = HttpContext.Current.Request.Cookies["test-app-redirect"]; if (redirectCookie != null) { redirectUrl = redirectCookie.Values["url"]; ExpireCookie(redirectCookie); } if (string.IsNullOrWhiteSpace(redirectUrl)) redirectUrl = "Default.aspx"; } HttpContext.Current.Response.Redirect(redirectUrl); }
验证会话代码
public static Account FromSession() { var sessionCookie = HttpContext.Current.Request.Cookies["test-app-session"]; if (sessionCookie != null && long.TryParse(sessionCookie.Values["account-id"], out long accountId)) { using (var db = Database.Connect()) { using (var cmd = db.Command("SELECT * FROM Account WHERE id=@id").Parameter("@id", accountId, DbType.Int64)) using (var reader = cmd.ExecuteReader()) if (reader.Read()) return new Account(reader); } } if (!Path.GetFileName(HttpContext.Current.Request.Path).Equals("Login.aspx", StringComparison.OrdinalIgnoreCase)) { var redirectCookie = new HttpCookie("test-app-redirect"); redirectCookie.Values["url"] = HttpContext.Current.Request.Url.ToString(); redirectCookie.Expires = DateTime.Now.AddHours(1); HttpContext.Current.Response.Cookies.Add(redirectCookie); HttpContext.Current.Response.Redirect("Login.aspx"); } return null; }
改造方案
核心思路是在服务器端维护会话与账号的映射关系:生成不可猜测的会话ID存入Cookie,同时在数据库中保存会话ID对应的账号信息、过期时间等校验数据。
1. 新增会话存储表
在数据库中创建Session表,用于存储会话关联信息:
CREATE TABLE Session ( SessionId VARCHAR(36) PRIMARY KEY, -- GUID字符串 AccountId BIGINT NOT NULL, ExpiresAt DATETIME NOT NULL, CreatedAt DATETIME DEFAULT GETDATE(), -- 可选:绑定客户端IP增强安全 ClientIp VARCHAR(45), FOREIGN KEY (AccountId) REFERENCES Account(id) )
2. 修改会话创建方法NewSession
生成GUID作为会话ID,存入Cookie,同时将会话信息写入数据库:
public static void NewSession(Account account) { var redirectUrl = "Login.aspx"; if (account == null) { var sessionCookie = HttpContext.Current.Request.Cookies["test-app-session"]; if (sessionCookie != null) { // 删除服务器端会话记录 using (var db = Database.Connect()) { var sessionId = sessionCookie.Values["session-id"]; if (!string.IsNullOrWhiteSpace(sessionId)) { db.Command("DELETE FROM Session WHERE SessionId=@sessionId") .Parameter("@sessionId", sessionId) .ExecuteNonQuery(); } ExpireCookie(sessionCookie); } } } else { var sessionId = Guid.NewGuid().ToString(); var expiresAt = DateTime.Now.AddHours(12); // 写入数据库会话记录 using (var db = Database.Connect()) { var clientIp = HttpContext.Current.Request.UserHostAddress; db.Command("INSERT INTO Session (SessionId, AccountId, ExpiresAt, ClientIp) VALUES (@sessionId, @accountId, @expiresAt, @clientIp)") .Parameter("@sessionId", sessionId) .Parameter("@accountId", account.Id) .Parameter("@expiresAt", expiresAt) .Parameter("@clientIp", clientIp) .ExecuteNonQuery(); } // 创建会话Cookie var sessionCookie = new HttpCookie("test-app-session"); sessionCookie.Values["session-id"] = sessionId; sessionCookie.Expires = expiresAt; // 安全优化:开启HttpOnly防止XSS窃取Cookie,开启Secure仅在HTTPS下传输 sessionCookie.HttpOnly = true; sessionCookie.Secure = HttpContext.Current.Request.IsSecureConnection; HttpContext.Current.Response.Cookies.Add(sessionCookie); var redirectCookie = HttpContext.Current.Request.Cookies["test-app-redirect"]; if (redirectCookie != null) { redirectUrl = redirectCookie.Values["url"]; ExpireCookie(redirectCookie); } if (string.IsNullOrWhiteSpace(redirectUrl)) redirectUrl = "Default.aspx"; } HttpContext.Current.Response.Redirect(redirectUrl); }
3. 修改会话验证方法FromSession
从Cookie获取会话ID,查询数据库获取关联账号,同时校验会话是否过期:
public static Account FromSession() { var sessionCookie = HttpContext.Current.Request.Cookies["test-app-session"]; if (sessionCookie != null) { var sessionId = sessionCookie.Values["session-id"]; if (!string.IsNullOrWhiteSpace(sessionId)) { using (var db = Database.Connect()) { // 查询会话并校验过期时间,可选校验IP var clientIp = HttpContext.Current.Request.UserHostAddress; using (var cmd = db.Command(@" SELECT a.* FROM Account a JOIN Session s ON a.id = s.AccountId WHERE s.SessionId=@sessionId AND s.ExpiresAt > GETDATE() AND s.ClientIp = @clientIp") .Parameter("@sessionId", sessionId) .Parameter("@clientIp", clientIp)) using (var reader = cmd.ExecuteReader()) { if (reader.Read()) { // 可选:延长会话过期时间 db.Command("UPDATE Session SET ExpiresAt=@newExpires WHERE SessionId=@sessionId") .Parameter("@newExpires", DateTime.Now.AddHours(12)) .Parameter("@sessionId", sessionId) .ExecuteNonQuery(); return new Account(reader); } else { // 会话过期或无效,删除记录并清除Cookie db.Command("DELETE FROM Session WHERE SessionId=@sessionId") .Parameter("@sessionId", sessionId) .ExecuteNonQuery(); ExpireCookie(sessionCookie); } } } } } if (!Path.GetFileName(HttpContext.Current.Request.Path).Equals("Login.aspx", StringComparison.OrdinalIgnoreCase)) { var redirectCookie = new HttpCookie("test-app-redirect"); redirectCookie.Values["url"] = HttpContext.Current.Request.Url.ToString(); redirectCookie.Expires = DateTime.Now.AddHours(1); HttpContext.Current.Response.Cookies.Add(redirectCookie); HttpContext.Current.Response.Redirect("Login.aspx"); } return null; }
4. 额外安全建议
- 定期清理数据库中过期的会话记录(可通过定时任务或SQL触发器实现)
- 开启Cookie的
SameSite属性(如SameSite=Strict或Lax)防止CSRF攻击 - 避免在Cookie中存储任何敏感信息,所有校验逻辑都在服务器端完成
内容的提问来源于stack exchange,提问作者Jack Gates
相关产品推荐
相关产品推荐

