NestJS中如何实现Azure AD Passport身份验证?
嗨,我之前在项目里刚好折腾过这个!确实NestJS官方在Azure AD Passport身份验证这块的文档有点缺失,而且实际实现过程中确实会踩不少坑。我把完整的实现步骤和避坑指南整理出来了,你可以参考下:
在NestJS中实现Azure AD Passport身份验证完整指南
一、先搞定Azure AD应用配置(这步很关键,错了后面全白搭)
- 登录Azure门户,找到Azure Active Directory → 应用注册 → 点击「新建注册」
- 填写应用名称,选择支持的账户类型(比如单租户/多租户,根据你的业务需求)
- 注册完成后,记下这三个关键信息:租户ID(目录ID)、客户端ID
- 转到「证书和密码」,点击「新建客户端密码」,设置过期时间后,复制生成的密码值(这就是客户端密钥,只显示一次,一定要存好)
- 转到「API权限」,添加Microsoft Graph的
User.Read委托权限,然后点击「授予管理员同意」(如果是多租户应用,可能需要租户管理员操作)
二、安装依赖包
打开终端,在你的NestJS项目里安装所需依赖:
npm install @nestjs/passport passport-azure-ad passport @nestjs/config # 如果需要处理自定义JWT令牌,还可以装以下包 npm install @nestjs/jwt passport-jwt
三、配置环境变量
用@nestjs/config管理配置,新建.env文件,把Azure的配置填进去:
AZURE_AD_TENANT_ID=你的租户ID AZURE_AD_CLIENT_ID=你的客户端ID AZURE_AD_CLIENT_SECRET=你的客户端密钥 # 如果用授权码流(前端跳转登录),需要填重定向URI AZURE_AD_REDIRECT_URI=http://localhost:3000/auth/callback
然后在app.module.ts里导入ConfigModule:
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AuthModule } from './auth/auth.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), // 全局可用 AuthModule, ], }) export class AppModule {}
四、创建Passport策略(分两种常见场景)
场景1:后端API用Bearer令牌验证(最常见的API保护场景)
新建auth/azure-ad-bearer.strategy.ts:
import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { BearerStrategy } from 'passport-azure-ad'; import { ConfigService } from '@nestjs/config'; @Injectable() export class AzureAdBearerStrategy extends PassportStrategy(BearerStrategy, 'azure-ad') { constructor(private readonly configService: ConfigService) { super({ // Azure AD的身份元数据地址 identityMetadata: `https://login.microsoftonline.com/${configService.get('AZURE_AD_TENANT_ID')}/v2.0/.well-known/openid-configuration`, clientID: configService.get('AZURE_AD_CLIENT_ID'), validateIssuer: true, // 验证令牌签发者 issuer: `https://login.microsoftonline.com/${configService.get('AZURE_AD_TENANT_ID')}/v2.0`, audience: configService.get('AZURE_AD_CLIENT_ID'), // 令牌受众,填客户端ID或者应用ID URI loggingLevel: 'info', passReqToCallback: false, }); } // 自定义验证逻辑,这里可以对接你的数据库,检查用户是否存在 async validate(payload: any) { return { userId: payload.oid, // Azure AD里的用户唯一ID email: payload.emails?.[0], name: payload.name, }; } }
场景2:Web应用用授权码流(前端跳转Azure登录页)
如果是前后端分离或者服务端渲染的应用,需要用OIDC策略处理登录跳转和回调:
新建auth/azure-ad-oidc.strategy.ts:
import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { OIDCStrategy } from 'passport-azure-ad'; import { ConfigService } from '@nestjs/config'; @Injectable() export class AzureAdOidcStrategy extends PassportStrategy(OIDCStrategy, 'azure-ad-oidc') { constructor(private readonly configService: ConfigService) { super({ identityMetadata: `https://login.microsoftonline.com/${configService.get('AZURE_AD_TENANT_ID')}/v2.0/.well-known/openid-configuration`, clientID: configService.get('AZURE_AD_CLIENT_ID'), clientSecret: configService.get('AZURE_AD_CLIENT_SECRET'), redirectUrl: configService.get('AZURE_AD_REDIRECT_URI'), responseType: 'code id_token', // 同时获取授权码和ID令牌 responseMode: 'form_post', scope: ['openid', 'profile', 'email', 'offline_access'], // 请求的权限 loggingLevel: 'info', passReqToCallback: false, }); } async validate(iss: string, sub: string, profile: any, accessToken: string, refreshToken: string, done: Function) { // 这里可以把用户信息存入你的数据库,或者生成自己的JWT令牌 const user = { userId: sub, email: profile.emails?.[0].value, name: profile.displayName, accessToken, refreshToken, }; done(null, user); // 将用户信息挂载到req.user上 } }
五、创建AuthModule和控制器
1. 配置AuthModule
新建auth/auth.module.ts:
import { Module } from '@nestjs/common'; import { PassportModule } from '@nestjs/passport'; import { AzureAdBearerStrategy } from './azure-ad-bearer.strategy'; // 如果用授权码流,还要导入AzureAdOidcStrategy import { AzureAdOidcStrategy } from './azure-ad-oidc.strategy'; import { AuthController } from './auth.controller'; @Module({ imports: [PassportModule], providers: [AzureAdBearerStrategy, AzureAdOidcStrategy], // 根据场景添加策略 controllers: [AuthController], }) export class AuthModule {}
2. 编写AuthController(授权码流用)
新建auth/auth.controller.ts:
import { Controller, Get, Post, UseGuards, Request, Response } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Controller('auth') export class AuthController { // 跳转Azure登录页面 @Get('login') @UseGuards(AuthGuard('azure-ad-oidc')) async login() { // 这个方法会自动触发跳转到Azure的登录页,无需额外代码 } // 登录回调处理 @Post('callback') @UseGuards(AuthGuard('azure-ad-oidc')) async callback(@Request() req, @Response() res) { // 登录成功后,这里可以生成自己的JWT令牌返回给前端,或者跳转到前端页面 // 比如: // const jwtToken = this.authService.generateJwt(req.user); // res.cookie('token', jwtToken, { httpOnly: true }); res.redirect('/dashboard'); // 跳转到你的前端首页 } }
六、保护你的API路由
在需要验证的控制器方法上,使用@UseGuards(AuthGuard('azure-ad')):
import { Controller, Get, UseGuards, Request } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Controller('users') export class UsersController { @Get('profile') @UseGuards(AuthGuard('azure-ad')) getProfile(@Request() req) { // req.user就是我们在validate方法里返回的用户信息 return req.user; } }
七、常见问题及解决方案
我在实现过程中踩过这些坑,给你列出来:
- 问题1:令牌验证失败,提示「audience不匹配」
解决:如果是保护API,需要在Azure应用注册里设置「应用ID URI」,然后把策略里的audience改成这个URI;如果是客户端应用,audience填客户端ID即可。 - 问题2:授权码流回调后获取不到用户邮箱/名称
解决:检查scope是否包含profile和email,并且在Azure门户里已经授予了对应的权限;另外,确保responseType包含id_token(因为用户信息在ID令牌里)。 - 问题3:passport-azure-ad版本兼容性问题
解决:目前passport-azure-ad@4.x和NestJS v10.x兼容良好,避免使用低于v4的版本,安装时可以指定版本:npm install passport-azure-ad@4.x。 - 问题4:本地开发时HTTPS报错
解决:Azure AD允许localhost使用HTTP,所以本地开发时重定向URI填http://localhost:xxx即可;如果是部署到服务器,必须用HTTPS。
内容的提问来源于stack exchange,提问作者Yahli Gitzi
相关产品推荐
相关产品推荐

