关于登录,Nest如何处理

248 阅读6分钟

前言

关于登录,是一个很常见的需求,并且在登录之后,我们需要保存用户的登录状态,防止用户重复登录。但是,http 协议是无状态的,所以我们需要在登录之后将用户的登录状态保存下来。

那么,我们可以使用什么方式来保存用户的登录状态呢?

这种问题,一般有两种解决方案:

  1. 服务端存储的 session 机制
  2. 客户端存储的 token 机制

服务端存储的 session 机制

服务端存储的 session 机制,是指在服务端存储用户的登录状态。在登录之后,服务端会生成一个 session id,然后将 session id 发送给客户端。

客户端的 session id 是保存在 cookie 中的,服务端的 session id 是保存在内存中的。客户端每次发送请求的时候,都会将 session id 发送给服务端。服务端会根据 session id 来判断用户是否登录。

Nest 中使用 session

Nest 里实现 session 实现还是用的 express 的中间件 express-session。因此我们需要先安装 express-session。

npm install express-session @types/express-session

然后,我们需要在 main.ts 文件中设置 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: "secret",
            resave: false,
            saveUninitialized: false,
            cookie: {
                maxAge: 600000,
            },
        })
    );
    await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

然后,我们就可以在 app.controller.ts 文件中使用 @Session 来获取 session。

import { Controller, Get, Session } from "@nestjs/common";
import { AppService } from "./app.service";

@Controller()
export class AppController {
    constructor(private readonly appService: AppService) {}

    @Get("getsession")
    getSession(@Session() session: Record<string, any>) {
        console.log(session);
        if (!session.views) {
            session.views = 0;
        }
        session.views++;
        return `You have viewed this page ${session.views} times`;
    }
}

通过 postman 测试:

image-7.png

session 可接收的参数

  • secret: 必填参数 用来给 session ID cookie 签名的。这可以是单个字符串,也可以是由多个字符串组成的数组。如果是多个字符串组成的数组,则只有第一个字符串用于签名,其他字符串用于验证。
  • name: 定义 session ID cookie 的名称。默认值是 connect.sid。
  • store: 用于存储 session ID 的位置。默认值是 new MemoryStore()。
  • proxy: 如果设置为 true,则通过代理(例如,使用了负载均衡器)时,强制将请求的协议更改为与代理相同的协议。默认为 false。
  • resave: 强制保存 session 即使它并没有变化。默认为 true。但是不推荐使用默认值。因为如果设置为 false,那么只有在 session 发生变化时才会被保存。如果设置为 true,那么每次请求都会被保存。为了防止不必要的写入,你应该使用 touch 方法来更新 session 的访问时间。
  • saveUninitialized: 强制将未初始化的 session 存储。当新建了一个 session 且未设定属性或值时,它就会处于未初始化状态。在这种情况下,未初始化的 session 也会被存储,但是在存储之前,会对其进行标记。默认为 true,但是推荐设置成 false。选择 false 的话,可以降低服务器存储未初始化 session 的压力,减少服务器的存储压力。同时,也有助于客户端在没有回话的情况下发出多个并行请求的竞争条件。
  • cookie: 包含 cookie 配置的对象。默认值为 { path: '/', httpOnly: true, secure: false, maxAge: null }。
  • cookie.domain: 定义 cookie 的域名。默认为 null,表示仅在当前域名下有效。
  • cookie.path: 定义 cookie 的路径。默认为 '/',表示在整个域名下都有效。
  • cookie.httpOnly: 定义 cookie 是否只可通过 HTTP 协议访问。默认为 true,表示只能通过 HTTP 协议访问。
  • cookie.secure: 定义 cookie 是否只可通过 HTTPS 协议访问。默认为 false,表示可以通过 HTTP 协议和 HTTPS 协议访问。
  • cookie.expires: 定义 cookie 的过期时间。默认为 null,表示浏览器关闭时过期。
  • cookie.maxAge: 定义 cookie 的最大有效期。默认为 null,表示浏览器关闭时过期。

    注意:如果同时设置了 expires 和 maxAge,那么将被用到的是在对象中最后一个设置的属性。建议使用 maxAge 属性,因为它更易于理解。

  • cookie.sameSite: 定义 cookie 的 SameSite 属性。默认为 'lax'。
  • cookie.signed: 定义 cookie 是否签名。默认为 true,表示签名。
  • rolling: 强制在每次请求时重置 cookie 的过期时间。默认为 false。

客户端存储的 token 机制

客户端存储的 token 机制,是指在客户端存储用户的登录状态。在登录之后,客户端会生成一个 token,然后将 token 发送给服务端。token 的方案常用 json 格式来保存,叫做 json web token,简称 JWT

JWT 是保存在 request header 中的一段字符串,它是由三部分组成:

  1. 头部(Header):包含了 token 的类型和签名算法等信息。
  2. 载荷(Payload):包含了 token 的具体内容,比如用户的 id、用户名、过期时间等。
  3. 签名(Signature):用于验证 token 的合法性,防止被篡改。 这三部分会分别做到 base64 编码,然后用点号连接起来,形成一个字符串。在前端发起请求的时候,会将这个字符串放在 request header 中。服务端会解析这个字符串,然后验证签名是否正确。如果签名正确,就说明这个 token 是合法的,可以使用。

Nest 中使用 JWT

Nest 中使用 JWT,需要先安装 @nestjs/jwt

npm install @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({
            secret: "secret",
            signOptions: { expiresIn: "1d" },
        }),
    ],
    controllers: [AppController],
    providers: [AppService],
})
export class AppModule {}

然后,我们就可以在 app.controller.ts 文件中注入 JwtService 来使用 JWT。

import { Controller, Get, Inject, Res } from "@nestjs/common";
import { AppService } from "./app.service";
import { JwtService } from "@nestjs/jwt";
import { Response } from "express";

@Controller()
export class AppController {
    constructor(private readonly appService: AppService) {}

    @Inject(JwtService)
    private readonly jwtService: JwtService;

    @Get("getjwt")
    getJwt(@Res({ passthrough: true }) response: Response) {
        const newToken = this.jwtService.sign({
            name: "test",
        });

        response.setHeader("Authorization", `Bearer ${newToken}`);
        return {
            token: newToken,
        };
    }
}

通过 postman 测试:

image-8.png

之后的 http 请求,都需要在 request header 中加上 Authorization 头,值为 Bearer ${token}

两种方案的区别

服务端存储的 session 机制的优点和缺点

  • 优点:

    • 服务器端会话管理:所有会话数据都存储在服务器端,客户端仅持有 Session ID。这使得服务器可以更好地控制会话生命周期和安全性。
    • 用户体验好:通过 Cookie 保存用户的会话信息(如 Session ID),用户无需每次访问都重新登录,提高了用户体验。
    • 支持复杂业务逻辑:由于 Session 信息存储在服务器端,因此可以支持更复杂的业务逻辑和状态管理。
  • 缺点:

    • 服务器负担大:随着用户数量的增加,服务器需要存储和管理大量的 Session 信息,这会增加服务器的负担。
    • 跨域问题:当应用需要跨域访问时,Session 机制会遇到一些问题,因为 Session ID 通常绑定到特定的域名和路径。
    • 扩展性差:在分布式系统中,Session 信息的共享和同步是一个挑战,需要额外的机制来实现。

客户端存储的 token 机制的优点和缺点

  • 优点:

    • 无状态:JWT 机制不需要在服务器端存储任何会话信息,因此是无状态的,这有助于减轻服务器的负担并提高系统的可扩展性。
    • 跨域支持:JWT 可以通过 Authorization 头传递,避免了跨域请求中的 Cookies 问题。
    • 安全性高:JWT 使用数字签名或加密机制来验证令牌的真实性,防止伪造和篡改。
  • 缺点:

    • 令牌大小问题:JWT 令牌的大小通常比 Session 令牌大,因为它包含了更多的信息。这可能会导致网络传输速度变慢,尤其是在带宽有限的情况下。
    • 令牌过期问题:JWT 的令牌过期时间是固定的,无法在令牌生成后更改。如果令牌过期,用户需要重新获取令牌,这可能会影响用户体验。
    • 不可撤销性:一旦 JWT 令牌生成,就无法撤销。如果需要撤销令牌,必须等待令牌过期或更改密钥,这可能会带来一些额外的复杂性。

如何抉择

在选择使用哪种机制时,需要考虑以下因素:

  • 会话需求:如果需要管理用户会话,可以选择使用服务端存储的 session 机制。这使得服务器可以更好地控制会话生命周期和安全性。
  • 安全性需求:如果需要更高的安全性,可以选择使用 JWT 机制。JWT 可以使用数字签名或加密机制来验证令牌的真实性,防止伪造和篡改。
  • 跨域支持:如果应用需要跨域访问,可以选择使用 JWT 机制。JWT 可以通过 Authorization 头传递,避免了跨域请求中的 Cookies 问题。
  • 扩展性需求:如果需要在分布式系统中实现会话共享和同步,可以选择使用服务端存储的 session 机制。这可以通过使用会话存储在服务器端的方式来实现。 在实际应用中,可以根据具体的需求和场景进行选择。

使用 JWT 实现用户认证

  1. 安装依赖包:
npm install --save @nestjs/passport passport @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt
  1. 使用 nest g resource auth 命令创建 auth 模块:
nest g resource auth
  1. auth.module.ts 中引入 JwtModulePassportModule
import { Module } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { AuthController } from "./auth.controller";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
@Module({
    imports: [
        JwtModule.register({
            secret: "secret",
            signOptions: { expiresIn: "1d" },
        }),
        PassportModule,
    ],
    providers: [AuthService],
    controllers: [AuthController],
})
export class AuthModule {}
  1. auth.controller.ts 中增加一个登录接口:
import { Controller, Post, Body } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { CreateAuthDto } from "./dto/create-auth.dto";

@Controller("auth")
export class AuthController {
    constructor(private readonly authService: AuthService) {}

    @Post("login")
    login(@Body() createAuthDto: CreateAuthDto) {
        return this.authService.login(createAuthDto);
    }
}
  1. auth.service.ts 中实现用户登录逻辑:
import { Injectable } from "@nestjs/common";
import { CreateAuthDto } from "./dto/create-auth.dto";
import { JwtService } from "@nestjs/jwt";

@Injectable()
export class AuthService {
    constructor(private jwtService: JwtService) {}

    login(createAuthDto: CreateAuthDto) {
        console.log(createAuthDto);
        return {
            accessToken: this.jwtService.sign({
                username: createAuthDto.username,
            }),
        };
    }
}
  1. 这个时候,通过 postman 测试登录接口,可以得到一个 JWT 令牌:

image-9.png

  1. 下面实现 JWT 认证。首先在 src/auth/strategy 目录中创建一个新的文件 jwt.strategy.ts
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { jwtConstants } from "./auth.module";

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, "jwt") {
    constructor() {
        super({
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
            secretOrKey: jwtConstants.secret,
        });
    }

    async validate(payload: { username: string }) {
        // 这里应该需要通过username去数据库查询用户信息,然后返回用户信息,这里只是简单的返回了一个对象
        return {
            username: payload.username,
        };
    }
}
  1. auth.module.ts 中添加一个新的 JwtStrategy 作为一个 provider:
import { Module } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { AuthController } from "./auth.controller";
import { PassportModule } from "@nestjs/passport";
import { JwtModule } from "@nestjs/jwt";
import { JwtStrategy } from "./jwt.strategy";

export const jwtConstants = {
    secret: "zjP9h6ZI5LoSKCReel34j",
};

@Module({
    imports: [
        PassportModule,
        JwtModule.register({
            secret: jwtConstants.secret,
            signOptions: { expiresIn: "12h" },
        }),
    ],
    controllers: [AuthController],
    providers: [AuthService, JwtStrategy],
})
export class AuthModule {}
  1. src/auth 目录下创建一个名为 jwt-auth.guard.ts:
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";

@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {}
  1. 在需要校验 token 的接口中使用 @UseGuards(JwtAuthGuard) 装饰器:
import { Controller, Get, UseGuards } from "@nestjs/common";
import { UsersService } from "./users.service";
import { JwtAuthGuard } from "src/auth/jwt-auth.guard";

@Controller("users")
export class UsersController {
    constructor(private readonly usersService: UsersService) {}

    @Get("list")
    @UseGuards(JwtAuthGuard)
    findAll() {
        return this.usersService.findAll();
    }
}

总结

这篇文章,我们介绍了如何使用 Nest 实现服务端存储的 session 和客户端存储的 token。以及分析了它们的区别。最后,我们还实现了一个简单的 JWT 认证。在实际项目中,我们可以根据自己的需求来选择合适的方案。