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:
- 创建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!; } }
- 修改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; } } }
- 在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
相关产品推荐
相关产品推荐

