Nest探索(十三)Nest 中实现 Session 和 JWT

611 阅读5分钟

💥 在掘金写技术好文,瓜分万元现金大奖 | 5月金石计划

前言

在日常开发中,我们知道 http 是无状态的,也就是前后两次请求之间是独立的。那么我们怎么实现登录状态的保存和登录校验呢?

常用的登录校验方式有两种:session + cookie 和 jwt:

  • 服务端存储的 session + cookie 方案
  • 客户端存储的 jwt token 方案

这里我们使用 Nest 来实现下这两种方法。

session + cookie

首先,新建一个nest项目,然后安装依赖 express-session ,即 express 的中间件。

nest new nest-jwt-and-session-demo-240609 -p npm

pnpm i express-session @types/express-session

接着,在 main.ts 模块中引入它:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as session from 'express-session';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(session({
    secret: 'hhh', // used to sign the session cookie.
    resave: false, // Forces the session to be saved back to the session store, even if the session was never modified during the request.
    saveUninitialized: false, // Forces a session that is "uninitialized" to be saved to the store. A session is uninitialized when it is new but not modified.
  }));
  await app.listen(3000);
}
bootstrap();

根据 express-session 文档可知:

  • secret 是指定的服务端用于签名 session cookie 。secret 可以是字符串或者多个secret 组成的数组,当是数组时,只有第一个元素会用于签名 session ID cookie,不过所有元素都会用于请求的校验签名。

  • resave 默认值是 true。设置为 true 表示每次访问都会更新 session并存入 session store,不管请求中有没有修改 session 的内容。

  • saveUninitalized 默认值是 true。设置为 true 表示不管 session 是否初始化了,都会存入 session store 中。session 的初始化意思是生成后未修改过。这里我们设置为 false 就好。

接着,我们试着在 controller 里面注入 session 对象:

@Get('ses')
ses(@Session() session) {
  console.log(session)
  session.count = session.count ? session.count + 1 : 1;
  return session.count;
}

然后 pnpm start:dev 运行项目,并访问 http://localhost:3000/ses

可以看到,第一次访问时,页面内容是1,第二次访问是2了。而两次的打印内容分别是:

Session {
  cookie: { path: '/', _expires: null, originalMaxAge: null, httpOnly: true }
}
Session {
  cookie: { path: '/', _expires: null, originalMaxAge: null, httpOnly: true },
  count: 1
}

说明 cookie 成功应用了,服务器可以识别到是已有的用户,也就是说 http 请求已经有了状态。

我们知道,浏览器在收到服务端返回的Set-Cookie后,会保存下来,在下次再访问该网站时,HTTP请求头就会携带Cookie。保存的位置是 Application - Cookie。再看两次请求的请求头和响应头,

第一次请求的请求头不带 Cookie,响应头中带了 Set-Cookie,Set-Cookie 的值为:

Set-Cookie: connect.sid=s%3AmYgJCiQ4vXPvE9yxN_ry5U_PU7UVpFOd.MaGvC4DGwJrySoQjl23RSqlCtkP%2Fv23KS9QpAntCtgE; Path=/; HttpOnly

而第二次请求的请求头带了 Cookie,响应头中不带 Set-Cookie,Cookie的值为:

Cookie: connect.sid=s%3AmYgJCiQ4vXPvE9yxN_ry5U_PU7UVpFOd.MaGvC4DGwJrySoQjl23RSqlCtkP%2Fv23KS9QpAntCtgE

这与 Set-Cookie 中的数值是对应的。

jwt token

首先,安装依赖项 @nestjs/jwt:

pnpm i @nestjs/jwt

然后,在 AppModule 里引入 JwtModule:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    // JwtModule 是一个动态模块,通过 register 传入 options。
    JwtModule.register({
      secret: 'hhh',
      signOptions: {
        expiresIn: '3d'
      }
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

这里设置了 secret 为 hhh,以及过期时间为 3天。

然后,在 controller 中注入 JwtService,并定义路由 jwt:

import { JwtService } from '@nestjs/jwt';

@Inject(JwtService)
private jwtService: JwtService;

@Get('jwt')
jwt(@Res({ passthrough: true}) response: Response) {
  const newToken = this.jwtService.sign({
    count: 1
  });
  response.setHeader('token', newToken);
  return 'hello';
}

这里,我们使用了 jwtService.sign()生成了一个 jwt token,内容是 count: 1,并把 token 放到 response header 里,返回给浏览器。

此外,根据官方文档可知,当我们注入 response 对象之后,需要在 @Res() 装饰器中设置 passthrough 为 true,才可以正常返回内容。

测试一下:

当浏览器访问 http://localhost:3000/jwt 时,可以看到响应头中带上了 token 字段:

token.png

接下来的请求,我们带上这个 token,在服务端接收后,count +1 之后再放入新的 token 返回给浏览器:

@Get('jwt')
jwt(@Headers('authorization') authorization: string, @Res({ passthrough: true }) response: Response) {
  if(authorization) {
    try {
      const token = authorization.split(' ')[1];
      const data = this.jwtService.verify(token);
      const newToken = this.jwtService.sign({
        count: data.count + 1
      });
      response.setHeader('token', newToken);
      return data.count + 1
    } catch(e) {
      console.log(e);
      throw new UnauthorizedException();
    }
  } else {
    // 生成一个 jwt token,放到 response header 里
    const newToken = this.jwtService.sign({
      count: 1
    });
    response.setHeader('token', newToken);
    return 1;
  }
}

在日常开发中,我们知道使用 token 是通过在请求头中加上 autorization 字段,内容为 Bearer xxx。因此,这里我们首先通过 @Headers 装饰器取出 autorization,然后通过 jwtService.verify() 进行验证。如果验证成功,就重新生成 jwt 并返回给浏览器;如果验证失败,则抛出 UnauthorizedException 异常,由 Nest 内置的 Exception Filter 来进行处理。

我们测试下代码:

第一次请求时,请求头不带 token,服务器会返回 token,返回值是 1;

第二次请求时,将服务端的 token 带上,返回值是 2;

authorization1.png

如果是错误的 token 呢?服务器的jwtService.verify()校验不通过,提示 JsonWebTokenError: jwt malformed,服务端最终会返回401状态码给浏览器:

authorization2.png

这样,我们就实现了 jwt 保存 http 状态的需求。

后记

总的来说,在日常开发中,我们实现登录状态的保存和登录校验的方式有两种:session + cookie 和 jwt:

  • 服务端存储的 session + cookie 方案
  • 客户端存储的 jwt token 方案

尤其是 jwt token 方案,在接触过的几个开发项目中都是使用了这种方案。

在 Nest 后端应用中,学会这两种登录校验方式是很重要的一环。

参考