The Copenhagen Book

会话管理

概述

在用户访问你的网站期间,他们会向服务器发出多次请求。如果需要在这些请求之间保持状态(如用户偏好),HTTP 本身不提供机制,因为它是无状态协议。

会话是一种在服务器上保持状态的方法,特别适用于管理身份验证状态,如客户端的身份。我们可以为每个会话分配一个唯一的 ID,并将其存储在服务器上,用作令牌。然后,客户端可以通过发送会话 ID 来关联请求与会话。要实现身份验证,我们可以将用户数据与会话一起存储。

会话 ID 必须足够长且随机,否则可能被他人通过猜测来冒充。请参阅服务器端令牌指南以生成安全的会话 ID。会话 ID 可以在存储前进行哈希处理,以提供额外的安全性。

根据应用程序的不同,您可能只需管理已验证用户的会话,也可能需要同时管理已验证和未验证用户的会话。您甚至可以管理两种不同类型的会话——一种用于身份验证,另一种用于与身份验证无关的状态。

会话生命周期

如果您只管理已验证用户的会话,则每当用户登录时都会创建一个新会话。如果计划同时管理未验证用户的会话,当传入请求不包含有效会话时,应自动创建会话。确保您的应用程序不易受到会话固定攻击的影响。

对于安全性要求高的应用程序,确保会话自动过期非常重要。这可以最大限度地减少攻击者劫持会话的时间。过期时间应与用户预期的单次使用时间相匹配。

然而,对于安全性要求较低的网站,如社交媒体应用,如果用户每天都需要登录会很烦人。一个好的做法是设置合理的过期时间,比如 30 天,并在会话被使用时延长过期时间。例如,会话默认在 30 天后过期,但如果在过期前 15 天内使用,会话的过期时间会被延长 30 天。这有效地使不活跃用户的会话失效,同时保持活跃用户的登录状态。

您也可以结合两种方法。例如,可以将过期时间设置为 1 小时,并每 30 分钟延长一次,但设置一个绝对过期时间为 12 小时,以确保会话不会持续超过这个时间。

const sessionExpiresIn = 30 * 24 * time.Hour

func validateSession(sessionId string) (*Session, error) {
	session, ok := getSessionFromStorage(sessionId)
	if !ok {
		return nil, errors.New("invalid session id")
	}
	if time.Now().After(session.ExpiresAt) {
		return nil, errors.New("expired session")
	}
	if time.Now().After(session.expiresAt.Sub(sessionExpiresIn / 2)) {
		session.ExpiresAt = time.Now().Add(sessionExpiresIn)
		updateSessionExpiration(session.Id, session.ExpiresAt)
	}
	return session, nil
}

Sudo 模式

一种替代短期会话的方法是实现长期会话结合 sudo 模式。Sudo 模式允许已验证用户通过重新验证其凭据(密码、WebAuthn 凭据、TOTP 等)在有限时间内访问安全关键组件。实现这种模式的简单方法是跟踪用户在每个会话中最后使用凭据的时间。这种方法提供了短期会话的安全优势,而不会频繁打扰用户。这也有助于防止会话劫持

会话劫持

会话劫持是指窃取会话。常见攻击包括 XSS、中间人攻击(MITM)和会话嗅探。MITM 攻击尤其难以防范,因为最终取决于用户保护其设备和网络。然而,仍有一些方法可以保护用户。

首先,考虑跟踪与会话相关的用户代理(设备)和 IP 地址,以检测可疑请求。IP 地址可能对移动用户是动态的,因此您可能需要跟踪一般区域(国家)而不是具体地址。基于这些信息限制连接到用户的会话数量也是一种好的防护措施。

由于 IP 地址和请求头很容易被伪造,因此建议在任何安全关键应用程序中实施sudo 模式

会话失效

会话可以通过从服务器和客户端存储中删除来失效。

当用户注销时,使当前会话失效;对于安全关键应用程序,使该用户的所有会话失效。

当用户获得新权限(电子邮件验证、新角色等)或更改密码时,也应使其所有会话失效。

客户端存储

客户端应将会话 ID 存储在用户设备中,以用于后续请求。浏览器主要提供两种存储数据的方法——cookies 和 Web Storage API。对于网站,应该优先使用 cookies,因为它们会被浏览器自动包含在请求中。

Cookies

会话 cookies 应具备以下属性:

  • HttpOnly: 仅服务器端可访问 cookies
  • SameSite=Lax: 对于关键网站使用 Strict
  • Secure: 仅可通过 HTTPS 发送 cookies
  • Max-AgeExpires: 必须定义以保持 cookies
  • Path=/: cookies 可从所有路由访问

在使用 cookies 时必须实施CSRF 保护,仅使用 SameSite 标志是不够的。使用 cookies 并不能自动保护用户免受跨站脚本攻击(XSS)。虽然会话 ID 不能被直接读取,但经过身份验证的请求仍然可以被发出,因为浏览器会自动在请求中包含 cookies。

cookies 的最大过期时间为 400 天。如果计划让会话长期有效,请持续设置 cookies。

SameSite 属性应优先使用 Lax 而不是 Strict,因为使用 Strict 会导致用户通过外部链接访问应用时浏览器不发送会话 cookie。

Web Storage API

另一种选择是将会话 ID 存储在 localStoragesessionStorage 中。如果网站存在 XSS 漏洞,这将允许攻击者直接读取和窃取用户的会话 ID。由于只需读取整个本地存储即可窃取令牌,因此它特别容易受到供应链攻击的影响,而无需使用任何特定于应用程序的漏洞。

会话令牌可以通过 Authorization 头与请求一起发送。不要将它们作为查询参数或表单数据发送,也不应接受以这种方式发送的令牌。

会话固定攻击

维护已验证和未验证用户会话的应用程序,并在用户登录时重用当前会话,容易受到会话固定攻击。

假设应用程序允许在 URL 中作为查询参数发送会话 ID。如果攻击者分享一个包含会话 ID 的登录页面链接,而用户登录后,攻击者现在拥有一个有效的会话 ID,可以冒充该用户。如果应用程序在表单或 cookies 中接受会话 ID,也可以进行类似的攻击,尽管后者需要 XSS 漏洞才能利用。

可以通过在用户登录时始终创建一个新会话,并仅通过 cookies 和请求头接受会话 ID 来避免此问题。