如何解决身份验证问题?

238 阅读11分钟

单系统身份验证问题

这里有几种方案

  1. 基于 cookie
  2. 基于 session
  3. 基于 token

基于 cookie

直接将身份信息写到 cookie,或明文或进行简单的加密

这种方式没太多好说,就是将身份信息直接写在了 cookie,好处是很简单,除了在响应里面加上了 set-cookie 字段把信息写进去就行了,坏处是信息容易泄露和篡改并且长度有限。在登录后别人一看你 cookie 里的键值对就把你传了几个参数、参数名和内容看完了(如果是明文),没有通过登录的步骤,自己填写 cookie 里的信息就直接通过了身份验证,这显然是不太好的。

所以单纯只用 cookie 的做法除了偷懒或者是对于保密性没有什么要求的情况之外,并不怎么使用。但是 cookie 是后面两种方法的基础。

基于 session

session 是指后端存放用户身份和登录状态的地方,其处理流程如下:

  1. 浏览器发送账号密码信息,服务端检查数据库,核验用户信息
  2. 服务端将用户登录状态存为 session,生成一个唯一的 sessionId
  3. 将 sessionId 存到 set-cookie 中,响应浏览器请求
  4. 浏览器再次请求时检验 sessionId 的合法性可以判断登录状态,通过 sessionId 取出对应 session 中的信息可以知晓用户身份

可以看出此时无论用户有多少身份信息,暴露在 cookie 中的都只用 sessionId,并且这里面的信息是很难篡改的(除非你闲的没事登录多一个账号,并在登录的有效期内把 sessionId 抄出来用),因为信息完全存储在服务端,所以信息的保密性是很好的——你没办法从 cookie 中破译出 “这里使用了session 来鉴权” 以外的其他信息。

通过这种方式进行鉴权还有一个问题需要考虑,那就是 session 存在那里。

  1. 内存型数据库,例如 redis,访问快,有一定的持久化能力
  2. 内存,例如直接开个 map 或者放在某个变量里面,简单,重启服务后无论状态有没有过期信息都没了。
  3. 数据库,例如 mysql,在这种频繁读写的情境下性能一般

还有一个问题就是在分布式的场景下,不同机器上的 session 内容可能不一致,如果如此,对于一个用户而言,处理后续业务请求的机器和原先处理登录请求的机器不是一台时 session 就不管用了。

解决方案有两个角度:

  1. 从存储来看,把所有 session 存在一起,使用独立的 redis 或者数据库
  2. 从分布的这个角度看,我们只需要保证同个 ip 的请求总是被导到同一台机器上进行处理即可。

通常第一种方案会好一点,因为分布式本身就是为了让每台机器都能够处理请求分摊压力,避免某台机器宕机,而不是一台很繁忙一台空闲,第二种方案在某种情况下是会造成这样的情况,和使用分布式的初衷有出入。

基于 token

session 带来的困扰是,后台需要存储额外的信息:登录状态。所以相对的,token 的解决方法是把所有登录状态信息打包好一次性编码成为 token,具体过程如下:

  1. 浏览器发送账号密码信息,服务端检查数据库,核验用户信息
  2. 把用户信息提前和配置好的一些用于 token 验证信息的信息一起编码成 token,通过 set-cookie 随登录接口的响应返回
  3. 浏览器再次访问业务接口,客户端对于 其携带的 cookie 种的 token 进行解码,核验,取出状态信息,业务处理

当然,世界上没有尽善尽美的事情,token 解放了服务端的存储资源,token 的编码就消耗了更多的计算资源用于编解码。

在加密不会给破解的前提下,他也不会暴露出额外的信息,cookie 中也只有 token 这一个键,推断不出有什么参数;如果给 token 加入的签名,也能判断是否经过篡改

在 token 中我们主要讨论的点就是这个 ”编码方式“,从原理上来讲这个编码方式只要满足在难破解的基础上尽量容易编码就行,加密的强度也可以具体的业务具体分析。

有一个比较成熟的 token 字符串编码方案就是 JWT(JSON Web Token)这种方式是将 token 中的字符串信息和 JSON 数据进行转换,这个方案只保证权限的鉴定,不保证。JWT 由三个部分组成:

  1. 头部 header

    这部分包含两个信息:声明 token 的类型(就填 JWT )、加密算法。将此 JSON 数据通过 base64 编码得到 token 的第一部分。

    {
      'typ': 'JWT',
      'alg': 'HS256'
    }
    
  2. 负载 playload

    JWT 的标准里面有一些标准的键,这个标准是开放的,建议但不强制使用,在此不展开。这一部分中含有的信息就是带有身份信息的 JSON。同样,base64编码后成为第二部分。

    {
    	"user" : "hengheng",
    	"pwd" : "123456",
    	"delete_at" : "2022-02-02 22:58"
    }
    
  3. 签名 signature

    这部分由 header(base64) 和 playload(base64) 用 . 连接接后的字符串、secret(自定义的密钥,用于加盐) 两者拼接后使用头部中声明的加密算法进行加密得到

    base64是一种将二进制和字符串进行转换的编码方式,base 的是 64,说明编码后有 64 个字符(a-zA-z0-9,+,/),具体操作是将二进制信息六个六个分为一组,再通过对应关系转成字符,为什么是六个一组那当然是因为 26=642^6=64 ,分组后末尾一组不够六位就低位补 0 ,分组后每一组都高位补两个 0 ,凑齐一个字节方便处理

