You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在NestJS 10中构建验证邮箱已确认的Guard

实现邮箱验证Guard的步骤

1. 更新JWT载荷,加入邮箱验证状态

首先需要在生成JWT令牌时,将用户的isEmailConfirmed属性加入到JWT载荷中。这样Guard可以直接从令牌中获取该状态(优点是性能好,缺点是用户验证邮箱后,旧令牌依然有效直到过期)。

修改getTokens方法

先通过用户ID获取用户的邮箱验证状态,再将其加入JWT payload:

// 注入UserService用于查询用户信息
constructor(
  private readonly jwtService: JwtService,
  private readonly configService: ConfigService,
  private readonly userService: UserService,
) {}

async getTokens(userId: string, username: string) {
  // 获取用户的邮箱验证状态
  const user = await this.userService.findOneById(userId);
  const isEmailConfirmed = user.isEmailConfirmed;

  const [accessToken, refreshToken] = await Promise.all([
    this.jwtService.signAsync(
      {
        sub: userId,
        username,
        isEmailConfirmed, // 新增邮箱验证状态字段
      },
      {
        secret: this.configService.get<string>('JWT_ACCESS_SECRET'),
        expiresIn: ACCESS_TOKEN_DURATION,
      },
    ),
    this.jwtService.signAsync(
      {
        sub: userId,
        username,
        isEmailConfirmed, // 刷新令牌同步加入该字段
      },
      {
        secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
        expiresIn: REFRESH_TOKEN_DURATION,
      },
    ),
  ]);

  return {
    accessToken,
    refreshToken,
  };
}

更新AccessTokenStrategy的载荷类型

修改JwtPayload类型,新增isEmailConfirmed字段:

type JwtPayload = {
  sub: string;
  username: string;
  isEmailConfirmed: boolean; // 新增字段
};

@Injectable()
export class AccessTokenStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_ACCESS_SECRET,
    });
  }

  validate(payload: JwtPayload) {
    return payload; // 验证后的载荷会注入到请求对象的user属性中
  }
}

2. 创建邮箱验证Guard

自定义Guard继承AuthGuard('jwt'),先确保用户已登录,再检查邮箱验证状态:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class EmailConfirmedGuard extends AuthGuard('jwt') {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    // 先调用父类方法确保用户已登录
    const isAuthenticated = await super.canActivate(context);
    if (!isAuthenticated) {
      return false;
    }

    // 获取请求中的用户信息
    const request = context.switchToHttp().getRequest();
    const user = request.user;

    // 校验邮箱是否已验证
    return user.isEmailConfirmed === true;
  }
}

3. 使用Guard保护接口

可以在单个路由或整个控制器上使用该Guard:

单个路由使用

import { Controller, Get, UseGuards } from '@nestjs/common';
import { EmailConfirmedGuard } from './email-confirmed.guard';

@Controller('protected')
export class ProtectedController {
  @Get()
  @UseGuards(EmailConfirmedGuard)
  getProtectedData() {
    return { message: '只有邮箱已验证的用户才能访问此接口' };
  }
}

整个控制器使用

@Controller('protected')
@UseGuards(EmailConfirmedGuard)
export class ProtectedController {
  @Get()
  getProtectedData() {
    return { message: '控制器下所有接口都需要邮箱验证' };
  }
}

可选方案:实时查询数据库验证

如果需要用户验证邮箱后立即生效(不等待JWT过期),可以在Guard中查询数据库获取最新状态:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { UserService } from '../user/user.service';

@Injectable()
export class EmailConfirmedGuard extends AuthGuard('jwt') {
  constructor(private readonly userService: UserService) {
    super();
  }

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const isAuthenticated = await super.canActivate(context);
    if (!isAuthenticated) {
      return false;
    }

    const request = context.switchToHttp().getRequest();
    const userId = request.user.sub;

    // 查询数据库获取最新用户状态
    const user = await this.userService.findOneById(userId);
    return user.isEmailConfirmed === true;
  }
}

该方案优点是状态实时更新,缺点是每次请求都要查询数据库,性能略有下降,适合对实时性要求高的场景。

内容的提问来源于stack exchange,提问作者Dave

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 09:57:47