## 認證(Authentication)
身份驗證是大多數現有應用程序的重要組成部分。有許多不同的方法、策略和方法來處理用戶授權。任何項目采用的方法取決于其特定的應用程序要求。本章介紹了幾種可以適應各種不同要求的身份驗證方法。
`passport` 是目前最流行的 `node.js` 認證庫,為社區所熟知,并相繼應用于許多生產應用中。將此工具與 `Nest` 框架集成起來非常簡單。為了演示,我們將設置 passport-http-bearer 和 passport-jwt 策略。
`Passport`是最流行的 `node.js` 身份驗證庫,為社區所熟知,并成功地應用于許多生產應用程序中。將這個庫與使用 `@nestjs/passport` 模塊的 `Nest` 應用程序集成起來非常簡單。在較高級別,`Passport` 執行一系列步驟以:
- 通過驗證用戶的"證"(例如用戶名/密碼、`JSON Web`令牌( `JWT` )或身份提供者的身份令牌)來驗證用戶的身份。
- 管理經過身份驗證的狀態(通過發出可移植的令牌,例如 `JWT`,或創建一個 `Express` 會話)
- 將有關經過身份驗證的用戶的信息附加到請求對象,以便在路由處理程序中進一步使用
`Passport`具有豐富的策略生態系統,可實施各種身份驗證機制。 盡管概念上很簡單,但是您可以選擇的 `Passport` 策略集非常多,并且有很多種類。 `Passport` 將這些不同的步驟抽象為標準模式,而 `@nestjs/passport` 模塊將該模式包裝并標準化為熟悉的Nest構造。
在本章中,我們將使用這些強大而靈活的模塊為 `RESTful API`服務器實現完整的端到端身份驗證解決方案。您可以使用這里描述的概念來實現 `Passport` 策略,以定制您的身份驗證方案。您可以按照本章中的步驟來構建這個完整的示例。您可以在[這里](https://github.com/nestjs/nest/tree/master/sample/19-auth-jwt)找到帶有完整示例應用程序的存儲庫。
### 身份認證
讓我們充實一下我們的需求。對于此用例,客戶端將首先使用用戶名和密碼進行身份驗證。一旦通過身份驗證,服務器將發出 `JWT`,該 `JWT` 可以在后續請求的授權頭中作為 `token`發送,以驗證身份驗證。我們還將創建一個受保護的路由,該路由僅對包含有效 `JWT` 的請求可訪問。
我們將從第一個需求開始:驗證用戶。然后我們將通過發行 `JWT` 來擴展它。最后,我們將創建一個受保護的路由,用于檢查請求上的有效 `JWT` 。
首先,我們需要安裝所需的軟件包。`Passport` 提供了一種名為 `Passport-local` 的策略,它實現了一種用戶名/密碼身份驗證機制,這符合我們在這一部分用例中的需求。
```bash
$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-local
```
對于您選擇的任何 `Passport` 策略,都需要 `@nestjs/Passport` 和 `Passport` 包。然后,需要安裝特定策略的包(例如,`passport-jwt` 或 `passport-local`),它實現您正在構建的特定身份驗證策略。此外,您還可以安裝任何 `Passport`策略的類型定義,如上面的 `@types/Passport-local` 所示,它在編寫 `TypeScript` 代碼時提供了幫助。
### Passport 策略
現在可以實現身份認證功能了。我們將首先概述用于任何 `Passport` 策略的流程。將 `Passport` 本身看作一個框架是有幫助的。框架的優雅之處在于,它將身份驗證過程抽象為幾個基本步驟,您可以根據實現的策略對這些步驟進行自定義。它類似于一個框架,因為您可以通過提供定制參數(作為 `JSON` 對象)和回調函數( `Passport` 在適當的時候調用這些回調函數)的形式來配置它。 `@nestjs/passport` 模塊將該框架包裝在一個 `Nest` 風格的包中,使其易于集成到 `Nest` 應用程序中。下面我們將使用 `@nestjs/passport` ,但首先讓我們考慮一下 `vanilla Passport` 是如何工作的。
在 `vanilla Passport` 中,您可以通過提供以下兩項配置策略:
1. 組特定于該策略的選項。例如,在 `JWT` 策略中,您可以提供一個秘令來對令牌進行簽名。
2. "驗證回調",在這里您可以告訴 `Passport` 如何與您的用戶存儲交互(在這里您可以管理用戶帳戶)。在這里,驗證用戶是否存在(或創建一個新用戶),以及他們的憑據是否有效。`Passport` 庫期望這個回調在驗證成功時返回完整的用戶消息,在驗證失敗時返回 `null`(失敗定義為用戶沒有找到,或者在使用 `Passport-local` 的情況下,密碼不匹配)。
使用 `@nestjs/passport` ,您可以通過擴展 `PassportStrategy` 類來配置 `passport` 策略。通過調用子類中的 `super()` 方法傳遞策略選項(上面第1項),可以選擇傳遞一個 `options` 對象。通過在子類中實現 `validate()` 方法,可以提供`verify` 回調(上面第2項)。
我們將從生成一個 `AuthModule` 開始,其中有一個 `AuthService` :
```bash
$ nest g module auth
$ nest g service auth
```
當我們實現 `AuthService` 時,我們會發現在 `UsersService` 中封裝用戶操作是很有用的,所以現在讓我們生成這個模塊和服務:
```bash
$ nest g module users
$ nest g service users
```
替換這些生成文件的默認內容,如下所示。對于我們的示例應用程序,`UsersService` 只是在內存中維護一個硬編碼的用戶列表,以及一個根據用戶名檢索用戶列表的 `find` 方法。在真正的應用程序中,這是您使用選擇的庫(例如 `TypeORM`、`Sequelize`、`Mongoose`等)構建用戶模型和持久層。
> users/users.service.ts
```typescript
import { Injectable } from '@nestjs/common';
export type User = any;
@Injectable()
export class UsersService {
private readonly users: User[];
constructor() {
this.users = [
{
userId: 1,
username: 'john',
password: 'changeme',
},
{
userId: 2,
username: 'chris',
password: 'secret',
},
{
userId: 3,
username: 'maria',
password: 'guess',
},
];
}
async findOne(username: string): Promise<User | undefined> {
return this.users.find(user => user.username === username);
}
}
```
在 `UsersModule` 中,惟一需要做的更改是將 `UsersService` 添加到 `@Module` 裝飾器的 `exports` 數組中,以便提供給其他模塊外部可見(我們很快將在 `AuthService` 中使用它)。
> users/users.module.ts
```typescript
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
```
我們的 `AuthService` 的任務是檢索用戶并驗證密碼。為此,我們創建了 `validateUser()` 方法。在下面的代碼中,我們使用 `ES6` 擴展操作符從 `user` 對象中提取 `password` 屬性,然后再返回它。稍后,我們將從 `Passport` 本地策略中調用 `validateUser()` 方法。
> auth/auth.service.ts
```typescript
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(private readonly usersService: UsersService) {}
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
}
```
> 當然,在實際的應用程序中,您不會以純文本形式存儲密碼。 取而代之的是使用帶有加密單向哈希算法的 `bcrypt` 之類的庫。使用這種方法,您只需存儲散列密碼,然后將存儲的密碼與輸入密碼的散列版本進行比較,這樣就不會以純文本的形式存儲或暴露用戶密碼。為了保持我們的示例應用程序的簡單性,我們違反了這個絕對命令并使用純文本。不要在真正的應用程序中這樣做!
現在,我們更新 `AuthModule` 來導入 `UsersModule` 。
> auth/auth.module.ts
```typescript
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
@Module({
imports: [UsersModule],
providers: [AuthService],
})
export class AuthModule {}
```
現在我們可以實現 `Passport` 本地身份驗證策略。在auth文件夾中創建一個名為 `local.strategy.ts` 文件,并添加以下代碼:
> auth/local.strategy.ts
```typescript
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 readonly 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;
}
}
```
我們遵循了前面描述的所有護照策略。在我們的 `passport-local` 用例中,沒有配置選項,因此我們的構造函數只是調用 `super()` ,沒有 `options` 對象。
我們還實現了 `validate()` 方法。對于每個策略,`Passport` 將使用適當的特定于策略的一組參數調用 `verify` 函數(使用 `@nestjs/Passport` 中的 `validate()` 方法實現)。對于本地策略,`Passport` 需要一個具有以下簽名的 `validate()` 方法: `validate(username: string, password: string): any`。
大多數驗證工作是在我們的 `AuthService` 中完成的(在 `UserService` 的幫助下),所以這個方法非常簡單。任何 `Passport` 策略的 `validate()` 方法都將遵循類似的模式,只是表示憑證的細節方面有所不同。如果找到了用戶并且憑據有效,則返回該用戶,以便 `Passport` 能夠完成其任務(例如,在請求對象上創建`user` 屬性),并且請求處理管道可以繼續。如果沒有找到,我們拋出一個異常,讓異常層處理它。
通常,每種策略的 `validate()` 方法的惟一顯著差異是如何確定用戶是否存在和是否有效。例如,在 `JWT` 策略中,根據需求,我們可以評估解碼令牌中攜帶的 `userId` 是否與用戶數據庫中的記錄匹配,或者是否與已撤銷的令牌列表匹配。因此,這種子類化和實現特定于策略驗證的模式是一致的、優雅的和可擴展的。
我們需要配置 `AuthModule` 來使用剛才定義的 `Passport` 特性。更新 `auth.module`。看起來像這樣:
> auth/auth.module.ts
```typescript
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
@Module({
imports: [UsersModule, PassportModule],
providers: [AuthService, LocalStrategy],
})
export class AuthModule {}
```
### 內置 Passport 守衛
守衛章節描述了守衛的主要功能:確定請求是否由路由處理程序。這仍然是正確的,我們將很快使用這個標準功能。但是,在使用 `@nestjs/passport` 模塊的情況下,我們還將引入一個新的小問題,這個問題一開始可能會讓人感到困惑,現在讓我們來討論一下。從身份驗證的角度來看,您的應用程序可以以兩種狀態存在:
1. 用戶/客戶端未登錄(未通過身份驗證)
2. 用戶/客戶端已登錄(已通過身份驗證)
在第一種情況下(用戶沒有登錄),我們需要執行兩個不同的功能:
- 限制未經身份驗證的用戶可以訪問的路由(即拒絕訪問受限制的路由)。 我們將使用熟悉的警衛來處理這個功能,方法是在受保護的路由上放置一個警衛。我們將在這個守衛中檢查是否存在有效的 `JWT` ,所以我們稍后將在成功發出 `JWT` 之后處理這個守衛。
- 當以前未經身份驗證的用戶嘗試登錄時,啟動身份驗證步驟。這時我們向有效用戶發出 `JWT` 的步驟。考慮一下這個問題,我們知道需要 `POST` 用戶名/密碼憑證來啟動身份驗證,所以我們將設置 `POST` `/auth/login` 路徑來處理這個問題。這就提出了一個問題:在這條路由上,我們究竟如何實施“護照-本地”戰略?
答案很簡單:使用另一種稍微不同類型的守衛。`@nestjs/passport` 模塊為我們提供了一個內置的守衛,可以完成這一任務。這個保護調用 `Passport` 策略并啟動上面描述的步驟(檢索憑證、運行`verify` 函數、創建用戶屬性等)。
上面列舉的第二種情況(登錄用戶)僅僅依賴于我們已經討論過的標準類型的守衛,以便為登錄用戶啟用對受保護路由的訪問。
### 登錄路由
有了這個策略,我們現在就可以實現一個簡單的 `/auth/login` 路由,并應用內置的守衛來啟動護照本地流。
打開 `app.controller.ts` 文件,并將其內容替換為以下內容:
> app.controller.ts
```typescript
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller()
export class AppController {
@UseGuards(AuthGuard('local'))
@Post('auth/login')
async login(@Request() req) {
return req.user;
}
}
```
對于 `@UseGuard(AuthGuard('local'))`,我們使用的是一個 `AuthGuard` ,它是在我們擴展護照-本地策略時 `@nestjs/passportautomatic` 為我們準備的。我們來分析一下。我們的 `Passport` 本地策略默認名為`"local"` 。我們在 `@UseGuards()` 裝飾器中引用這個名稱,以便將它與護照本地包提供的代碼關聯起來。這用于消除在應用程序中有多個 `Passport` 策略時調用哪個策略的歧義(每個策略可能提供一個特定于策略的 `AuthGuard` )。雖然到目前為止我們只有一個這樣的策略,但我們很快就會添加第二個,所以這是消除歧義所需要的。
為了測試我們的路由,我們將 `/auth/login` 路由簡單地返回用戶。這還允許我們演示另一個 `Passport` 特性: `Passport` 根據從 `validate()` 方法返回的值自動創建一個 `user` 對象,并將其作為 `req.user` 分配給請求對象。稍后,我們將用創建并返回 `JWT` 的代碼替換它。
因為這些是 `API` 路由,所以我們將使用常用的`cURL`庫來測試它們。您可以使用 `UsersService` 中硬編碼的任何用戶對象進行測試。
```bash
$ # POST to /auth/login
$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
$ # result -> {"userId":1,"username":"john"}
```
如果上述內容可以正常工作,可以通過直接將策略名稱傳遞給`AuthGuard()`來引入代碼庫中的魔術字符串。作為替代,我們推薦創建自己的類,如下所示:
> auth/local-auth.guard.ts
```typescript
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
```
```typescript
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(@Request() req) {
return req.user;
}
```
### JWT 功能
我們已經準備好進入JWT部分的認證系統。讓我們回顧并完善我們的需求:
- 允許用戶使用用戶名/密碼進行身份驗證,返回 `JWT` 以便在后續調用受保護的 `API` 端點時使用。我們正在努力滿足這一要求。為了完成它,我們需要編寫發出 `JWT` 的代碼。
- 創建基于`token` 的有效`JWT` 的存在而受保護的API路由。
我們需要安裝更多的包來支持我們的 `JWT` 需求:
```bash
$ npm install @nestjs/jwt passport-jwt
$ npm install @types/passport-jwt --save-dev
```
`@nest/jwt` 包是一個實用程序包,可以幫助 `jwt` 操作。`passport-jwt` 包是實現 `JWT` 策略的 `Passport`包,`@types/passport-jwt` 提供 `TypeScript` 類型定義。
讓我們仔細看看如何處理 `POST` `/auth/login` 請求。我們使用護照本地策略提供的內置`AuthGuard` 來裝飾路由。這意味著:
1. 只有在了用戶之后,才會調用路由處理程序
2. req參數將包含一個用戶屬性(在passport-local 身份驗證流期間由 `Passport` 填充)
考慮到這一點,我們現在終于可以生成一個真正的 `JWT` ,并以這種方式返回它。為了使我們的服務保持干凈的模塊化,我們將在 `authService` 中生成 `JWT` 。在auth文件夾中添加 `auth.service.ts` 文件,并添加 `login()` 方法,導入`JwtService` ,如下圖所示:
> auth/auth.service.ts
```typescript
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService
) {}
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: any) {
const payload = { username: user.username, sub: user.userId };
return {
access_token: this.jwtService.sign(payload),
};
}
}
```
我們使用 `@nestjs/jwt` 庫,該庫提供了一個 `sign()` 函數,用于從用戶對象屬性的子集生成 `jwt`,然后以簡單對象的形式返回一個 `access_token` 屬性。注意:我們選擇 `sub` 的屬性名來保持我們的 `userId` 值與`JWT` 標準一致。不要忘記將 `JwtService` 提供者注入到 `AuthService`中。
現在,我們需要更新 `AuthModule` 來導入新的依賴項并配置 `JwtModule` 。
首先,在auth文件夾下創建 `auth/constants.ts`,并添加以下代碼:
> auth/constants.ts
```typescript
export const jwtConstants = {
secret: 'secretKey',
};
```
我們將使用它在 `JWT` 簽名和驗證步驟之間共享密鑰。
不要公開此密鑰。我們在這里這樣做是為了清楚地說明代碼在做什么,但是在生產系統中,您必須使用適當的措施來保護這個密鑰,比如機密庫、環境變量或配置服務。
現在,在`auth` 文件夾下 `auth.module.ts`,并更新它看起來像這樣:
```typescript
auth/auth.module.tsJS
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy],
exports: [AuthService],
})
export class AuthModule {}
```
我們使用 `register()` 配置 `JwtModule` ,并傳入一個配置對象。有關 `Nest JwtModule` 的更多信息請參見[此處](https://github.com/nestjs/jwt/blob/master/README.md),有關可用配置選項的更多信息請參見[此處](https://github.com/auth0/node-jsonwebtoken#usage)。
現在我們可以更新 `/auth/login` 路徑來返回 `JWT` 。
> app.controller.ts
```typescript
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth/auth.service';
@Controller()
export class AppController {
constructor(private readonly authService: AuthService) {}
@UseGuards(AuthGuard('local'))
@Post('auth/login')
async login(@Request() req) {
return this.authService.login(req.user);
}
}
```
讓我們繼續使用 `cURL` 測試我們的路由。您可以使用 `UsersService` 中硬編碼的任何用戶對象進行測試。
```bash
$ # POST to /auth/login
$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
$ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
$ # Note: above JWT truncated
```
### 實施 Passport JWT
我們現在可以處理我們的最終需求:通過要求在請求時提供有效的 `JWT` 來保護端點。護照對我們也有幫助。它提供了用于用 `JSON Web` 標記保護 `RESTful` 端點的 `passport-jwt` 策略。在 `auth` 文件夾中 `jwt.strategy.ts`,并添加以下代碼:
> auth/jwt.strategy.ts
```typescript
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
```
對于我們的 `JwtStrategy` ,我們遵循了前面描述的所有 `Passport` 策略的相同配方。這個策略需要一些初始化,因此我們通過在 `super()` 調用中傳遞一個 `options` 對象來實現。您可以在[這里](https://github.com/mikenicholson/passport-jwt#configure-strategy)閱讀關于可用選項的更多信息。在我們的例子中,這些選項是:
- `jwtFromRequest`:提供從請求中提取 `JWT` 的方法。我們將使用在 `API` 請求的授權頭中提供`token`的標準方法。這里描述了其他選項。
`ignoreExpiration`:為了明確起見,我們選擇默認的 `false` 設置,它將確保 `JWT` 沒有過期的責任委托給 `Passport` 模塊。這意味著,如果我們的路由提供了一個過期的 `JWT` ,請求將被拒絕,并發送 `401` 未經授權的響應。護照會自動為我們辦理。
`secret orkey`:我們使用權宜的選項來提供對稱的秘密來簽署令牌。其他選項,如 `pemo` 編碼的公鑰,可能更適合于生產應用程序(有關更多信息,請參見[此處](https://github.com/mikenicholson/passport-jwt#extracting-the-jwt-from-the-request))。如前所述,無論如何,不要把這個秘密公開。
`validate()` 方法值得討論一下。對于 `JWT` 策略,`Passport` 首先驗證 `JWT` 的簽名并解碼 `JSON `。然后調用我們的 `validate()` 方法,該方法將解碼后的 `JSON` 作為其單個參數傳遞。根據 `JWT` 簽名的工作方式,我們可以保證接收到之前已簽名并發給有效用戶的有效 `token` 令牌。
因此,我們對 `validate()` 回調的響應很簡單:我們只是返回一個包含 `userId` 和 `username` 屬性的對象。再次回憶一下,`Passport` 將基于 `validate()` 方法的返回值構建一個`user` 對象,并將其作為屬性附加到請求對象上。
同樣值得指出的是,這種方法為我們留出了將其他業務邏輯注入流程的空間(就像"掛鉤"一樣)。例如,我們可以在 `validate()` 方法中執行數據庫查詢,以提取關于用戶的更多信息,從而在請求中提供更豐富的用戶對象。這也是我們決定進行進一步令牌驗證的地方,例如在已撤銷的令牌列表中查找 `userId` ,使我們能夠執行令牌撤銷。我們在示例代碼中實現的模型是一個快速的 `"無狀態JWT"` 模型,其中根據有效 `JWT` 的存在立即對每個 `API` 調用進行授權,并在請求管道中提供關于請求者(其 `userid` 和 `username`)的少量信息。
在 `AuthModule` 中添加新的 `JwtStrategy` 作為提供者:
> auth/auth.module.ts
```typescript
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
```
通過導入 `JWT` 簽名時使用的相同密鑰,我們可以確保 `Passport` 執行的驗證階段和 `AuthService` 執行的簽名階段使用公共密鑰。
實現受保護的路由和 `JWT` 策略保護,我們現在可以實現受保護的路由及其相關的保護。
打開 `app.controller.ts` 文件,更新如下:
> app.controller.ts
```typescript
import { Controller, Get, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth/auth.service';
@Controller()
export class AppController {
constructor(private readonly authService: AuthService) {}
@UseGuards(AuthGuard('local'))
@Post('auth/login')
async login(@Request() req) {
return this.authService.login(req.user);
}
@UseGuards(AuthGuard('jwt'))
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}
```
同樣,我們將應用在配置 `passport-jwt` 模塊時 `@nestjs/passport` 模塊自動為我們提供的 `AuthGuard` 。這個保護由它的默認名稱 `jwt` 引用。當我們請求` GET /profile` 路由時,保護程序將自動調用我們的 `passport-jwt` 自定義配置邏輯,驗證 `JWT` ,并將用戶屬性分配給請求對象。
確保應用程序正在運行,并使用 `cURL` 測試路由。
```bash
$ # GET /profile
$ curl http://localhost:3000/profile
$ # result -> {"statusCode":401,"error":"Unauthorized"}
$ # POST /auth/login
$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
$ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm... }
$ # GET /profile using access_token returned from previous step as bearer code
$ curl http://localhost:3000/profile -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."
$ # result -> {"userId":1,"username":"john"}
```
注意,在 `AuthModule` 中,我們將 `JWT` 配置為 `60` 秒過期。這個過期時間可能太短了,而處理令牌過期和刷新的細節超出了本文的范圍。然而,我們選擇它來展示`JWT` 的一個重要品質和 `jwt` 護照戰略。如果您在驗證之后等待 `60` 秒再嘗試 `GET /profile` 請求,您將收到 `401` 未授權響應。這是因為 `Passport` 會自動檢查 `JWT` 的過期時間,從而省去了在應用程序中這樣做的麻煩。
我們現在已經完成了 `JWT` 身份驗證實現。`JavaScript` 客戶端(如 `Angular/React/Vue` )和其他 `JavaScript` 應用程序現在可以安全地與我們的 `API` 服務器進行身份驗證和通信。
### 示例[#](#example)
在[這里](https://github.com/nestjs/nest/tree/master/sample/19-auth-jwt)可以看到本節完整的程序代碼。
### 默認策略
在我們的 `AppController` 中,我們在 `@AuthGuard()` 裝飾器中傳遞策略的名稱。我們需要這樣做,因為我們已經介紹了兩種 `Passport` 策略(護照本地策略和護照 `jwt` 策略),這兩種策略都提供了各種 `Passport` 組件的實現。傳遞名稱可以消除我們鏈接到的實現的歧義。當應用程序中包含多個策略時,我們可以聲明一個默認策略,這樣如果使用該默認策略,我們就不必在 `@AuthGuard` 裝飾器中傳遞名稱。下面介紹如何在導入 `PassportModule` 時注冊默認策略。這段代碼將進入 `AuthModule` :
要確定默認策略行為,您可以注冊 `PassportModule` 。
> auth.module.ts
```typescript
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
UsersModule
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
```
### 請求范圍策略
`passport`API基于將策略注冊到庫的全局實例。因此策略并沒有設計為依賴請求的選項的或者根據每個請求動態生成實例(更多內容見[請求范圍提供者](https://docs.nestjs.cn/8/fundamentals?id=%e6%89%80%e6%9c%89%e8%af%b7%e6%b1%82%e6%b3%a8%e5%85%a5))。當你配置你的策略為請求范圍時,`Nest`永遠不會將其實例化,因為它并沒有和任何特定路徑綁定。并沒有一個物理方法來決定哪個"請求范圍"策略會根據每個請求執行。
然而,在策略中總有辦法動態處理請求范圍提供者。我們在這里利用[模塊參考](https://docs.nestjs.cn/8/fundamentals?id=%e6%a8%a1%e5%9d%97%e5%8f%82%e8%80%83)特性。
首先,打開`local.strategy.ts`文件并且將`ModuleRef`按照正常方法注入其中:
```typescript
constructor(private moduleRef: ModuleRef){
super({
passReqToCallback:true;
})
}
```
> 注意: `ModuleRef` 類需要從`@nestjs/core`中導入。
要保證`passReqToCallback`屬性和上述示例中一樣配置為`true`。
在下一步中,請求的實例將被用于獲取一個當前上下文標識,而不是生成一個新的(更多關于請求上下文的內容見[這里](https://docs.nestjs.cn/8/fundamentals?id=%e6%a8%a1%e5%9d%97%e5%8f%82%e8%80%83))。
現在,在`LocalStrategy`類的`validate()`方法中,使用`ContextIdFactory`類中的`getByRequest()`方法來創建一個基于請求對象的上下文id,并將其傳遞給`resolve()`調用:
```typescript
async validate(
request: Request,
username: string,
password: string,
) {
const contextId = ContextIdFactory.getByRequest(request);
// "AuthService" is a request-scoped provider
const authService = await this.moduleRef.resolve(AuthService, contextId);
...
}
```
在上述例子中,`resolve()`方法會異步返回`AuthService`提供者的請求范圍實例(我們假設`AuthService`被標示為一個請求范圍提供者)。
### 擴展守衛
在大多數情況下,使用一個提供的`AuthGuard`類是有用的。然而,在一些用例中你可能只是希望簡單地擴展默認的錯誤處理或者認證邏輯。在這種情況下,你可以通過一個子類來擴展內置的類并且覆蓋其方法。
```typescript
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
// 在這里添加自定義的認證邏輯
// 例如調用 super.logIn(request) 來建立一個session
return super.canActivate(context);
}
handleRequest(err, user, info) {
// 可以拋出一個基于info或者err參數的異常
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}
```
除了擴展默認的錯誤處理和身份驗證邏輯之外,我們還可以允許身份驗證通過一系列策略。 第一個成功、重定向或錯誤的策略將停止鏈。 身份驗證失敗將依次通過每個策略,如果所有策略都失敗,則最終失敗。
~~~typescript
export class JwtAuthGuard extends AuthGuard(['strategy_jwt_1', 'strategy_jwt_2', '...']) { ... }
~~~
### 全局啟用身份驗證[#](#enable-authentication-globally)
如果您的絕大多數端點應該默認受到保護,您可以將身份驗證保護注冊為全局保護,而不是在每個控制器頂部使用 `@UseGuards()` 裝飾器,您可以簡單地標記哪些路由應該是公共的。
首先,使用以下構造(在任何模塊中)將 JwtAuthGuard 注冊為全局防護:
~~~typescript
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
~~~
有了這個,Nest 將自動將 `JwtAuthGuard` 綁定到所有端點。
現在我們必須提供一種機制來將路由聲明為公共的。 為此,我們可以使用 `SetMetadata` 裝飾器工廠函數創建自定義裝飾器。
~~~typescript
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
~~~
在上面的文件中,我們導出了兩個常量。 一個是名為 `IS_PUBLIC_KEY` 的元數據密鑰,另一個是我們將稱為 `Public` 的新裝飾器本身(您可以將其命名為 `SkipAuth` 或 `AllowAnon`,只要適合您的項目)。
現在我們有了一個自定義的 `@Public()` 裝飾器,我們可以用它來裝飾任何方法,如下所示:
~~~typescript
@Public()
@Get()
findAll() {
return [];
}
~~~
最后,我們需要 `JwtAuthGuard` 在找到`“isPublic”`元數據時返回 true。 為此,我們將使用 `Reflector` 類(在[此處](https://docs.nestjs.com/guards#putting-it-all-together)閱讀更多內容)。
~~~typescript
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}
~~~
### 請求范圍的策略[#](#request-scoped-strategies)
身份驗證 API 基于將策略注冊到庫的全局實例。 因此,策略并非設計為具有依賴于請求的選項或根據請求動態實例化(閱讀有關[請求范圍](https://docs.nestjs.com/fundamentals/injection-scopes)提供程序的更多信息)。 當您將策略配置為請求范圍時,Nest 將永遠不會實例化它,因為它不綁定到任何特定路由。 沒有物理方法可以確定每個請求應執行哪些“請求范圍”策略。
但是,有一些方法可以在策略內動態解析請求范圍的提供程序。 為此,我們利用了模塊引用功能。
首先,打開`local.strategy.ts`文件,按正常方式注入`ModuleRef`:
~~~typescript
constructor(private moduleRef: ModuleRef) {
super({
passReqToCallback: true,
});
}
~~~
>**提示**:`ModuleRef` 類是從 `@nestjs/core` 包中導入的。
請務必將 `passReqToCallback` 配置屬性設置為 `true`,如上所示。
在下一步中,請求實例將用于獲取當前上下文標識符,而不是生成新的標識符(在此處閱讀有關請求上下文的更多信息)。
現在,在 `LocalStrategy` 類的 `validate()` 方法中,使用 `ContextIdFactory` 類的 `getByRequest()` 方法根據請求對象創建上下文 id,并將其傳遞給 `resolve()` 調用:
~~~typescript
async validate(
request: Request,
username: string,
password: string,
) {
const contextId = ContextIdFactory.getByRequest(request);
// "AuthService" is a request-scoped provider
const authService = await this.moduleRef.resolve(AuthService, contextId);
...
}
~~~
在上面的示例中,`resolve()` 方法將異步返回 `AuthService` 提供者的請求范圍實例(我們假設 `AuthService` 被標記為請求范圍提供者)。
### 自定義 Passport
根據所使用的策略,`Passport`會采用一系列影響庫行為的屬性。使用 `register()` 方法將選項對象直接傳遞給`Passport`實例。例如:
```typescript
PassportModule.register({ session: true });
```
您還可以在策略的構造函數中傳遞一個 `options` 對象來配置它們。至于本地策略,你可以通過例如:
```typescript
constructor(private readonly authService: AuthService) {
super({
usernameField: 'email',
passwordField: 'password',
});
}
```
看看[Passport Website](http://www.passportjs.org/docs/oauth/)官方文檔吧。
### 命名策略
在實現策略時,可以通過向 `PassportStrategy` 函數傳遞第二個參數來為其提供名稱。如果你不這樣做,每個策略將有一個默認的名稱(例如,"jwt"的 `jwt`策略 ):
```typescript
export class JwtStrategy extends PassportStrategy(Strategy, 'myjwt')
```
然后,通過一個像 `@AuthGuard('myjwt')` 這樣的裝飾器來引用它。
### GraphQL
為了使用帶有 `GraphQL` 的 `AuthGuard` ,擴展內置的 `AuthGuard` 類并覆蓋 `getRequest()` 方法。
```typescript
@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
```
要使用上述結構,請確保在 `GraphQL` 模塊設置中將 `request (req)`對象作為上下文值的一部分傳遞:
```typescript
GraphQLModule.forRoot({
context: ({ req }) => ({ req }),
});
```
要在 `graphql` 解析器中獲得當前經過身份驗證的用戶,可以定義一個`@CurrentUser()`裝飾器:
```typescript
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
export const CurrentUser = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req.user;
},
);
```
要在解析器中使用上述裝飾器,請確保將其作為查詢的參數:
```typescript
@Query(returns => User)
@UseGuards(GqlAuthGuard)
whoAmI(@CurrentUser() user: User) {
return this.userService.findById(user.id);
}
```
- 介紹
- 概述
- 第一步
- 控制器
- 提供者
- 模塊
- 中間件
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 自定義裝飾器
- 基礎知識
- 自定義提供者
- 異步提供者
- 動態模塊
- 注入作用域
- 循環依賴
- 模塊參考
- 懶加載模塊
- 應用上下文
- 生命周期事件
- 跨平臺
- 測試
- 技術
- 數據庫
- Mongo
- 配置
- 驗證
- 緩存
- 序列化
- 版本控制
- 定時任務
- 隊列
- 日志
- Cookies
- 事件
- 壓縮
- 文件上傳
- 流式處理文件
- HTTP模塊
- Session(會話)
- MVC
- 性能(Fastify)
- 服務器端事件發送
- 安全
- 認證(Authentication)
- 授權(Authorization)
- 加密和散列
- Helmet
- CORS(跨域請求)
- CSRF保護
- 限速
- GraphQL
- 快速開始
- 解析器(resolvers)
- 變更(Mutations)
- 訂閱(Subscriptions)
- 標量(Scalars)
- 指令(directives)
- 接口(Interfaces)
- 聯合類型
- 枚舉(Enums)
- 字段中間件
- 映射類型
- 插件
- 復雜性
- 擴展
- CLI插件
- 生成SDL
- 其他功能
- 聯合服務
- 遷移指南
- Websocket
- 網關
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 適配器
- 微服務
- 概述
- Redis
- MQTT
- NATS
- RabbitMQ
- Kafka
- gRPC
- 自定義傳輸器
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 獨立應用
- Cli
- 概述
- 工作空間
- 庫
- 用法
- 腳本
- Openapi
- 介紹
- 類型和參數
- 操作
- 安全
- 映射類型
- 裝飾器
- CLI插件
- 其他特性
- 遷移指南
- 秘籍
- CRUD 生成器
- 熱重載
- MikroORM
- TypeORM
- Mongoose
- 序列化
- 路由模塊
- Swagger
- 健康檢查
- CQRS
- 文檔
- Prisma
- 靜態服務
- Nest Commander
- 問答
- Serverless
- HTTP 適配器
- 全局路由前綴
- 混合應用
- HTTPS 和多服務器
- 請求生命周期
- 常見錯誤
- 實例
- 遷移指南
- 發現
- 誰在使用Nest?