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

Nestjs结合Nodemailer与OAuth2,如何刷新过期的Access Token?

解决NestJS中Nodemailer OAuth2 Token过期自动刷新问题

问题分析

当前代码在应用启动时一次性获取Access Token并配置到Nodemailer transport中,当Token过期后无法自动刷新,导致后续邮件发送失败。

方案一:利用Nodemailer内置的OAuth2自动刷新(推荐)

Nodemailer的OAuth2认证支持直接传入refresh_token,内部会自动处理Token的过期检测与刷新,无需手动提前获取Access Token。只需调整MailerModule的配置,移除手动获取Token的逻辑:

import { MailerModule } from "@nestjs-modules/mailer";
import { Global, Module } from "@nestjs/common";
import { HandlebarsAdapter } from "@nestjs-modules/mailer/dist/adapters/handlebars.adapter";
import { DatabaseService } from "@/base/database/database.service";
import { ConfigService } from "@nestjs/config";
import { MailService } from "@/base/mail/mail.service";

@Global()
@Module({
  imports: [
    MailerModule.forRootAsync({
      useFactory: async (
        databaseService: DatabaseService,
        configService: ConfigService,
      ) => {
        return {
          transport: {
            service: "gmail",
            auth: {
              type: "OAuth2",
              user: configService.get<string>("NODEMAILER_USER"),
              clientId: configService.get<string>("GOOGLE_CLIENT_ID"),
              clientSecret: configService.get<string>("GOOGLE_CLIENT_SECRET"),
              refreshToken: configService.get<string>("GOOGLE_REFRESH_TOKEN"), // 兼容refresh_token下划线写法
            },
          },
          defaults: {
            from: `No reply <${configService.get<string>("NODEMAILER_USER")}>`,
          },
          template: {
            dir: __dirname + "/template",
            adapter: new HandlebarsAdapter(),
            options: {
              strict: true,
            },
          },
        };
      },
      inject: [DatabaseService, ConfigService],
    }),
  ],
  controllers: [],
  providers: [MailService],
  exports: [MailService],
})
export class MailModule {}

说明:Nodemailer会在每次发送邮件前自动检查Token有效性,若过期则用refresh_token获取新的Access Token,全程无需手动干预。

方案二:自定义Token刷新逻辑(适合需自定义监控的场景)

如果需要自主控制Token刷新时机,可创建OAuth2Client单例服务,在每次发邮件前检查并更新Token:

  1. 创建OAuth2客户端服务:
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { OAuth2Client } from "google-auth-library";

@Injectable()
export class OAuth2Service {
  private oAuth2Client: OAuth2Client;

  constructor(private configService: ConfigService) {
    this.oAuth2Client = new OAuth2Client(
      configService.get<string>("GOOGLE_CLIENT_ID"),
      configService.get<string>("GOOGLE_CLIENT_SECRET"),
    );
    this.oAuth2Client.setCredentials({
      refresh_token: configService.get<string>("GOOGLE_REFRESH_TOKEN"),
    });
  }

  async getValidAccessToken(): Promise<string> {
    const credentials = this.oAuth2Client.credentials;
    // 提前5分钟刷新即将过期的Token
    if (credentials.expiry_date && Date.now() >= credentials.expiry_date - 5 * 60 * 1000) {
      const response = await this.oAuth2Client.refreshAccessToken();
      return response.credentials.access_token!;
    }
    return credentials.access_token!;
  }
}
  1. 修改MailService,发送前更新Token:
import { Injectable, Logger } from "@nestjs/common";
import { MailerService } from "@nestjs-modules/mailer";
import { ISendMailOptions } from "@nestjs-modules/mailer/dist/interfaces/send-mail-options.interface";
import { OAuth2Service } from "./oauth2.service";

@Injectable()
export class MailService {
  private readonly logger = new Logger(MailService.name);

  constructor(
    private mailerService: MailerService,
    private oAuth2Service: OAuth2Service,
  ) {}

  public async sendMail(mailOptions: ISendMailOptions) {
    try {
      const accessToken = await this.oAuth2Service.getValidAccessToken();
      // 更新transport的Access Token
      (this.mailerService.transport as any).auth.accessToken = accessToken;

      await this.mailerService.sendMail(mailOptions);
      this.logger.log(`Mail is sent to ${mailOptions.to}`);
    } catch (error) {
      throw error;
    }
  }
}
  1. 在MailModule中注册OAuth2Service:
// 其他导入不变
import { OAuth2Service } from "./oauth2.service";

@Global()
@Module({
  imports: [
    MailerModule.forRootAsync({
      useFactory: async (configService: ConfigService) => {
        return {
          transport: {
            service: "gmail",
            auth: {
              type: "OAuth2",
              user: configService.get<string>("NODEMAILER_USER"),
              clientId: configService.get<string>("GOOGLE_CLIENT_ID"),
              clientSecret: configService.get<string>("GOOGLE_CLIENT_SECRET"),
              refresh_token: configService.get<string>("GOOGLE_REFRESH_TOKEN"),
            },
          },
          // 其余配置不变
        };
      },
      inject: [ConfigService],
    }),
  ],
  controllers: [],
  providers: [MailService, OAuth2Service], // 新增OAuth2Service
  exports: [MailService],
})
export class MailModule {}

说明:此方案适合需要添加Token刷新日志、监控告警等自定义逻辑的场景。

内容的提问来源于stack exchange,提问作者Acus Gia Phuc

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 16:44:51