title: NestJS认证系统
description: 深入学习NestJS中的认证系统实现,包括Passport集成、JWT认证、会话认证和本地策略
keywords: NestJS, 认证系统, Passport, JWT, 会话认证, 本地策略, 身份验证

NestJS认证系统

学习目标

通过本章节的学习,你将能够:

  • 理解认证系统的基本概念和工作原理
  • 掌握Passport在NestJS中的集成使用
  • 实现JWT认证机制
  • 理解会话认证的实现方式
  • 配置和使用本地策略进行用户名密码认证
  • 构建完整的认证系统,包括注册、登录、令牌刷新等功能

核心知识点

认证系统基础

认证是验证用户身份的过程,确保用户是他们声称的那个人。在Web应用中,认证通常包括以下步骤:

  1. 用户提供凭据(如用户名和密码)
  2. 系统验证这些凭据
  3. 系统为已验证的用户颁发令牌或创建会话
  4. 用户使用令牌或会话访问受保护的资源

Passport.js

Passport.js是Node.js中最流行的认证中间件,它提供了一种灵活的方式来实现各种认证策略。NestJS通过@nestjs/passport模块提供了对Passport的集成支持。

JWT认证

JSON Web Token (JWT)是一种基于令牌的认证机制,它将用户信息编码到一个加密的令牌中,然后在后续请求中使用该令牌进行身份验证。JWT的主要优势包括:

  • 无状态:不需要在服务器端存储会话信息
  • 可扩展:可以在令牌中包含自定义声明
  • 跨域:适用于分布式系统和微服务架构

会话认证

会话认证是一种传统的认证机制,它在服务器端存储用户会话信息,并通过cookie在客户端和服务器之间传递会话ID。会话认证的主要优势包括:

  • 易于实现和理解
  • 适合单体应用
  • 可以轻松实现会话过期和撤销

本地策略

本地策略是Passport中最基本的认证策略,它使用用户名(或电子邮件)和密码进行认证。本地策略通常与数据库集成,用于验证用户凭据。

实用案例分析

案例:完整的认证系统

我们将构建一个完整的认证系统,包括用户注册、登录、令牌刷新和受保护的API访问。

1. 安装依赖

首先,我们需要安装必要的依赖:

npm install @nestjs/passport passport passport-local @nestjs/jwt passport-jwt bcrypt
npm install --save-dev @types/passport-local @types/passport-jwt @types/bcrypt

2. 配置JWT模块

AppModule中配置JWT模块:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthModule } from './auth/auth.module';
import { UserModule } from './user/user.module';

@Module({
  imports: [
    UserModule,
    AuthModule,
    PassportModule,
    JwtModule.register({
      secret: 'your-secret-key',
      signOptions: { expiresIn: '60s' },
    }),
  ],
})
export class AppModule {} 

3. 用户实体

定义用户实体:

// src/user/entities/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, Unique } from 'typeorm';

@Entity()
@Unique(['username', 'email'])
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  username: string;

  @Column()
  email: string;

  @Column()
  password: string;
}

4. 认证服务

创建认证服务,实现注册、登录和令牌刷新功能:

// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { User } from '../user/entities/user.entity';

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(User) 
    private usersRepository: Repository<User>,
    private jwtService: JwtService,
  ) {}

  // 注册新用户
  async register(username: string, email: string, password: string): Promise<User> {
    // 加密密码
    const hashedPassword = await bcrypt.hash(password, 10);

    // 创建用户
    const user = this.usersRepository.create({
      username,
      email,
      password: hashedPassword,
    });

    return this.usersRepository.save(user);
  }

  // 验证用户凭据
  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersRepository.findOne({ where: { username } });
    if (!user) {
      return null;
    }

    const isPasswordValid = await bcrypt.compare(pass, user.password);
    if (user && isPasswordValid) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  // 登录并生成令牌
  async login(user: any) {
    const payload = { username: user.username, sub: user.id };
    return {
      access_token: this.jwtService.sign(payload),
      refresh_token: this.jwtService.sign(payload, { expiresIn: '7d' }),
    };
  }

  // 刷新令牌
  async refreshToken(refreshToken: string) {
    try {
      const decoded = this.jwtService.verify(refreshToken);
      const user = await this.usersRepository.findOne({ where: { id: decoded.sub } });
      if (!user) {
        throw new UnauthorizedException();
      }

      const payload = { username: user.username, sub: user.id };
      return {
        access_token: this.jwtService.sign(payload),
      };
    } catch (error) {
      throw new UnauthorizedException();
    }
  }
}

5. 本地策略

创建本地策略,用于用户名密码认证:

// src/auth/strategies/local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from '../auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

6. JWT策略

创建JWT策略,用于验证访问令牌:

// src/auth/strategies/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: 'your-secret-key',
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

7. 认证控制器

创建认证控制器,处理注册、登录和令牌刷新请求:

// src/auth/auth.controller.ts
import { Controller, Post, Body, UseGuards, Request } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';

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

  @Post('register')
  async register(@Body() body: {
    username: string;
    email: string;
    password: string;
  }) {
    return this.authService.register(body.username, body.email, body.password);
  }

  @UseGuards(AuthGuard('local'))
  @Post('login')
  async login(@Request() req) {
    return this.authService.login(req.user);
  }

  @Post('refresh')
  async refreshToken(@Body('refresh_token') refreshToken: string) {
    return this.authService.refreshToken(refreshToken);
  }
}

