登录认证
本文最后更新于 2025年8月25日 下午
参考:
https://juejin.cn/post/6844904034181070861
https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
https://juejin.cn/post/6845166891393089544
概念
认证 Authentication
通俗地讲就是验证当前用户的身份,证明“你是你自己”。互联网中的认证有:
- 用户名密码登录
- 邮箱发送登录链接
- 手机号接收验证码
授权 Authorization
用户授予第三方应用访问该用户某些资源的权限。
- 手机安装应用时,系统会询问是否允许授予权限(访问相册、地理位置等权限)
- 微信小程序登录时,微信会询问是否允许授予权限(获取昵称、头像、地区、性别等个人信息)
实现授权有 Cookie、Session、Token、OAuth 等方式。
凭证 Credentials
实现认证和授权的前提是需要一种媒介(证书) 来标记访问者的身份。
在现实生活中,每个人都有一张专属的居民身份证,是用于证明持有人身份的一种法定证件。通过身份证,我们可以办理手机卡/银行卡/个人贷款/交通出行等等,这就是认证的凭证。
在互联网应用中,一般网站都会有两种模式:游客模式和登录模式。游客模式下,可以正常浏览网站上面的文章,但是如果想要点赞/收藏/分享文章就需要登录或者注册账号。当用户登录成功后,服务端就会给这个用户使用的浏览器颁发一个令牌(Token),这个令牌用来表明用户身份,每次浏览器发送请求时会带上该令牌,就可以使用游客模式下无法使用的功能。
方案
Cookie
HTTP 是无状态的协议(对于事务处理没有记忆能力,每次浏览器和服务端会话完成时,服务端不会保存任何会话信息),每一个 HTTP 请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是否是同一个人。所以服务端与浏览器为了进行会话跟踪(知道是谁在访问),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 Cookie 和 Session 去实现。更详细内容见:Cookie。
所以可以把用户信息直接存放到 Cookie 当中,请求时浏览器会直接携带 Cookie。
Session
Session 是另一种记录服务端和浏览器会话状态的机制。不管用户有没有登录,都会创建 Session。Session 基于 Cookie 实现,Session 被存储在服务端端,SessionId 被存储到客户端的 Cookie。
Session 认证流程:
- 用户首次请求服务端时,服务端会根据用户提交的相关信息,创建对应的 Session
- 服务端请求返回时将此 Session 的唯一标识信息 SessionId 返回给浏览器
- 浏览器接收到服务端返回的 SessionId 信息后,会将此信息存入到 Cookie,同时 Cookie 记录此 SessionId 属于哪个域名
- 用户之后请求服务端时,请求会自动判断此域名下是否存在 Cookie 信息:若存在,自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionId,再根据 SessionId 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作
SessionId 是连接 Cookie 和 Session 的一道桥梁,大部分系统根据此原理来验证用户登录状态。
把用户信息存放到 Cookie 和 Session 的对比:
- 安全性
Session 更安全,Session 把内容存储在服务端端;Cookie 把内容存储在浏览器 - 存取值类型不同
Session 可以是任意数据类型;Cookie 只支持字符串数据,保存其他类型的数据需转换成字符串 - 有效期不同
Cookie 可设置为长时间有效,比如默认登录功能;Session 一般失效时间比较短,客户端关闭(默认情况下)或者 Session 超时都会失效 - 存储大小不同
单个 Cookie 保存的数据不能超过 4K;Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务端资源
Token
Acesss Token
Acesss Token 是访问资源接口(API)时所需要的资源凭证。
特点:
- 服务端无状态化、可扩展性好
- 支持移动端设备
- 安全
- 支持跨程序调用
简单的组成:
- uid(用户唯一的身份标识)
- time(当前时间的时间戳)
- sign(签名,Token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)
身份验证流程:
- 客户端使用用户名跟密码请求登录
- 服务端收到请求后验证用户名与密码
- 验证成功后,服务端会签发一个 Token 并把这个 Token 发送给客户端
- 客户端收到 Token 以后,会把它存储起来(比如放在 Cookie 或者 LocalStorage)
- 客户端每次向服务端请求资源时都需要带着服务端签发的 Token
- 服务端收到请求后验证 Token,如果验证成功,就向客户端返回请求的数据
每一次请求都需要携带 Token 并需要把 Token 放到 HTTP 的 Header 里。基于 Token 的用户认证是一种服务端无状态的认证方式,服务端不用存储 Token 数据。用解析 Token 的计算时间换取 Session 的存储空间,从而减轻服务端的压力,减少频繁的查询数据库。
Refresh Token
Refresh Token 是专用于刷新 Acesss Token 的 Token。没有 Refresh Token 也可以刷新,但是需要用户再输入登录用户名与密码,会很麻烦。有了 Refresh Token 可以减少这个麻烦,浏览器直接用 Refresh Token 去更新 Acesss Token,无需用户进行额外的操作。
Access Token 有效期比较短,当 Acesss Token 由于过期而失效时,使用 Refresh Token 就可以获取新的 Access Token,如果 Refresh Token 也失效了用户就只能重新登录。
Refresh Token 及其过期时间存储在服务端的数据库,只有在申请新的 Acesss Token 时才会验证,不会对业务接口响应时间造成影响。
Session 存储位置
参考:https://juejin.cn/post/6922782392390746125
WebStorage
WebStorage 的作用域遵循浏览器的同源策略,但是也意味着任何在网站上运行的 JavaScript 都可以访问 WebStorage,所以容易受到 XSS 攻击,尤其是项目中用到了很多第三方 JavaScript 类库。
为了防止 XSS,一般的处理是避开和编码所有不可信的数据。但这并不能百分百防止 XSS。因为我们可能使用托管在 CDN、其它一些公共 JavaScript 库,还有像 npm 这样的包管理器导入别人的代码到我们的应用程序中。如果使用的脚本中有一个被盗用,恶意的 JavaScript 可以嵌入到页面上并且盗用 WebStorage。这些类型的 XSS 攻击可以得到每个人的 WebStorage。
这也是为什么许多组织建议不要在 WebStorage 中存储任何有价值信息。以及不要信任 WebStorage 中的信息,包括会话标识符和令牌。作为一种存储机制,WebStorage 不强制执行任何安全标准。
Cookie
优点:
- 可以制定
httponly属性防止被 JavaScript 读取 - 也可以制定
secure属性保证 Token 只在 HTTPS 下传输
缺点:
- 不符合 Restful 最佳实践
- 容易遭受 CSRF 攻击
总结
关于 Token 存在 Cookie 还是 LocalStorage 有两个观点:
- 支持 Cookie 的开发人员强烈建议不要将敏感信息(如 JWT)存储在 LocalStorage,因为它对 XSS 攻击毫无抵抗力
- 支持 LocalStorage 的则认为撇开 LocalStorage 的各种优点不谈,如果做好适当的 XSS 防护,收益远大于风险
放在 Cookie 中看似看全,看似“解决”一个问题(但仍然存在 XSS),却引入了另一个问题(CSRF);放在 LocalStorage 中具有更灵活,更大空间,天然免疫 CSRF 的特征。Cookie 空间有限,而 JWT 一半都占用较多字节,而且有时不止需要存储一个 JWT。确保代码以及第三方库的代码有足够的 XSS 检查,在此之上将 Token 存放在 LocalStorage 中。
在 XSS 面前,即便使用了 httpOnly,Cookie 无法被获取,黑客依然可以诱导或在用户毫不知情的情况下做任何事情。记住,黑客的代码和你的代码一样被用户信任!只要 XSS 存在,那么无论将信息存储在 Cookie 还是 LocalStorage,都是一样脆弱不堪,唯一的区别只是获取难度。XSS 漏洞很难被发现,是因为一个网站的构建不仅仅是基于你自己的代码,第三方的代码同样已可能存在 XSS。
Session 和 Token
Session 是一种记录服务端和浏览器会话状态的机制,服务端有状态化,记录会话信息。Token 则是访问资源接口时所需要的资源凭证,是令牌,服务端无状态化,不记录会话信息。
Session 和 Token 并不矛盾,作为身份认证时 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,但 Session 就必须依赖链路层来保障通讯安全。如果需要实现有状态的会话,仍然可以增加 Session 来在服务端端保存一些状态。
Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionId 的不可预测性暂且认为是安全的。对于 Token,如果指的是 OAuth Token 类似机制,提供的是认证和授权:认证是针对用户,授权是针对 App,目的是让某 App 有权利访问某用户的信息,这里 Token 是唯一的,不可以转移到其它 App 上,也不可以转到其它用户上。Session 只提供一种简单的认证,即只要有此 SessionId,即认为有此 User 的全部权利,是需要严格保密的,此数据应该只保存在站方,不应共享给其它网站或者第三方 App。
所以简单来说:如果你的用户数据可能需要和第三方共享或者允许第三方调用 API 接口,用 Token。如果永远只是自己的网站或 App,用什么都无所谓。
JWT
JWT(JSON Web Token)是目前最流行的跨域认证解决方案,是一种认证授权机制。
举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求是,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后都向持久层请求数据。这种方案优点是架构清晰,缺点是工程量比较大。另外,万一持久层挂了就会单点失败。另一种方案是服务端索性不保存 session 数据,所有数据都保存在浏览器,每次请求都发回服务端,JWT 就是这种方案的一个代表。
JWT 是为了在网络应用环境间传递声明,而执行的一种基于 JSON 的开放标准。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务端获取资源。比如用在用户登录上,可以使用 HMAC 算法或者 RSA 的公/私秘钥对 JWT 进行签名。因为这些数字签名存在,这些传递的信息是可信的。
原理
JWT 的原理是,服务端认证以后,生成一个 JSON 对象,发回给用户。以后,浏览器与服务端通信时都要发回这个 JSON 对象。服务端完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务端会在生成这个对象时会加上签名。服务端不再保存任何 session 数据,也就是说服务端变成无状态,从而比较容易实现扩展。
数据结构
JWT 是一个很长的字符串,中间用点(.)分隔成三个部分:
- Header(头部):这部分是一个 JSON 对象,描述 JWT 的元数据
- Payload(负载):这部分也是一个 JSON 对象,用来存放实际需要传递的数据
- Signature(签名):这部分是对前两部分的签名,防止数据被篡改
写成一行,就是:Header.Payload.Signature。
认证流程
- 用户输入用户名和密码登录,服务端认证成功后,会返回给客户端一个 JWT
- 客户端将 JWT 保存到本地(通常使用 LocalStorage,也可以使用 Cookie)
- 此后,客户端每次与服务端通信时都带上这个 JWT
使用方式
客户端收到服务端返回的 JWT,可以储存在 Cookie 里,也可以储存在 LocalStorage。
- 当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求头信息的
Authorization字段里,使用 Bearer 模式添加 JWT,所以内容看起来是这样:Authorization: Bearer <Token>。服务端的保护路由将会检查请求头Authorization中的 JWT 信息,如果合法,则允许用户的行为。因为 JWT 是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要。又因为 JWT 没有使用 Cookie,所以可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题 - 另一种做法是,跨域的时候,把 JWT 放在 POST 请求的数据体中
- 还可以通过 URL 传输,如:
http://www.example.com/user?Token=xxx
特点
- JWT 默认不加密,但也可以加密,生成原始 Token 以后,可以用密钥再加密一次
- 不加密 JWT 的情况下,不能将秘密数据写入 JWT
- JWT 不仅可以用于认证也可以用于交换信息,有效使用 JWT 可以降低服务端查询数据库的次数
- JWT 本身包含了认证信息,一旦泄露任何人都可以获得该令牌的所有权限。因此为了减少盗用,JWT 有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证
- JWT 最大的优势是服务端不再需要存储 Session,使得服务端认证鉴权业务可以方便扩展。但这也是 JWT 最大的缺点:由于服务端不需要存储 Session 状态,因此使用过程中无法废弃某个 Token 或者更改 Token 的权限。就是说一旦签发了 JWT,到期之前就会始终有效,除非服务端部署额外的逻辑
对比
JWT 是 Token 的一种,和普通的 Token 相比,有以下区别。
相同:
- 都是访问资源的令牌
- 都可以记录用户的信息
- 都是使服务端无状态化
- 都是只有验证成功后,客户端才能访问服务端上受保护的资源
不同:
- 服务端验证客户端发送的 Token 时,在解析 Token 数据后,还需要去数据库查询获取用户信息,然后验证 Token 是否有效
- JWT 是将 Token 和用户信息加密后一起存储在客户端,服务端只需使用密钥解密进行校验即可,不需要再查询或者减少查询数据库
SSO
问题
使用 Cookie 时需要考虑
- 把用户信息存储在客户端,容易被篡改,使用前需要验证合法性
- 不要存储敏感数据,比如用户密码,账户余额
- 使用
httpOnly属性在一定程度上提高安全性 - 设置正确的
domain和path属性能减少数据传输 - 尽量减少 Cookie 的体积,能存储的数据量不能超过 4KB
- 一个浏览器针对一个网站最多存 20 个 Cookie,浏览器一般只允许存放 300 个 Cookie
- 移动端对 Cookie 的支持不好,而 Session 需要基于 Cookie 实现,所以移动端常用 Token
使用 Session 时需要考虑
- 把用户信息存储在服务端,当用户同时在线量比较多时这些 Session 会占据较多的内存,需要在服务端定期清理过期 Session
- 当网站采用集群部署时,会遇到多台 Web 服务端之间如何做 Session 共享的问题,因为 Session 是由单个服务端创建的,但是处理用户请求的服务端不一定就是创建 Session 的服务端,那么该服务端就无法拿到之前已经放入到 Session 中的登录凭证之类的信息。当多个应用要共享 Session 时,除了以上问题还可能遇到跨域问题,因为不同的应用可能部署的主机不一样,需要在各个应用之间做好 Cookie 跨域的处理
- SessionId 是存储在 Cookie 中的,如果浏览器禁止 Cookie 或不支持 Cookie,一般会把 SessionId 跟在 url 参数后面,即重写 url,所以 Session 不一定非得需要靠 Cookie 实现
- 移动端对 Cookie 的支持不好,而 Session 需要基于 Cookie 实现,所以移动端常用 Token
使用 Token 时需要考虑
- 如果不把 Token 放到 Cookie,可以避免 CSRF 攻击
- 移动端对 Cookie 的支持不好,而 Session 需要基于 Cookie 实现,所以移动端常用 Token
使用 JWT 时需要考虑
- JWT 适合一次性的命令认证,颁发一个有效期极短的 JWT,即使暴露了危险也很小,由于每次操作都会生成新的 JWT,因此也没必要保存 JWT,真正实现无状态
- 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输
使用加密算法时需要考虑
- 绝不要以明文存储密码或发送密码,即使是对密码的所有者也应该这样。如果你需要 “忘记密码” 的功能,可以随机生成一个新的一次性的(这点很重要)密码,然后把这个密码发送给用户
- 永远使用哈希算法来处理密码,绝不要使用 Base64 或其他编码方式来存储密码,后者是以明文存储密码是一样的。编码以及加密,都是双向的过程,而密码应是保密的,应该只被它的所有者知道,这个过程必须是单向的,哈希正是用于做这个的。从来没有解哈希的说法,但是编码存在解码,加密存在解密
- 绝不要使用弱哈希或已被破解的哈希算法,像 MD5 或 SHA1 ,只使用强密码哈希算法
关闭浏览器 Session 就消失
不是。对于 Session 来说,除非程序通知服务端删除一个 Session 否则服务端会一直保留,程序一般都是在用户退出的时候发个指令去删除 Session。
然而浏览器从来不会主动在关闭之前通知服务端它将要关闭,因此服务端根本不会有机会知道浏览器已经关闭。之所以有这种错觉,是大部分 Session 机制都使用会话 Cookie 来保存 SessionId,而关闭浏览器后这个 SessionId 消失,再次连接服务端时也就无法找到原来的 Session。如果服务端设置的 Cookie 被保存在硬盘上,或者使用某种手段改写浏览器发出的 HTTP 请求头,还把原来的 SessionId 发送给服务端,那么再次打开浏览器仍然能够使用之前的 Session。
恰恰是由于关闭浏览器不会导致 Session 被删除,迫使服务端为 Session 设置一个失效时间,当距离客户端上一次使用 Session 的时间超过该失效时间时,服务端会认为客户端已经停止了活动,才会把 Session 删除以节省存储空间。