登录和授权:Cookie与Authorization Header机制详解

前言

登录和授权这两个词的意思很相近,登录 是身份认证的过程,验证你就是账号的所有者。授权则是将权限或是可以象征权限的东西授予给你,让你能够做一些拥有权限才能做的事,比如获取用户信息。

这么看来,登录的目的其实就是为了获取权限。所以,在实际应用中,多数情况下"登录"和"授权"区别并不大。

我们来看看登录和授权的两种方式,分别是 CookieAuthorization Header。Cookie 已经存在很久了,而 Authorization Header 在当前的 Web API和移动应用中更加流行,我们先来看看第一个------Cookie

Cookie 的起源是购物车。当时有浏览器公司会帮别人开发网站,恰好一个电商网站提出了购物车功能的需求,想要将购物车的数据存储到本地,浏览器的开发者就为此新增了一个 Cookie 的功能。

Cookie 可以在本地存储任何服务器想要存储在本地的数据。

工作机制

它的工作机制是: 服务器将想要客户端存储的数据放到 Set-Cookie Header 中,通过响应报文发送给客户端,客户端按照协议自动将数据存在本地。

比如用户往购物车添加了苹果,那么浏览器会给服务器发送请求,Body 中包含表示苹果的数据。

服务器会将苹果数据放到 Set-Cookie Header 中进行响应,表示让客户端存储当前的 Cookie 值。

客户端收到响应后,会自动将 Set-Cookie 的值存在浏览器的缓存中,之后浏览器就可以使用这个 Cookie 与服务器进行交互,即在请求中携带此 Cookie。

并且 Cookie 对应的目标服务器也会被一并存储下来,这是为了区分 Cookie 所属的域名。防止出现访问 A 网站,却带上了 B 网站 Cookie 的情况。

当用户往购物车又添加了一根香蕉时,浏览器又会发起请求,但此时会自动将之前存储的 Cookie 携带上。

服务器返回时,会返回计算之后的 Cookie 值。

客户端收到响应结果后,发现当前要存储 cart 这个 key 已经存在,就会覆盖它的值,更新为 "apple=1&banana=1"

整个过程,计算由服务器完成,存储由客户端完成。当用户要结账时,只需发送结账的请求,购物车相关信息会通过 Cookie Header 被携带上。

现在看来,这个机制可能有点多余。因为既然信息都要存储本地,直接在本地进行计算不就行了吗?

这是因为当时客户端脚本能力很弱,并不能方便地进行计算,直到 JavaScript 的出现和发展,前端才有了逻辑运算能力。

  • 会话管理: 登录状态、购物车等。

    登录时,客户端会将用户名和密码发送给服务器。如果验证通过,服务器就会将会话信息记录下来,并且将会话 id 通过 Set-Cookie 返回。会话你可以理解为用户登录到用户退出(登录)的完整过程,session_id 是权限证明,让无状态的 HTTP 能够识别当前请求是同一次会话中的。

    会话 id 可用于后续的身份认证,只有携带上这个会话 id,才能进行后续操作,比如获取用户信息、修改头像、昵称。

    同一个服务器,可以存放多个 Cookie,只需 key 不同即可。所以我们才能既保存购物车信息,又持有会话 id。

  • 个性化: 用户偏好、主题

    比如改变网站的显示风格时,也会将此次改变发送给服务器。在服务器返回新的页面数据之前,服务器会将此次更改与 client_id 关联并记录在服务端,以便下次该用户访问网站时,能够提供最新的配置。

  • Tracking: 分析用户行为

    使用 Cookie 分析用户行为的工作模式是:

    比如从网站 A 访问一张由第三方网站 B 提供的图片时,可以在图片链接后加上 ?from=shop.com,用来给网站 B 的服务器提供额外的信息:你是从哪个网站中访问到这张图片的。

    更常见的是,在网页加载含有第三方网站 B 的资源时,浏览器会自动请求这个资源链接。此时,网站 B 的服务器就可以在响应中设置其专属的 Cookie(例如 client_id)。这样,当用户访问了其他同样内嵌了网站 B 资源的网站 C 时,浏览器会再次请求并自动携带上这个 client_id,从而让第三方网站 B 得知同一个用户访问了网站 A,又访问了网站 C。

    这样第三方会越发了解该用户的画像,以便更好地推广告。

总之,Cookie 是一种客户端帮助服务端记录信息的工作机制。虽然它非常经典,但在处理登录状态时,现在我们更多会使用专门为授权而设计的 Authorization Header。

Authorization

为什么呢?

因为它本就是 HTTP 中设计用来做登录授权的 Header,更加标准和灵活。

它有两种主流用法,第一种是 Basic。

Basic

格式为:Authorization: Basic <username:password(Base64ed)>

其中 (Base64ed) 的意思是将 username:password 这个字符串进行一次 Base64 编码。比如一个获取用户信息的请求报文可以是这样的:

sql 复制代码
GET /user/1 HTTP/1.1
Host: example.com
Authorization Basic dXNlcm5hbWU6cGFzc3dvcmQ=

如果服务器验证成功,就可以成功获取到用户信息;否则返回的状态码是 401,表示用户未授权,需要认证。

Basic 这种用法很实用,但是有安全风险。

  • 账号、密码使用 Base64 编码进行传输,如果被截获,会导致账号、密码泄露。不过当启用了 HTTPS 后,整个请求报文会被加密,也就无法得到 Token 值了。

  • 在客户端会反复使用到 Token,所以会将 Token 存储到本地。要是手机被 Root 了,且用户授予了某个恶意软件 Root 权限,就可能会导致 Token 被恶意软件盗取。

    不过一般来说,用户是不会主动将手机的安全性破坏的。

