秋来冬风的博客

安全登录会话第三版设计方案——公开登录凭据也安全的机制

目录

摘要

本文记录一种安全登录会话第三版设计方案,go实现开源在github safesession库

用于在基于http协议的网络服务中保存用户的登录状态。

设计思路是在Session ID in Cookie Value的基础上改进。

引言

背景

http协议是无状态的,服务器不会记住之前的任何交互,所以需要额外设计来保持登录状态。

常见的保持登录方法有Session和jwt。

jwt的体积大,默认不加密,无法吊销,所以不考虑。

Session可以存储在cookie或localStorage。

cookie是http内在机制,每次发送请求时自动携带在http标头,所以最初选择在Session ID in Cookie Value的基础上改进。

设计原则

服务器保留必要信息验证Session本身是否是自己签发的。

收集有高灵敏度或高特异性的特征验证Session是否被盗用。

尽力避免因更换设备或长途旅行,而导致登录失效。

术语与定义

本文档中出现的以下关键词:“MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、“NOT RECOMMENDED”、“MAY”以及“OPTIONAL”,其含义应按照 BCP 14 RFC2119 RFC8174 中的规定进行解释。而且,这些关键词只有在全部大写的情况下才具有上述含义,正如本文档中所显示的那样。

本文档中的术语如下所述。

  • Session:通过一个随机生成的ID来维持登录状态的机制。
  • safeSession:本文档描述的,在Session ID in Cookie Value的基础上改进的维持登录状态机制。
  • safeSession的必要验证信息:包括ID和CreateTime两个数据。
  • 登录会话凭据:safeSession转换为Cookie值的内容。
  • 调用者:使用safeSession的开发者。
  • 实现:实现safeSession的完整代码。
  • 高灵敏度:能很准确识别safeSession被盗用。
  • 高特异性:能很准确识别safeSession没被盗用。

运行环境

协议要求

safeSession SHOULD 运行在使用https协议的服务,并且SHOULD使用TLS1.3。

客户端要求

在浏览器或实现cookie和user-agent机制的非浏览器运行。

服务器要求

服务器MUST持久化保存safeSession的必要验证信息,并自行清理过期信息。

服务器MUST支持AES-256-GCM,并保证密钥和safeSession的必要验证信息,至少一个不被泄露或伪造。

调用者MUST为safeSession设置一个最大有效期,当现在时间-上一次登录的时间>最大有效期,safeSession因超过最大登录有效期而失效。

safeSession的结构定义

go代码表示

// Session 表示一个登录会话。
type Session struct {
	// ID 对每个登录会话是唯一的。
	ID string `gorm:"primaryKey;type:char(64)"`
	// CreateTime 是上一次登录的时间。
	// 应该命名为LastLoginTime,为了不在生产环境修改数据库表,
	// 所以不改名。
	CreateTime time.Time
	// Ip 是上一次登录的ip信息。
	Ip IPInfo `json:"-" gorm:"-:all"`
	// Gps 是上一次登录的gps信息。
	Gps GpsInfo `json:"-" gorm:"-:all"`
	// CSRF_TOKEN 用来防范跨站请求伪造攻击。
	// 调用者设置它。
	CSRF_TOKEN string `json:"-" gorm:"-:all"`
	// 下列字段是创建登录会话时的客户端设备信息,
	// 和ip信息以及CSRF_TOKEN一起保存在客户浏览器,不在服务器保存。
	Os, OsVersion string `json:"-" gorm:"-:all"`
	// Name 是用来登录的用户的唯一身份表示。
	Name string `json:"-" gorm:"-:all"`
	// Device 是浏览器指纹或设备指纹。
	Device string `json:"-" gorm:"-:all"`
	// Broswer 是浏览器名
	// Browser是正确拼写,为了不在生产环境修改数据库表,
	// 所以不改名。
	// 在非浏览器环境运行时,设置为user-agent提取到的应用名。
	Broswer string `json:"-" gorm:"-:all"`
	Screen  Screen `json:"-" gorm:"-:all"`
	// PNum 是逻辑处理器数量,
	// 通常使用navigator.hardwareConcurrency获取。
	PNum int64 `json:"-" gorm:"-:all"`
}

// IPInfo 是ip信息。
type IPInfo struct {
	Country, Region, City string
	ISP                   string
	Longitude, Latitude   float64
	AS                    int64
}

