全栈搭建个人博客(3)--登录状态保持

1,602 阅读7分钟

作者 | 周周酱

本文写于2021年6月11日。首发于周周酱个人博客,转载请注明出处。

在上一篇文章里,描述了搭建Nest博客后台系统的基础步骤,在搭建好基础项目以及划分功能模块之后,接下来还要一步步完善,首先完成的功能是用户登录鉴权这一部分。在日常工作中,每一个系统都有用户登录状态维护这一块内容的实现,但总有些地方有点模糊,这一次通过自己完成前后端的实现,加深这方面的理解。

首先需要了解一些基础知识。由于HTTP 是无状态的协议,每个请求都是完全独立的,服务端无法确认当前访问者的身份,服务器与浏览器为了进行会话跟踪,就必须主动地去维护一个状态。

了解Cookie、Session、JWT

Cookie

是服务器在本地机器上存储的一小段文本信息,第一次登录后,由服务端通过响应报文向客户端浏览器发送一个叫做Set-Cookie的首部字段信息,客户端会把Cookie保存在本地。当 浏览器再次请求时,浏览器会自动将Cookie一同提交给服务器,服务器便可以通过Cookie识别用户身份。

仅适用cookie来保存用户登录信息,会有一些弊端

  • 不安全,Cookie可能被截取篡改 ,因此不能存放敏感数据
  • Cookie只能保存少量的数据,一般不超过4KB,Cookie 的数量也有限制,无法保存大量信息。
  • Cookie可以被禁用
  • 不允许跨域携带Cookie

Session

被称为“会话控制”,代表服务器与浏览器的一次会话过程,Session是一种服务器端的机制,Session 对象存储用户会话所需的属性,比如登录信息,存储在服务器端的内存、缓存比如Memcache、或者数据库里,并给每个客户端分配唯一的SessionId作为身份标识,客户端每次向服务器发请求的时候,都带上SessionId,服务器就知道这个请求来自于谁了。至于客户端怎么保存这个身份标识,可以有很多种方式,最常见的是采用 Cookie 的方式,类似下图。 session cookie (1).png 使用Session结合Cookie的方式,实现了对客户端的请求的状态维持,但是由于Session存储在服务器上,如果 Web 服务器使用了负载均衡,那么下一个操作请求到了另一台服务器的时候Session会丢失,导致状态维持失效。一种办法是Session 数据持久化,写入数据库或别的持久层。还有办法是如果能让服务器不保持相关状态(即服务器无状态),就不会出现这个问题。

JWT(JSON Web Token)

Token

在了解JWT前,需要了解Token,Token是服务端生成的一串字符串,通常作为访问资源接口(API)时所需要的资源凭证。通过一次登录验证,服务器得到一个鉴权字符串Token,后续在浏览器和服务器交互的过程中,在请求头中带上Token传到服务端,服务端再获取Token进行校验,完成身份验证。

JWT

JWT是一种规范化的 Token,JWT包含三个部分: Header头部,Payload荷载和Signature签名。 由三部分生成JWT,三部分之间用“.”号做分割。

  • Header

描述关于该 JWT 的最基本信息,例如其类型以及签名所使用的算法 微信图片_20210610162429.png

  • Payload

用来存放实际需要传递的数据,有以下官方字段,但不是强制的

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

微信图片_20210610162433.png

  • Signature

是对前两部分的签名,防止数据篡改 需要指定一个密钥,这个密钥放在服务端,不能泄露给用户。然后,使用 Header 里面指定的签名算法,按照公式产生签名 微信图片_20210610162436.png

根据案例中的配置信息,最终产生的JWT如下 微信图片_20210610162439.png

JWT的组成和生成方式,已经了解差不多,那么JWT的使用方式,可以看下面的图,非常直观地展示了一般使用JWT作为登录状态保持的实现方式。 JWT (1).png

注意

JWT 默认是不加密的,任何人拿到JWT字符串,都可以通过解码获取其中的内容,造成信息泄露,比如,我拿到前面生成的JWT中间Payload部分(也就是我们存用户信息的部分),通过base64解码,就可以直接拿到用户信息,包括id,name,email等。 微信图片_20210610164845.png

  • 所以未对JWT进行加密的情况下,不应该在JWT中存放一些敏感信息(非常重要),所以可以在获取Token后再次进行加密。

  • 保护好签名密钥,不能对外泄露,因为一般签名算法类型使用的是公开的几种,非常容易猜中你使用的是哪一种,如果知道了签名密钥,就可以对JWT进行伪造和篡改,造成安全问题。

JWT相对于Cookie+Session的方式,有一些优点:

  1. 不依赖Cookie, JWT 只要客户端能够进行存储就能够使用,所以没有因为Cookie被禁用导致的问题。
  2. JWT的鉴权机制是无状态的,不需要在服务端去保留用户的认证信息或者会话信息,在负载均衡下,不需要考虑到另一台服务器时Session丢失的情况。
  3. 跨域认证,因为并不依赖 Cookie ,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)

代码实现

前面说了很多基础的知识,对Cookie,Session,JWT等登录验证的方式都做了了解。下面说下具体到我的博客后台系统中是怎么实现的,这里采用的是JWT的方式。

配置签名密钥

上一篇说到项目的配置项是放在.env中,通过@nestjs/config来读取的,在.env中增加一个配置项用于存放生成JWT令牌的密钥,这个密钥需要妥善保存。

微信图片_20210610223533.png

登录接口

在用户模块的控制器中添加登录接口,在完成用户名密码的校验之后,调用自定义方法生成JWT并返回到前端。

微信图片_20210610223901.png

创建JWT

注入ConfigService并读取Secret,借助jsonwebtoken这个三方库生成JWT,在JWT内容中保存了用户的id,并设置过期时间。

微信图片_20210610224224.png

登录完成后浏览器拿到服务端给的JWT字符串,存在localstorage中,后面每次请求接口,都会在请求头中带上,一般放在authorization字段。

微信图片_20210610225216.png

AuthMiddleware

接下来写一个通用中间件,在需要授权的路由处理前做用户信息认证,也就是在这里拿到前端请求头上携带的JWT,再获取Secret做认证,拿到JWT中的用户信息,验证通过则将用户信息放入当前请求对象中,便于在后面的上下文中使用。这样就完成了JWT登录状态保持的实现。

微信图片_20210610224931.png

其他

方便后面获取用户id在业务代码中使用,可以使用nest中的自定义decorator,使代码十分简洁。

微信图片_20210610230409.png 微信图片_20210610230418.png

小结

到此为止,已经创建好了Nest项目的基本结构,实现了用户登录认证,后面就是具体的业务逻辑实现了。

完整代码请访问 zzj-admin-api