安全登录会话第三版设计方案——公开登录凭据也安全的机制
目录
摘要
本文记录一种安全登录会话第三版设计方案,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。
- tmp = safeSession的字符串表示
- tmp = 服务器使用自己的密钥和AES-256-GCM加密后的tmp
- tmp = base32对字符串tmp编码后的值
响应Cookie的要求
- 设置Secure和HttpOnly。
- 默认Domain为空,实现MUST允许设置其他值。
- 默认samesite为Lax,Path为/,实现MUST允许设置其他值。
- 设置MaxAge为调用者指定的值。
- cookie name为session,实现MUST允许设置为其他name。
服务器保存的数据
与客户端在cookie保存了加密后是所有数据不同,服务器仅保存ID和CreateTime,用于清理过期safeSession,以及验证safeSession是否是自己签发且在有效期内。
safeSession的验证
转换cookie值为safeSession
Cookie 的 Value MUST按下列规则转换为 safeSession。
- tmp = Cookie 的 Value
- tmp = base32对字符串tmp解码后的值
- 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的安全性依赖于下列三点做到至少两点:
- AES-256-GCM密钥未泄露。
- 服务器数据库保存ID未被篡改。
- 登录凭据未泄露。
讨论
公开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?
分两种情况
- 用户故意修改,最坏导致重新登录,所以不担心。
- 攻击者故意修改,因为必须一次猜对所有验证信息,user-agent是否伪造对安全性影响不大。
Device 质量不高会怎么样?
如果Device生成的值容易变化,可能导致在升级台式机CPU,一键更换手机等情况后,需要二次验证或直接登录失效,但不影响安全性。
结语
修正的问题
现在没有了第二版方案里,各种正确情况的误判被盗,并且允许了二次验证。
现在可以在go以外的编程语言实现这种设计了。
支持单点登录,将一次登录用在多个域名。
本次总结
在Session的基础上,通过各种辅助验证,以及对加密的巧妙运用,做到了在安全维持登录会话的同时,自身仅存储少量数据,并且即使泄露登录会话凭据,也不太影响安全性。
开始写时间:2025-11-03
结束写时间:2026-01-02