我们再来看第二种,叫 Bearer。

Bearer

Bearer 可以翻译为 "持有人",也就是持有了访问令牌(Token)的意思。

它的格式是:Authorization: Bearer <bearer token>

bearer token 的值一般是从授权方获取,而非本地计算得出。

最常见的获取方式是通过 OAuth2 的授权流程,在讲它的流程之前,我们先来看看第三方登录。

在掘金点击第三方登录后,会弹出如下窗口:

弹出这个窗口的目的是进行第三方授权,让 Github 将你的信息的获取权限授予给掘金。此时授权的第三方是掘金,为什么呢?你在 Gitbhub 的信息权限本是你和 Github 约定的。

授权后,掘金会通过这个权限拿到你在 Github 上的信息,比如用户名、头像等,然后使用这些信息注册一个掘金账号(如果未注册的话)并登录该账号。

这就是第三方登录,它内部包含了一个第三方授权的过程。知道了这些,我们来看第三方授权的实际过程,也就是 OAuth2 的流程

  1. 首先,第三方网站 A(如掘金)会向授权方网站 B(如 Github)申请第三方授权合作,得到 client_idclient_secret

  2. 我们在网站 A,点击了第三方登录,就会跳转到网站 B,并带上 client_id,使得授权方能够根据该 client_id 获取需要给用户展示的信息,比如上图中的图标、名称、授予的权限、授权后的重定向网址等。

  3. 在用户授权后,网站 B 会返回一个 Authorization Code 给网站 A,这个是授权码,不过它并不是 Token,而是用来获取 Token 的。

    为什么不直接返回 Token?

    为了安全,因为 OAuth2 的授权过程并不强制使用 HTTPS。如果使用 HTTP,可能会导致 Token 泄露。并且用户电脑环境的安全性也无法保证,所以 Token 的传递不应该经过用户浏览器。

  4. 客户端拿到 Authorization Code 后,会将其发送给自己的服务器,让服务器通过授权码以及 client_secret,向授权方获取 Token。

    其中 client_secret 是密码的作用,用于验证身份。这个是高度机密的凭证,绝对不能泄露,一般是安全地保存在服务器上。

此时 OAuth2 授权的流程就结束了。

接着还有登录流程:

服务器 A 会使用获得的 Token,向网站 B 发送请求获取用户信息。然后在自己的服务器中完成用户账户的注册(或匹配已有账户),并登录。然后将登录成功的响应返回给浏览器,浏览器根据响应显示登录成功的页面。

现在,第三方登录的整个过程就结束了。

在现实中,有些实现会选择将 Token 发给客户端,虽然这样可行,但也破坏了 OAuth2 精心设计的安全性,因为这样可能会导致 Token 在不安全的环境中泄露。

另外,Bearer Token 也可以在自己的 App 中使用,其工作方式是这样的:

  1. 向自己的服务器发起登录请求,其请求报文可能是这样的:

    H 复制代码
    POST /login HTTP/1.1
    Host: api.example.com
    
    username=my_username&password=my_password
  2. 服务器的响应可能为:

    H 复制代码
    HTTP/1.1 200 OK
    ...
    
    {"access_token":"eyJhbGciOiJFZERTQSIsImtpZCI6IkNNR1RRMlZFOTIifQ.eyJleHAiOj..."}
  3. 然后客户端会将上述的 token 值存到本地,后续请求会带上这个 token,来作为权限的证明。

最后我们来看看 Refresh Token,它大概长这样:

json 复制代码
{
  "token_type":"Bearer",
  "access_token":"xxxxx",
  "refresh_token":"xxxxx",
  "expires_time":"xxxxx"
}

我们可以使用 access_token 来完成获取用户信息等操作,而 refresh_token 则是一种长期有效的凭证,用于在 access_token 过期后,无需与用户再次交互,即可获取一个新的、有效的 access_token

为什么要这么做?

首先,为了安全,access_token 的有效期会比较短(如几小时)。当它失效后,我们可以使用 refresh_token 来刷新它,从而避免让用户多次重新登录授权,提高用户体验;

其次,Token 还是有可能泄露的。使用 refresh_token 机制,能够降低 access_token 泄露所造成的危害。同时,服务端也可以通过 refresh_token 在必要时,让某个用户的所有凭证失效。

所以 refresh_token 的重要性也很高,不亚于用户的密码,它需要被安全地保存,不能泄露。

相关推荐
怀旧,3 分钟前
【Linux系统编程】13. Ext系列⽂件系统
android·linux·缓存
Dabei4 分钟前
Android 语音助手简单实现与语音助手“执行任务”交流
android·前端
jzlhll12317 分钟前
android NDSDManager onResolveFailed errorCode=3的解决方案
android
芦半山44 分钟前
四年之后,重新审视 MTE:从硬件架构到工程落地
android·安全
2501_916007471 小时前
iOS与Android符号还原服务统一重构实践总结
android·ios·小程序·重构·uni-app·iphone·webview
allk551 小时前
Android 屏幕适配全维深度解析
android·性能优化·界面适配
Android系统攻城狮1 小时前
Android ALSA驱动进阶之获取采样格式位宽snd_pcm_format_width:用法实例(九十八)
android·pcm·音频进阶·alsa驱动
莫比乌斯环2 小时前
【日常随笔】Android 跳离行为分析 - Instrumentation
android·架构·代码规范
aningxiaoxixi2 小时前
android 媒体之 MediaSession
android·媒体
GoldenPlayer2 小时前
Android文件权限报错
android