8. 受保护的路由

创建一个示例控制器,演示如何保护路由:

// src/user/user.controller.ts
import { Controller, Get, UseGuards, Request } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('user')
export class UserController {
  @UseGuards(AuthGuard('jwt'))
  @Get('profile')
  getProfile(@Request() req) {
    return req.user;
  }

  @UseGuards(AuthGuard('jwt'))
  @Get('protected')
  getProtectedResource() {
    return { message: 'This is a protected resource' };
  }
}

9. 认证模块

创建认证模块,组织所有认证相关的组件:

// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { LocalStrategy } from './strategies/local.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';
import { AuthController } from './auth.controller';
import { User } from '../user/entities/user.entity';

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    PassportModule,
    JwtModule.register({
      secret: 'your-secret-key',
      signOptions: { expiresIn: '60s' },
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}

代码优化建议

  1. 使用环境变量:将JWT密钥等敏感信息存储在环境变量中,而不是硬编码在代码中

  2. 密码策略

    • 使用更强的密码哈希算法
    • 实施密码强度验证
    • 考虑使用密码重置机制
  3. 令牌管理

    • 实现令牌黑名单,用于撤销令牌
    • 考虑使用短期访问令牌和长期刷新令牌的组合
    • 为令牌添加更多安全声明(如颁发时间、过期时间等)
  4. 错误处理

    • 实现统一的错误响应格式
    • 避免在错误消息中泄露敏感信息
    • 记录认证失败的尝试
  5. 安全增强

    • 实施速率限制,防止暴力破解
    • 考虑使用HTTPS保护传输中的令牌
    • 实现多因素认证

常见问题与解决方案

1. 令牌过期问题

问题:JWT令牌过期后需要重新登录

解决方案

  • 实现令牌刷新机制
  • 设置合理的令牌过期时间
  • 考虑使用滑动过期策略

2. 密码安全问题

问题:密码存储不安全

解决方案

  • 使用bcrypt等强哈希算法
  • 实施密码强度验证
  • 定期提醒用户更改密码

3. 认证失败处理

问题:认证失败时的错误处理不当

解决方案

  • 实现统一的错误处理中间件
  • 提供清晰的错误消息
  • 避免在错误消息中泄露敏感信息

4. 令牌撤销问题

问题:无法撤销已颁发的令牌

解决方案

  • 实现令牌黑名单
  • 使用短期令牌
  • 考虑使用Redis存储黑名单

5. 跨域认证问题

问题:在跨域环境中认证失败

解决方案

  • 正确配置CORS
  • 确保令牌在跨域请求中正确传递
  • 考虑使用cookie存储令牌(需设置正确的cookie属性)

小结

本章节我们学习了NestJS中的认证系统实现,包括:

  • 认证系统的基本概念和工作原理
  • Passport在NestJS中的集成使用
  • JWT认证机制的实现
  • 会话认证的工作原理
  • 本地策略的配置和使用
  • 完整认证系统的构建,包括注册、登录、令牌刷新等功能

通过这些知识,你可以构建安全、可靠的认证系统,保护你的应用免受未授权访问。

互动问答

  1. 问题:JWT和会话认证的主要区别是什么?
    答案:JWT是无状态的,不需要在服务器端存储会话信息,而会话认证需要在服务器端存储会话状态。JWT适合分布式系统,会话认证适合单体应用。

  2. 问题:如何在NestJS中使用Passport实现认证?
    答案:通过以下步骤:1. 安装必要的依赖;2. 配置Passport模块;3. 创建认证策略;4. 使用@UseGuards(AuthGuard('strategy-name'))装饰器保护路由。

  3. 问题:什么是密码哈希,为什么它很重要?
    答案:密码哈希是将密码转换为不可逆的字符串的过程。它很重要,因为即使数据库被泄露,攻击者也无法获取用户的原始密码。

  4. 问题:如何实现令牌刷新机制?
    答案:通过以下步骤:1. 生成访问令牌和刷新令牌;2. 当访问令牌过期时,使用刷新令牌获取新的访问令牌;3. 验证刷新令牌的有效性。

  5. 问题:如何增强认证系统的安全性?
    答案:可以通过以下方式:1. 使用强密码哈希算法;2. 实施多因素认证;3. 设置合理的令牌过期时间;4. 实现令牌黑名单;5. 实施速率限制防止暴力破解。

实践作业

  1. 作业1:扩展我们的认证系统,添加密码重置功能

  2. 作业2:实现多因素认证,使用电子邮件或短信验证码

  3. 作业3:将JWT密钥和其他敏感配置移至环境变量

  4. 作业4:实现令牌黑名单,用于撤销已颁发的令牌

  5. 作业5:创建一个完整的前端应用,与我们的认证系统集成

通过完成这些作业,你将能够更加深入地理解认证系统的实现细节,为构建安全的应用打下坚实的基础。

« 上一篇 14-database-relations 下一篇 » 16-authorization