这三部分用 . 连接后就得到了 JWT。通过过程就可以看出,负责信息的 playload 部分其实就是个明文,所以不能存敏感信息,但由于有第三部分来保证 token 是服务端签发,可以验证访问者的身份。密钥 secret 千万不能泄露。

还有一个使用 token 会出现问题的点是,过期时间也是被打包编码进入 token 的,要想修改就得整个重新编码,如果每次验证完都修改过期时间会比较耗时,在 session 方案中这个问题是不存在的,直接在服务端,延长对应 session 的过期时间即可,无需 sessionId 重新生成。

session 和 token 的对比和总结

做一个比喻的话,使用 session 就有点像使用学生卡,我们进宿舍需要刷卡,机器通过识别学生卡发射出的电信号(sessionId )后请求后台,后台利用存储的数据(session)判断学生卡是不是我们学校的,学生卡有没有过期,然后通知门禁是否放行。token 的方法有点像准考证,监考员通过准考证上的盖章判断证件的真实性,再看看照片和你长的像不像,就能通过上面的姓名、班级等信息知道考生的身份了。

session 和 token 两个方案之间的差别我感觉主要是集中于 真正的身份信息存储位置,服务端存数据 / 不存数据

需要澄清的一点是实现这两种身份验证并不是一定要依赖于 cookie,这也是为啥她两为啥不直接叫 cookie 身份验证了,但是它是使用的最方便的,因为它就是为了解决 http 无状态的身份验证专门造的。另外还有 localStorage、sessionStorage 也可以让客户端存放身份凭证,前者没有有效期,不删除一直都在,常用于界面的设置,后者浏览器一关闭就销毁了,属于一次性用品。

多系统身份验证问题

这里提出的问题是,假如我这家公司有多个服务系统,我希望我的用户在 A 系统(或者任一系统)登录后,无需重新登录就可以直接在 B 系统(另外任一系统)中获取到登录的状态。这是一种 “一次登录,全线通行” 的要求,达成这样的效果我们称之为单点登录,在任意一个系统点登录后就相当于在所有系统中完成了身份的验证。

虚假的 单点登录 [同一域名下]

不同的服务可能分散在不同的域名下,此时访问不同系统时带上的 cookie 是不一样的,所以在跨域名访问不同的系统时,即使不嫌麻烦地将所有身份验证的方式方法、密钥都搞成一样的,身份验证也会受到阻碍。如果万一,不同的系统域名不同但是其实都是在一个主域名下,那么直接将 cookie 的 domin 属性改成主域名就行,这样访问所有在这个主域名下的域名时都能够带上统一的 cookie。但是这样虚假的单点登录的问题还是有的,系统分散在了不同的主域名下就处理不来了。

真实的 单点登录 (SingleSign-On,SSO) [不同域名下]

其实这个的单点使用范围是很广的,留心一下就发现其实华工的系统(草)就是这么做的,统一认证登录就相当于一个 SSO。这个方案本质上就是为多系统统一单独做一个身份验证系统,在访问业务时需要身份验证就给重定向过去,验证好处理完了就附带上参数,再重定向回业务。

几个部分具体流程如下,此处用了 session 验证方式 :

SSO 未登录,App 未登录:

  1. 用户访问 app 系统,app 系统是需要登录的,但用户现在没有登录。
  2. 跳转到 SSO登录系统。SSO 系统也没有登录,弹出用户登录页让用户登录。
  3. 用户填写用户名、密码,SSO 系统进行认证后,将登录状态写入 SSO 的session,浏览器中写入SSO 域下的Cookie。
  4. SSO 系统登录完成后会随机生成一个ST(Service Ticket),然后跳转到app系统,同时将ST作为参数传递给 app 系统。
  5. app系统拿到ST后,从后台向 SSO 发送请求,验证ST是否有效。(这里还要验证一次)
  6. 验证通过后,app 系统将登录状态写入 session 并设置app域下的 Cookie。

至此,跨域单点登录就完成了。以后我们再访问 app系统时,app就是登录的。接下来,我们再看看访问 app2 系统时的流程。

  1. 用户访问 app2 系统,app2 系统没有登录,跳转到SSO。
  2. 由于SSO已经登录了,不需要重新登录认证。
  3. SSO 生成 ST,浏览器跳转到 app2 系统,并将ST作为参数传递给 app2。
  4. app2 拿到 ST,后台访问 SSO,验证 ST 是否有效。
  5. 验证成功后,app2 将登录状态写入 session,并在 app2 域下写入 Cookie。

这样,app2 系统不需要走登录流程,就已经是登录了。即使 SSO,app 和 app2 在不同的域,它们之间的 session 不共享也是没问题的。

为什么在 SSO 系统登录后,跳回原业务系统时,带了个参数 ST,业务系统还要拿 ST 再次访问SSO进行验证?看上去这个步骤有点多余。如果 SSO 登录认证通过后,通过重定向返回时将用户信息返回给原业务系统,原业务系统直接设置已登录状态,这样流程简单,也完成了登录,不是快速吗?

其实这样问题时很严重的,如果我在SSO没有登录,而是直接在浏览器中敲入验证后重定向的地址,并带上伪造的用户信息,是不是业务系统就会认为已经登录。