网络请求之SessionID VS Token

sessionID是什么?为什么需要sessionID?

设想一个场景:我要拉取别人给我的回复或其他我的信息。我就要把我的用户ID发给服务端,这样服务端才能知道我是谁然后去给我查消息;但是服务端又怎么才能确认这个请求就是"我"发出的而不是其他人知道我的用户ID然后冒充的呢?使用密码。于是这个请求当中就需要带上 用户ID+用户密码,服务端收到请求后就用用户ID和用户密码去数据库里验证,看id和密码是否对得上,验证通过后再去拿数据回复。单独来看这个流程似乎没问题,但是数据库查询是一个很重的操作,当并发的请求很多时,海量地数据库查询将可能把数据库挤爆。为此,sessionID就被提出来了。原理是这样:当账户第一次登录时,用户ID和密码势必不可少,服务端收到后去数据库查询、验证也无法避免,但是验证通过后服务端会生成一个sessionID,生成方法各种各样,但是唯一不变的是这个sessionID具有唯一性,并且服务端会将该sessionID以键值对(可以理解为Map结构)的形式sessionID:用户信息 存放在内存 当中,并在回复客户端(sessionID方案主要用在浏览器端,所以这里的客户端可以看做浏览器)时捎带给客户端。客户端收到后保存到本地 ,下次请求时就带上sessionID,服务端收到后用该ID去内存里查询就知道用户ID了,然后就可以据此去获取对应的回复数据了。所以使用sessionID可以避免每次请求都要去数据库里查询并验证用户身份,大大减轻数据库操作的负担。

以上就是sessionID的大致原理。下面再补充一点详细细节,可以更好的理解http协议字段。服务端回复时,会在响应头里写一行类似 Set-Cookie: sessionId=abc123 的数据。浏览器会自动把这个字符串存到本地磁盘,并且在下次请求时,自动在请求头里加上 Cookie: sessionId=abc123。开发者完全不需要写代码去手动处理。

sessionID安全吗?听起来很容易泄漏呀

如果没有采用加密,比如使用http协议而非https,那么通过网络嗅探、抓包的方式就可以获取到sessionID;在Root/越狱的手机上,如果APP把sessionID存到了本地,也会泄漏;如果网站存在漏洞,攻击者植入恶意 JS 代码,也可以读取浏览器 Cookie 中的 SessionID 。种种情况表明,sessionID有可能泄漏

Token是什么?

对于sessionID这种方案,浏览器通常都会自动完成,并不需要开发者特意去实现,但是在APP端却没有这套自动实现的机制。App 收到服务端返回的 sessionId 后,你必须手动 把它存到 Android 的 SharedPreferences 里或其他地方;下次发请求时,你必须在请求头Header里手动设置进去------更何况移动端其实用的是Token机制,而非session/cookie机制。

为什么移动端不像浏览器那样自动完成这些呢?

Cookie 的核心价值是跨页面携带状态 + 域名沙箱隔离,这是浏览器的刚需。浏览器是网页渲染工具,注定了它要访问各种不同的网站,要在不同网页、不同域名之间来回跳转,并能在必要的时候保持连接的连续性(比如登录账户后,跳转到不同网页,但需要不同网页之间仍然保持登录状态)。浏览器拥有统一全局会话池,所有页面、请求共用一套 Cookie 内存 + 持久化数据库;内核自动执行完整 Cookie 规则:解析Set-Cookie、匹配 Domain/Path/SameSite、过期清理、请求自动附加 Cookie,整套逻辑底层封装,业务代码零介入。

App 本身不是网页渲染工具,尤其在发展初期,它只是被定位为一个轻量的访问客户端,所以它只提供基础的网络访问能力,所以没有像浏览器那样设计一个全系统级别的统一管理中心,而且出于安全的需求,也不该出现这样的全局管理中心。那么,把APP当做浏览器不就行了?也可以在APP内部实现一套自动cookie机制呀!理论上确实可以,但是这是一套复杂的机制,每个APP都自动实现这样一套机制,得不偿失。另外,移动端都有WebView组件,它有着自己的一套cookie机制,这就导致了一个极其头疼的 Bug:你在网页里登录了(WebView 存了 Cookie),但原生界面的头像还是未登录状态。为了解决这类问题就又要想方设法。

简而言之,让移动端也实现自动Cookie的机制将会带来严重的得不偿失,远不如手动添加cookie灵活、方便。还有一个原因,有更好的选择------Token,令牌。

Token 是一串带有签名的字符串,它自己就能证明"我是谁",而不需要服务器专门在内存里存一份档案去查。下面以最常见的JSON Web Token(JWT)为例讲一讲Token的原理。

JWT的结构由三部分构成,xxxxx.yyyyy.zzzzz,中间用点隔开:

Header (头部):声明这是什么类型的令牌,以及用了什么加密算法。