// GpsInfo 是gps信息。
type GpsInfo struct {
	Longitude, Latitude float64
}

// Screen 是屏幕信息。
type Screen struct {
	Width, Height int64
}

safeSession各字段的生成

int类型的字段,未能获取时应该设置为-1。

float64类型的字段,未能获取时应该设置为1.79769313486231570814527423731704356798070e+308。

Name 遵从调用者的设置。

字符串字段中不得包含"\x00"(go写法)。

ID的生成

MUST由至少256个随机比特位生成。

SHOULD使用加密安全的随机数来源生成,例如windows的ProcessPrng API。

CreateTime的生成

服务器创建safeSession的时间。

Ip的生成

从客户端ip获取结构定义要求的信息,如果未能获取,可以不设置和只设置部分获取到的。

Os、OsVersion、Broswer的生成

从user-agent获取。

OsVersion SHOULD 为大版本号。

Broswer SHOULD 不包括任何版本信息。

Gps、Screen、PNum、Device的生成

这些字段可以设置,但不是必须设置。

浏览器客户端

使用js调用navigator.hardwareConcurrency等浏览器API获取。

SHOULD 在获取Gps信息时,配置enableHighAccuracy=true(高精度模式)。

Device SHOULD 在同一个设备尽可能的生成同样的值,即使浏览器版本升级。

非浏览器客户端

MAY 使用POST请求,通过json格式,将所需字段发送到服务器。

Device SHOULD 在同一个设备尽可能的生成同样的值,即使应用程序版本升级。

safeSession的被盗判断

如果某些字段在创建safeSession时为空,不进行下列判断中涉及这些字段的部分。

Cookie里是被加密的上一次登录数据,user-agent和POST请求提供这一次登录的数据。

高灵敏度特征不一样

Os、Broswer至少一个与上一次登录不一致,safeSession因疑似被盗失效。

高特异性特征可疑

Device与上一次登录不一致且

  • 1.Ip.Isp、Ip.AS号至少一个与上一次登录不一致
  • 2.PNum与上一次登录不一致
  • 3.OsVersion与上一次登录不一致
  • 4.Screen任意字段与上一次登录不一致
  • 5.Ip,Gps的定位数据至少一个与上一次登录差距太大

实现MUST允许调用者覆盖1和5中有关Ip字段的验证。

safeSession因疑似被盗失效。

定位数据差距太大的判定

默认MAY经纬度差距超过50公里,或Ip.Country、Ip.Region不一致视为差距太大。

实现可以将相邻的Ip.Country、Ip.Region出现在两次登录视为一致,而不构成差距太大。

实现MUST支持调用者设置如何判断定位数据差距太大。

safeSession的存储

safeSession的字符串表示

按照如下规则序列化为字符串

根据safeSession的结构定义定义的字段顺序,将每一个字段值从先到后的序列化为字符串,每个字段值后面放8个0比特位分隔。

每一个字段值:

  • 字符串保持原样。
  • 整数序列化为自身的字符串表示。
  • 浮点数序列化为-ddd.dddd的形式,符号和小数位可选。
  • 结构体的每一个字段值从先到后按定义顺序,序列化。
  • time.Time编码为RFC3399Nano格式,示例2026-01-02T15:03:02.1365778+08:00

转换为cookie值

safeSession MUST按下列规则转换为 Cookie 的 Value。

  1. tmp = safeSession的字符串表示
  2. tmp = 服务器使用自己的密钥和AES-256-GCM加密后的tmp
  3. tmp = base32对字符串tmp编码后的值

响应Cookie的要求

  1. 设置Secure和HttpOnly。
  2. 默认Domain为空,实现MUST允许设置其他值。
  3. 默认samesite为Lax,Path为/,实现MUST允许设置其他值。
  4. 设置MaxAge为调用者指定的值。
  5. cookie name为session,实现MUST允许设置为其他name。

服务器保存的数据

与客户端在cookie保存了加密后是所有数据不同,服务器仅保存ID和CreateTime,用于清理过期safeSession,以及验证safeSession是否是自己签发且在有效期内。

safeSession的验证

转换cookie值为safeSession

Cookie 的 Value MUST按下列规则转换为 safeSession。

  1. tmp = Cookie 的 Value
  2. tmp = base32对字符串tmp解码后的值
  3. tmp = 服务器使用自己的密钥和AES-256-GCM解密后的tmp

将tmp按 safeSession的字符串表示 描述的序列化的方法,反过来转换回safeSession。

