【NestJS】登录状态的保存方案

147 阅读4分钟

方案介绍

有两种方案:

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

服务端存储的 session + cookie

session + cookie 的给 http 添加状态的方案是服务端保存 session 数据,然后把 id 放入 cookie 返回,cookie 是自动携带的,每个请求可以通过 cookie 里的 id 查找到对应的 session,从而实现请求的标识。

这种方案有 CSRF、分布式 session、跨域等问题,不过都是有解决方案的。

缺点:

  • CSRF(跨站请求伪造)
    • 一般会用随机值来解决,每次随机生成一个值返回,后面再发起的请求需要包含这个值才行,否则就认为是非法的。这个随机值叫做 token,可以放在参数中,也可以放在 header 中,因为钓鱼网站拿不到这个随机值,就算带了 cookie 也没发通过服务端的验证。
  • 分布式 session:登录之后 session 是保存在某一台服务器的,之后可能会访问到别的服务器,这时候那台服务器是没有对应的 session 的,就没法完成对应的功能。
    • 解决方法是,把 session 保存在 redis,这样每台服务器都去那里查,只要一台服务器登录了,其他的服务器也就能查到 session,
  • 跨域
    • 解决方法是,要求后端代码设置了对应的 header:Access-Control-Allow-Origin: "当前域名"; Access-Control-Allow-Credentials: true

客户端存储的 token

JWT 的方案是把状态数据保存在 header 里,每次请求需要以authorization: Bearer xxxxx.xxxxx.xxxx形式手动携带。服务端就可以解析出对应的 header、payload、verify signature 这三部分,然后根据 header 里的算法也对 header、payload 加上 salt 做一次加密,如果得出的结果和 verify signature 一样,就接受这个 token。

没有 session + cookie 方案的 CSRF、分布式、跨域的问题,但是也有安全性、性能、没法控制等问题。

缺点:

  • 安全性:JWT 把数据直接 Base64 之后就放在了 header 里,那别人就可以轻易从中拿到状态数据,比如用户名等敏感信息,也能根据这个 JWT 去伪造请求。
    • 解决方法是,要搭配 https 来用
  • 性能:JWT 把状态数据都保存在了 header 里,每次请求都会带上,比起只保存个 id 的 cookie 来说,请求的内容变多了,性能也会差一些。
    • 解决方法是,JWT 里也不要保存太多数据。
  • 没法让 JWT 失效:因为是保存在客户端,那我们是没法手动让他失效的。
    • 可以配合 redis 来解决,记录下每个 token 对应的生效状态,每次先去 redis 查下 jwt 是否是可用的,这样就可以让 jwt 失效。

Nest里两种方案的使用

session + cookie

npm install express-session @types/express-session

在入口模块里启用:

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: 'guang',
    resave: false,
    saveUninitialized: false
  }));
  await app.listen(3000);
}
bootstrap();
  • 使用 express-session 中间件,指定加密的密钥 secret。
  • resave 为 true 是每次访问都会更新 session,不管有没有修改 session 的内容,而 false 是只有 session 内容变了才会去更新 session。
  • saveUninitalized 设置为 true 是不管是否设置 session,都会初始化一个空的 session 对象。比如你没有登录的时候,也会初始化一个 session 对象,这个设置为 false 就好。

在 controller 里就可以注入 session 对象:

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

jwt

npm install @nestjs/jwt

在 AppModule 里引入 JwtModule:

方法一:

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

@Module({
  imports: [
    JwtModule.register({
      secret: 'hazymoon',
      signOptions: {
        expiresIn: '7d'
      }
    })
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

方法二:

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

@Module({
  imports: [
    JwtModule.registerAsync({
      async useFactory() {
        await 1
        return {
          secret: 'hazymoon',
          signOptions: {
            expiresIn: '7d'
          }
        }
      }
    })
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

在 controller 里注入 JwtModule 里的 JwtService:

@Inject(JwtService)
private jwtService: JwtService;

@Get('login')
ttt(@Res({ passthrough: true}) response: Response) {
    const newToken = this.jwtService.sign({
      count: 1
    });

    response.setHeader('token', newToken);
    return 'hello';
}
  • 这里使用 jwtService.sign 来生成一个 jwt token,放到 response header 里。

之后的请求需要带上这个 token,在服务端取出来

@Get('getToken')
ttt(@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 {
      const newToken = this.jwtService.sign({
        count: 1
      });

      response.setHeader('token', newToken);
      return 1;
    }
}