Payload (负载):这是核心! 里面直接存放了用户的身份信息(比如 UserID、角色权限、有效期 等)。有固定字段,也支持自定义字段。

Signature(签名):服务端用私钥对前两部分生成的防伪水印,防止数据被篡改。

当APP登录账户时,跟sessionID/Cookie方案一样,服务端收到请求后也需要去数据库里验证,验证通过后,服务器开始"造"Token:把用户信息(UserID、角色、过期时间等)放进 Payload,用服务器私钥对 Header + Payload 算出一个签名(Signature),把三部分拼在一起,生成一串 JWT 字符串放在对客户端的回复体中。APP 收到 Token 后,把它存进本地安全存储区(iOS 的 Keychain / Android 的 EncryptedSharedPreferences)。之后的请求里APP 把 Token 手动塞进 HTTP 请求头里,格式通常是 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx.yyyyy;服务端收到请求后取出Token,用同样的私钥,对 Token 的 Header + Payload 重新算一遍签名,把算出来的签名和 Token 里的 Signature 对比,如果一致 → 说明 Token 合法且未被篡改,如果不一致 → 直接拒绝,返回 401。签名验证通过后**,**服务器直接从 Payload 里解析出 UserID 等信息,拿去处理业务逻辑。

sessionID/Cookie 和 Token 的比较

(1)sessionID/Cookie机制虽然单个占用内存很小,但是当用户量很大时,其消耗的内存仍是一大开销,而Token机制无此开销。

(2)sessionID是在接收到登录请求的服务端(A)生成并被(A)保存起来,如果后面的请求被分派到了其他服务端(B),则B没有该sessionID就会认为该请求无效。为了解决该问题,就还要做sessionID共享、同步这些措施,增加了复杂性。Token机制也没有该困扰,只要算法、密钥一样,任何服务端都适用。

(3)如果使用http协议,sessionID和Token都有被泄露的可能,因为可以通过抓包直接看到明文(Token是经Base64编码后的明文),理论上都可以被第三方伪造请求并拉取到数据,所以一定要使用https协议。

(4)sessionID/Cookie机制,如果服务端主动删除存于内存中的sessionID就可以强制客户端下线,因为客户端发来的请求将被判为无效;Token机制无法由服务端强制客户端下线,除非Token时效到期。所以为了解决这类问题又衍生出了一些辅助手段 AccessToken + RefreshToken

AccessToken + RefreshToken

前面已经说过,Token是有有效期(在payload部分),如果有效期很长,一旦泄露,恶意者就有很长时间搞破坏,如果有效期很短,则用户就要频繁登录,为兼顾两者就推出了AccessToken + RefreshToken方案,其实就是普通Token机制和sessionID机制的融合。

短时效凭证(AccessToken):日常业务调用,缩短被盗后的风险窗口;

长时效凭证(RefreshToken):仅用来换新凭证,不用频繁登录,同时后端留存记录,支持强制下线。

阶段 1:用户登录(首次鉴权)

用户提交账号密码 / 验证码 / 第三方授权登录;

后端校验账号密码,数据库验证身份;

生成短期 AccessToken(JWT)+ 长期 RefreshToken;

将 RefreshToken、用户 UID、当前设备 ID 存入 Redis;

两个令牌一同返回客户端;

App 把双 Token 写入本地加密存储。

阶段 2:正常业务请求(AccessToken 未过期)

网络全局拦截器自动读取本地 AccessToken,塞进请求头;

后端校验 JWT 签名、判断未过期;

解析 UID 执行业务逻辑,返回数据。

阶段 3:AccessToken 过期,自动无感刷新(核心流程)

调用业务接口,后端检测 Token 过期,返回 401;

客户端捕获 401,暂停后续业务请求,发起单独刷新接口 /api/refresh_token;

刷新请求携带:RefreshToken + 当前设备 ID;

后端刷新接口逻辑:

① 查询 Redis,校验该 RefreshToken 是否存在、未过期、未拉黑;

② 校验 RefreshToken 绑定的设备 ID 与当前请求设备一致;

③ 校验通过,生成全新 AccessToken 返回;

客户端覆盖本地旧 AccessToken;

自动重试刚才失败的业务接口,用户全程看不到登录弹窗,无感知。

阶段 4:RefreshToken 失效两种场景

场景 A:Refresh 自然过期

刷新接口返回 401/403;客户端清空本地所有 Token,跳转登录页,需要重新输入账号登录。

场景 B:主动强制下线(核心优势,纯 JWT 做不到)

触发场景:手动退出登录、异地登录、账号封禁、用户主动注销设备;

后端操作:删除 Redis 中该设备对应的 RefreshToken;

下次客户端刷新 AccessToken 时,校验失败,App 清空凭证,强制跳转登录。

理论上,sessionID/Cookie机制也存在过期问题,一旦过期就只能重新输入账号和密码。也有一些提升体验的措施,不做过多介绍。