验证本身的真实性与有效性

MUST执行下列验证

验证

  • 客户端的safeSession的ID是否在服务器保存。
  • CreateTime距离现在未超过调用者设置的最大有效期。

如果不同时满足,删除保留的服务器ID,并响应客户端删除safeSession。

这通常意味着因超过最大登录有效期而safeSession失效。

验证未被盗

按照 safesession的被盗判断 验证。

实现 MUST 允许调用者进行二次验证,在上述验证不通过且可能的时候,通过短信验证码等方式验证是否是真的被盗。

验证附加规则

实现 MUST 提供上述两项验证通过的safeSession给调用者,验证是否满足附加规则,比如一个账号只允许登录一台设备等。

更新字段

上述验证通过后,更新CreateTime为当前时间。

理论论证

以下论证安全性,包括防盗,防伪,量子安全等。

256位的ID使得难以伪造safeSession,CreateTime确保无法使用过期的safeSession。

对称密码是量子安全的,而且目前没有公开的有效攻击方法能够破解正确实现的AES-256-GCM。所以通过使用它加密,同时做到了量子安全,以及大部分数据不存在服务器。减轻了服务器存储开销。

Cookie的安全设置加上一系列的特征验证,加上https的要求,使得即使用户配合通过浏览器开发者工具,窃取了Cookie值,并配合在其身旁使用另一条相同型号的电脑,也不意味着一定能成功盗用。

只要一次验证不通过,safeSession就会失效,意味着即使成功窃取,除非一次满足所有验证,就会攻击失败。

总的来说,safeSession的安全性依赖于下列三点做到至少两点:

  1. AES-256-GCM密钥未泄露。
  2. 服务器数据库保存ID未被篡改。
  3. 登录凭据未泄露。

讨论

公开Cookie Value会影响安全性吗?

不影响,因为经过AES-256-GCM加密的Cookie Value,不能提供任何信息给调用者。

协议使用TLS1.2还安全吗?

还安全,原因如上所述。

注意:协议要求写着 SHOULD使用TLS1.3,主要是出于性能和配置简单的考虑,实践中,如果认为保持旧设备兼容性要价值,可以使用TLS1.2。

因为RFC2119中SHOULD的含义正是 SHOULD表示强烈推荐但不是强制性的;实现者可以不遵守,但需要有充分的理由,并且清楚这样做的后果;

可以设计为不用Cookie吗?

可以,使用LocalStorage、SessionStorage、IndexedDB都能获得一样的性能。

因为Cookie虽然能随请求一起发送,但后续如果选择一个POST请求提供一些验证信息,使用其他机制虽然不自动发送,但跟在POST请求发送是一样的延迟(约增加1RTT)。

而且安全性不变,因为公开其中的值不影响安全性,所以即使其他机制直接不能抵抗XSS也没关系。

可以使用AES-128吗?

可以,虽然Grover 算法可在量子计算机上的AES-128有效安全强度降至约64位,但参考 https://blog.cloudflare.com/nist-post-quantum-surprise/#grovers-algorithmGrover 算法虽在理论上提供二次加速,但受限于量子硬件的物理特性、并行瓶颈和极高操作成本,它对 AES-128 的实际威胁被严重高估。

为什么CSRF_TOKEN是调用者管理?

因为不是只有这一种防范CSRF的办法,所以提供CSRF_TOKEN的支持,但调用者决定是否使用。

为什么不用担心伪造user-agent?

分两种情况

  1. 用户故意修改,最坏导致重新登录,所以不担心。
  2. 攻击者故意修改,因为必须一次猜对所有验证信息,user-agent是否伪造对安全性影响不大。

Device 质量不高会怎么样?

如果Device生成的值容易变化,可能导致在升级台式机CPU,一键更换手机等情况后,需要二次验证或直接登录失效,但不影响安全性。

结语

修正的问题

现在没有了第二版方案里,各种正确情况的误判被盗,并且允许了二次验证。

现在可以在go以外的编程语言实现这种设计了。

支持单点登录,将一次登录用在多个域名。

本次总结

在Session的基础上,通过各种辅助验证,以及对加密的巧妙运用,做到了在安全维持登录会话的同时,自身仅存储少量数据,并且即使泄露登录会话凭据,也不太影响安全性。

开始写时间:2025-11-03

结束写时间:2026-01-02

Tags:
Categories: