Skip to content

当然!我们来攻克任何严肃应用都必须跨过的第一道大门:认证 (Authentication)。这就像是为你的应用配备一位严谨可靠的“门禁保安”,他的唯一职责就是回答一个问题:“你是谁?请证明你的身份。

为你的应用配备“门禁保安”:NestJS 认证 (Authentication) 完全指南

想象你的应用是一个高度机密的科研基地。

不是任何人都能随意进出。在基地大门口,站着一位尽职尽责的保安。每当有人试图进入时,保安都会执行一套严格的程序:

  1. 出示凭证:访客需要出示他的“身份证”或“通行卡”(在 Web 世界里,这通常是用户名/密码,或者一个令牌 Token)。
  2. 验证凭证:保安会拿出验证设备,检查这张卡的真伪(比如,验证密码是否正确,或令牌是否由基地内部签发且未过期)。
  3. 放行或拒绝
    • 如果验证通过,保安会点点头,并在访客身上贴一个“已认证”的标签(将用户信息附加到请求对象上),然后放行。
    • 如果验证失败,保安会立刻将其拦下(返回一个 401 Unauthorized 错误)。

这个验证身份的过程,就是认证 (Authentication)

它与授权 (Authorization) 是两个不同的概念:

  • 认证 (Authentication):回答“你是谁?”。比如,确认你是“张三研究员”。
  • 授权 (Authorization):回答“你能做什么?”。比如,确认“张三研究员”有权限进入 A 级实验室,但没有权限进入 S 级核心区。授权通常在认证之后进行。

NestJS 作为一个模块化的框架,本身不强制任何特定的认证策略。但它与著名的 Node.js 认证库 Passport 进行了深度、优雅的集成。@nestjs/passport 模块让我们可以用一种非常“NestJS-Style”的方式来实现各种复杂的认证流程。

1. 认证的核心思想:Passport 的“策略”模式

Passport 的设计非常巧妙,它把每一种认证方式(如用户名密码、JWT、OAuth 等)都看作是一种独立的策略 (Strategy)

  • 你想用用户名/密码登录?那就使用 passport-local 策略。
  • 你想用 JWT (JSON Web Token) 进行无状态认证?那就使用 passport-jwt 策略。
  • 你想支持微信或 GitHub 登录?那就使用 passport-weixinpassport-github 策略。

你需要做的,就是选择并配置你需要的策略,然后告诉 Passport 在何时使用它们。

2. 实战:搭建经典的 JWT 认证流程

JWT (JSON Web Token) 是现代 Web API 中最流行的无状态认证方案。

流程简介:

  1. 用户使用用户名和密码进行登录
  2. 服务器验证通过后,不使用 Session,而是生成一个加密签名的 JWT(一个长字符串),并将其返回给客户端。
  3. 客户端(如前端应用)将这个 JWT 存储起来(通常在 localStorageAuthorization Header 中)。
  4. 之后,客户端在每一次请求需要认证的接口时,都必须在 Authorization 请求头中带上这个 JWT,格式为 Bearer <token>
  5. 服务器端的“门禁保安”(JWT 策略守卫)会拦截请求,提取并验证这个 JWT 的签名和有效期。验证通过则放行。

第一步:安装“保安系统”和“令牌工具”

bash
# NestJS 的 Passport 和 JWT 模块
npm install @nestjs/passport @nestjs/jwt passport passport-jwt

# 类型定义文件
npm install -D @types/passport-jwt

第二步:配置“令牌签发中心” (AuthModuleJwtModule)

src/auth/auth.module.ts

typescript
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module'; // 假设我们有一个 UsersModule
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy'; // 稍后创建
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    // 注册 JwtModule,并进行配置
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        secret: configService.get<string>('JWT_SECRET'), // 签名的密钥,必须保密!
        signOptions: { expiresIn: '60m' }, // 令牌有效期 60 分钟
      }),
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy], // 注册 AuthService 和我们的 JWT 策略
})
export class AuthModule {}
  • JwtModule.registerAsync: 我们从 ConfigService 动态获取 JWT_SECRET,这是安全最佳实践。这个 secret 是用来给 JWT 签名的,如果泄露,任何人都可以伪造你的令牌。

第三步:创建“登录服务”和“令牌签发逻辑” (AuthService)

AuthService 负责核心的业务逻辑:验证用户身份,并签发 JWT。

src/auth/auth.service.ts

typescript
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(
    private readonly usersService: UsersService,
    private readonly jwtService: JwtService // 注入 JwtService
  ) {}

  // 1. 验证用户密码 (实际项目中密码应被哈希存储和比较)
  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      // 简化比较,实际应使用 bcrypt.compare
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  // 2. 登录成功后,生成 JWT
  async login(user: any) {
    // payload 是你想放在 JWT 中的信息
    const payload = { username: user.username, sub: user.userId };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

第四步:实现 JWT 策略 (JwtStrategy) - “保安的验证手册”

JwtStrategy 告诉 Passport 如何从请求中提取 JWT,以及如何验证它。

src/auth/jwt.strategy.ts

typescript
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '../users/users.service';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly configService: ConfigService,
private readonly usersService: UsersService,
) {
super({
// 1. 指定从何处提取 JWT
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
// 2. 忽略过期检查 (false 表示不忽略,由 Passport 自动处理)
ignoreExpiration: false,
// 3. 提供用于验证签名的密钥
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}

// 4. Passport 在验证 JWT 签名和有效期后,会调用这个 validate 方法
// payload 是从 JWT 中解码出的 JSON 对象
async validate(payload: { sub: number; username: string }) {
// 在这里,你可以根据 payload 中的信息(如 userId)去数据库查找完整的用户信息
const user = await this.usersService.findById(payload.sub);
if (!user) {
throw new UnauthorizedException();
}
// 这个方法返回的对象,将被附加到 request.user 上
return user;
}
}

第五步:部署“门禁保安” (AuthGuard)

现在,我们有了所有的部件,只需要在需要保护的路由上,部署一个“门禁保安”——AuthGuard

src/users/users.controller.ts

typescript
import { Controller, Get, UseGuards, Request } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('users')
export class UsersController {

  // 使用 @UseGuards() 并传入 AuthGuard('jwt')
  // 这会激活我们编写的 JwtStrategy
  @UseGuards(AuthGuard('jwt'))
  @Get('profile')
  getProfile(@Request() req) {
    // 因为 JwtStrategy 的 validate 方法返回了 user 对象
    // 所以现在我们可以通过 req.user 来访问它
    return req.user;
  }
}

现在,任何未提供有效 JWT 的请求,在访问 /users/profile 时,都会被 AuthGuard 自动拦截,并返回 401 Unauthorized 错误。

4. 中国企业级方案的思考

虽然标准的 JWT 流程非常通用,但在中国的复杂业务场景下,企业往往会构建更精细、更安全的认证体系。

痛点与演进:

  1. JWT 的“无状态”困境: JWT 一旦签发,在它过期之前,服务器是无法使其失效的。如果用户修改了密码,或者管理员想强制某个用户下线,标准的 JWT 方案做不到。
  2. 多设备登录管理: 如何管理一个用户在 App, Web, 小程序等多个设备上的登录状态?如何允许用户“踢掉”其他设备?
  3. SSO (单点登录): 在企业内部,通常需要一个统一的认证中心(如基于 OAuth2/OIDC 的身份提供商),用户只需登录一次,就可以访问所有相互信任的应用。

企业级方案 Demo:JWT + Refresh Token + Redis

这是一个非常流行的、兼顾了无状态和可控性的增强方案。

  • 登录时: 服务器签发两个 Token:
    • Access Token: 和我们上面实现的一样,生命周期很(如 15 分钟)。它用于访问受保护的资源。
    • Refresh Token: 一个随机生成的、无意义的长字符串,生命周期很(如 7 天或 30 天)。它只用于获取新的 Access Token。
  • 存储:
    • 客户端存储 Access Token 和 Refresh Token。
    • 服务器在 Redis 中,以 userId 为键,存储一份 Refresh Token 的白名单(或黑名单)。
  • 请求时:
    • 客户端使用 Access Token 访问 API。
  • Access Token 过期后:
    • 客户端发现 Access Token 过期(收到 401 错误),就会调用一个专门的 /auth/refresh 接口,并带上那个长周期的 Refresh Token
    • 服务器收到 Refresh Token 后,去 Redis 中校验它是否存在且有效。
    • 校验通过后,服务器签发一个新的 Access Token新的 Refresh Token,返回给客户端。客户端用新的替换掉旧的。
  • 强制下线/修改密码:
    • 只需要从 Redis 中将该用户的 Refresh Token 删除即可。这样,即使用户的旧 Refresh Token 还在有效期内,他也无法再用它来换取新的 Access Token,从而被强制下线。

伪代码

AuthService.refreshToken():

typescript
async refreshToken(userId: number, oldRefreshToken: string) {
  // 1. 从 Redis 中获取该用户的有效 refresh token
  const validToken = await this.redisClient.get(`refresh_token:${userId}`);

  // 2. 校验传入的 token 是否匹配
  if (!validToken || validToken !== oldRefreshToken) {
    throw new UnauthorizedException('Invalid refresh token');
  }

  // 3. 签发新的 Access Token 和 Refresh Token
  const newAccessToken = this.jwtService.sign({ sub: userId });
  const newRefreshToken = generateRandomString(); // 生成新的 Refresh Token

  // 4. 将新的 Refresh Token 更新到 Redis 中
  await this.redisClient.set(`refresh_token:${userId}`, newRefreshToken, { EX: 7 * 24 * 3600 }); // 7天有效期

  return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}

这种模式在保持了 JWT 高性能、跨域友好的优点的同时,通过引入 Redis 作为状态存储,巧妙地解决了 JWT 无法主动失效的问题,是目前构建可控、安全、用户体验良好的认证系统的最佳实践之一。

总结

认证是应用的“门禁系统”,是安全的第一道关卡。

  • 核心工具: NestJS 通过 @nestjs/passportPassport.js 强强联合,使用策略 (Strategy) 模式来应对各种认证需求。
  • 主流方案: JWT (JSON Web Token) 是现代无状态 API 的首选认证方案。
  • 实现步骤: 配置 JwtModule -> 实现 JwtStrategy -> 在需要保护的路由上使用 AuthGuard('jwt')
  • 企业级演进: 面对 JWT 无法失效的问题,采用 JWT + Refresh Token + Redis 的组合方案,是兼顾性能、安全和用户体验的行业标准。

掌握了认证,你就掌握了控制谁可以进入你的应用,以及他们进来后是谁的关键权力。