時間:2023-04-19 01:06:02 | 來源:網(wǎng)站運(yùn)營
時間:2023-04-19 01:06:02 來源:網(wǎng)站運(yùn)營
加固你的網(wǎng)站安全–nestjs文檔更新:在最新的Nestjs文檔中,作者將安全內(nèi)容作為單獨(dú)的章節(jié)撰寫,以強(qiáng)調(diào)網(wǎng)絡(luò)安全的重要性,依據(jù)官方文檔的做法,可以有效提高網(wǎng)站安全性。$ npm install --save @nestjs/passport passport passport-local$ npm install --save-dev @types/passport-local
對于您選擇的任何 Passport 策略,都需要 @nestjs/Passport 和 Passport 包。然后,需要安裝特定策略的包(例如,passport-jwt 或 passport-local),它實(shí)現(xiàn)您正在構(gòu)建的特定身份驗(yàn)證策略。此外,您還可以安裝任何 Passport策略的類型定義,如上面的 @types/Passport-local 所示,它在編寫 TypeScript 代碼時提供了幫助。$ nest g module auth$ nest g service auth
當(dāng)我們實(shí)現(xiàn) AuthService 時,我們會發(fā)現(xiàn)在 UsersService 中封裝用戶操作是很有用的,所以現(xiàn)在讓我們生成這個模塊和服務(wù):$ nest g module users$ nest g service users
替換這些生成文件的默認(rèn)內(nèi)容,如下所示。對于我們的示例應(yīng)用程序,UsersService 只是在內(nèi)存中維護(hù)一個硬編碼的用戶列表,以及一個根據(jù)用戶名檢索用戶列表的 find 方法。在真正的應(yīng)用程序中,這是您使用選擇的庫(例如 TypeORM、Sequelize、Mongoose等)構(gòu)建用戶模型和持久層。users/users.service.ts
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 數(shù)組中,以便提供給其他模塊外部可見(我們很快將在 AuthService 中使用它)。users/users.module.ts
import { Module } from '@nestjs/common';import { UsersService } from './users.service';@Module({ providers: [UsersService], exports: [UsersService],})export class UsersModule {}
我們的 AuthService 的任務(wù)是檢索用戶并驗(yàn)證密碼。為此,我們創(chuàng)建了 validateUser() 方法。在下面的代碼中,我們使用 ES6 擴(kuò)展操作符從 user 對象中提取 password 屬性,然后再返回它。稍后,我們將從 Passport 本地策略中調(diào)用 validateUser() 方法。auth/auth.service.ts
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; }}
當(dāng)然,在實(shí)際的應(yīng)用程序中,您不會以純文本形式存儲密碼。 取而代之的是使用帶有加密單向哈希算法的 bcrypt 之類的庫。使用這種方法,您只需存儲散列密碼,然后將存儲的密碼與輸入密碼的散列版本進(jìn)行比較,這樣就不會以純文本的形式存儲或暴露用戶密碼。為了保持我們的示例應(yīng)用程序的簡單性,我們違反了這個絕對命令并使用純文本。不要在真正的應(yīng)用程序中這樣做!
現(xiàn)在,我們更新 AuthModule 來導(dǎo)入 UsersModule 。
auth/auth.module.ts
import { Module } from '@nestjs/common';import { AuthService } from './auth.service';import { UsersModule } from '../users/users.module';@Module({ imports: [UsersModule], providers: [AuthService],})export class AuthModule {}
現(xiàn)在我們可以實(shí)現(xiàn) Passport 本地身份驗(yàn)證策略。在auth文件夾中創(chuàng)建一個名為 local.strategy.ts 文件,并添加以下代碼:auth/local.strategy.ts
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; }}
我們遵循了前面描述的所有護(hù)照策略。在我們的 passport-local 用例中,沒有配置選項(xiàng),因此我們的構(gòu)造函數(shù)只是調(diào)用 super() ,沒有 options 對象。auth/auth.module.ts
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 {}
app.controller.ts
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 ,它是在我們擴(kuò)展護(hù)照-本地策略時 @nestjs/passportautomatic 為我們準(zhǔn)備的。我們來分析一下。我們的 Passport 本地策略默認(rèn)名為"local" 。我們在 @UseGuards() 裝飾器中引用這個名稱,以便將它與護(hù)照本地包提供的代碼關(guān)聯(lián)起來。這用于消除在應(yīng)用程序中有多個 Passport 策略時調(diào)用哪個策略的歧義(每個策略可能提供一個特定于策略的 AuthGuard )。雖然到目前為止我們只有一個這樣的策略,但我們很快就會添加第二個,所以這是消除歧義所需要的。$ # 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"}
如果上述內(nèi)容可以正常工作,可以通過直接將策略名稱傳遞給AuthGuard()來引入代碼庫中的魔術(shù)字符串。作為替代,我們推薦創(chuàng)建自己的類,如下所示:auth/local-auth.guard.ts
import { Injectable } from '@nestjs/common';import { AuthGuard } from '@nestjs/passport';@Injectable()export class LocalAuthGuard extends AuthGuard('local') {}@UseGuards(LocalAuthGuard)@Post('auth/login')async login(@Request() req) { return req.user;}
$ npm install @nestjs/jwt passport-jwt$ npm install @types/passport-jwt --save-dev
@nest/jwt 包是一個實(shí)用程序包,可以幫助 jwt 操作。passport-jwt 包是實(shí)現(xiàn) JWT 策略的 Passport包,@types/passport-jwt 提供 TypeScript 類型定義。auth/auth.service.ts
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() 函數(shù),用于從用戶對象屬性的子集生成 jwt,然后以簡單對象的形式返回一個 access_token 屬性。注意:我們選擇 sub 的屬性名來保持我們的 userId 值與JWT 標(biāo)準(zhǔn)一致。不要忘記將 JwtService 提供者注入到 AuthService中。auth/constants.ts
export const jwtConstants = { secret: 'secretKey',};
我們將使用它在 JWT 簽名和驗(yàn)證步驟之間共享密鑰。auth/auth.module.tsJSimport { 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 ,并傳入一個配置對象。有關(guān) Nest JwtModule 的更多信息請參見此處,有關(guān)可用配置選項(xiàng)的更多信息請參見此處。app.controller.ts
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); }}
讓我們繼續(xù)使用 cURL 測試我們的路由。您可以使用 UsersService 中硬編碼的任何用戶對象進(jìn)行測試。$ # 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
auth/jwt.strategy.ts
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() 調(diào)用中傳遞一個 options 對象來實(shí)現(xiàn)。您可以在這里閱讀關(guān)于可用選項(xiàng)的更多信息。在我們的例子中,這些選項(xiàng)是:auth/auth.module.ts
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 {}
通過導(dǎo)入 JWT 簽名時使用的相同密鑰,我們可以確保 Passport 執(zhí)行的驗(yàn)證階段和 AuthService 執(zhí)行的簽名階段使用公共密鑰。app.controller.ts
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; }}
同樣,我們將應(yīng)用在配置 passport-jwt 模塊時 @nestjs/passport 模塊自動為我們提供的 AuthGuard 。這個保護(hù)由它的默認(rèn)名稱 jwt 引用。當(dāng)我們請求GET /profile 路由時,保護(hù)程序?qū)⒆詣诱{(diào)用我們的 passport-jwt 自定義配置邏輯,驗(yàn)證 JWT ,并將用戶屬性分配給請求對象。$ # 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 秒過期。這個過期時間可能太短了,而處理令牌過期和刷新的細(xì)節(jié)超出了本文的范圍。然而,我們選擇它來展示JWT 的一個重要品質(zhì)和 jwt 護(hù)照戰(zhàn)略。如果您在驗(yàn)證之后等待 60 秒再嘗試 GET /profile 請求,您將收到 401 未授權(quán)響應(yīng)。這是因?yàn)?Passport 會自動檢查 JWT 的過期時間,從而省去了在應(yīng)用程序中這樣做的麻煩。auth.module.ts
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 {}
constructor(private moduleRef: ModuleRef){ super({ passReqToCallback:true; })}
注意: ModuleRef 類需要從@nestjs/core中導(dǎo)入。
要保證passReqToCallback屬性和上述示例中一樣配置為true。
在下一步中,請求的實(shí)例將被用于獲取一個當(dāng)前上下文標(biāo)識,而不是生成一個新的(更多關(guān)于請求上下文的內(nèi)容見這里)。
現(xiàn)在,在LocalStrategy類的validate()方法中,使用ContextIdFactory類中的getByRequest()方法來創(chuàng)建一個基于請求對向的上下文id,并將其傳遞給resolve()調(diào)用:
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提供者的請求范圍實(shí)例(我們假設(shè)AuthService被標(biāo)示為一個請求范圍提供者)。import { ExecutionContext, Injectable, UnauthorizedException,} from '@nestjs/common';import { AuthGuard } from '@nestjs/passport';@Injectable()export class JwtAuthGuard extends AuthGuard('jwt') { canActivate(context: ExecutionContext) { // 在這里添加自定義的認(rèn)證邏輯 // 例如調(diào)用 super.logIn(request) 來建立一個session return super.canActivate(context); } handleRequest(err, user, info) { // 可以拋出一個基于info或者err參數(shù)的異常 if (err || !user) { throw err || new UnauthorizedException(); } return user; }}
PassportModule.register({ session: true });
您還可以在策略的構(gòu)造函數(shù)中傳遞一個 options 對象來配置它們。至于本地策略,你可以通過例如:constructor(private readonly authService: AuthService) { super({ usernameField: 'email', passwordField: 'password', });}
看看Passport Website官方文檔吧。export class JwtStrategy extends PassportStrategy(Strategy, 'myjwt')
然后,通過一個像 @AuthGuard('myjwt') 這樣的裝飾器來引用它。@Injectable()export class GqlAuthGuard extends AuthGuard('jwt') { getRequest(context: ExecutionContext) { const ctx = GqlExecutionContext.create(context); return ctx.getContext().req; }}
要使用上述結(jié)構(gòu),請確保在 GraphQL 模塊設(shè)置中將 request (req)對象作為上下文字的一部分傳遞:GraphQLModule.forRoot({ context: ({ req }) => ({ req }),});
要在 graphql 解析器中獲得當(dāng)前經(jīng)過身份驗(yàn)證的用戶,可以定義一個@CurrentUser()裝飾器: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; },);
要在解析器中使用上述裝飾器,請確保將其作為查詢的參數(shù):@Query(returns => User)@UseGuards(GqlAuthGuard)whoAmI(@CurrentUser() user: User) { return this.userService.findById(user.id);}
role.enum.ts
export enum Role { User = 'user', Admin = 'admin',}
在更復(fù)雜的系統(tǒng)中,角色信息可能會存儲在數(shù)據(jù)庫里,或者從一個外部認(rèn)證提供者那里獲取。
然后,創(chuàng)建一個@Roles()的裝飾器,該裝飾器允許某些角色擁有獲取特定資源訪問權(quán)。
roles.decorator.ts
import { SetMetadata } from '@nestjs/common';import { Role } from '../enums/role.enum';export const ROLES_KEY = 'roles';export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
現(xiàn)在可以將@Roles()裝飾器應(yīng)用于任何路徑處理程序。cats.controller.ts
@Post()@Roles(Role.Admin)create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto);}
最后,我們創(chuàng)建一個RolesGuard類來比較當(dāng)前用戶擁有的角色和當(dāng)前路徑需要的角色。為了獲取路徑的角色(自定義元數(shù)據(jù)),我們使用Reflector輔助類,這是個@nestjs/core提供的一個開箱即用的類。roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';import { Reflector } from '@nestjs/core';@Injectable()export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [ context.getHandler(), context.getClass(), ]); if (!requiredRoles) { return true; } const { user } = context.switchToHttp().getRequest(); return requiredRoles.some((role) => user.roles?.includes(role)); }}
參見應(yīng)用上下文章節(jié)的反射與元數(shù)據(jù)部分,了解在上下文敏感的環(huán)境中使用Reflector的細(xì)節(jié)。
該例子被稱為“基礎(chǔ)的”是因?yàn)槲覀儍H僅在路徑處理層面檢查了用戶權(quán)限。在實(shí)際項(xiàng)目中,你可能有包含不同操作的終端/處理程序,它們各自需要不同的權(quán)限組合。在這種情況下,你可能要在你的業(yè)務(wù)邏輯中提供一個機(jī)制來檢查角色,這在一定程度上會變得難以維護(hù),因?yàn)槿狈σ粋€集中的地方來關(guān)聯(lián)不同的操作與權(quán)限。
在這個例子中,我們假設(shè)request.user包含用戶實(shí)例以及允許的角色(在roles屬性中)。在你的應(yīng)用中,需要將其與你的認(rèn)證守衛(wèi)關(guān)聯(lián)起來,參見認(rèn)證。
要確保該示例可以工作,你的User類看上去應(yīng)該像這樣:
class User { // ...other properties roles: Role[];}
最后,在控制層或者全局注冊RolesGuard。providers: [ { provide: APP_GUARD, useClass: RolesGuard, },],
當(dāng)一個沒有有效權(quán)限的用戶訪問一個終端時,Nest自動返回以下響應(yīng):{ "statusCode": 403, "message": "Forbidden resource", "error": "Forbidden"}
如果你想返回一個不同的錯誤響應(yīng),需要拋出特定異常來代替返回一個布爾值。
cats.controller.ts
@Post()@RequirePermissions(Permission.CREATE_CAT)create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto);}
在這個例子中,許可(和RBAC部分的角色類似)是一個TypeScript的枚舉,它包含了系統(tǒng)中所有的許可。
$ npm i @casl/ability
在本例中,我們選擇CASL,但也可以根據(jù)項(xiàng)目需要選擇其他類似庫例如accesscontrol或者acl。
安裝完成后,為了說明CASL的機(jī)制,我們定義了兩個類實(shí)體,User和Article。
class User { id: number; isAdmin: boolean;}
User類包含兩個屬性,id是用戶的唯一標(biāo)識,isAdmin代表用戶是否有管理員權(quán)限。class Article { id: number; isPublished: boolean; authorId: string;}
Article類包含三個屬性,分別是id、isPublished和authorId,id是文章的唯一標(biāo)識,isPublished代表文章是否發(fā)布,authorId代表發(fā)表該文章的用戶id。export enum Action { Manage = 'manage', Create = 'create', Read = 'read', Update = 'update', Delete = 'delete',}
manage是CASL的關(guān)鍵詞,代表任何操作。
要封裝CASL庫,需要創(chuàng)建CaslModule和CaslAbilityFactory。
$ nest g module casl$ nest g class casl/casl-ability.factory
創(chuàng)建完成后,在CaslAbilityFactory中定義creteForUser()方法。該方法將為用戶創(chuàng)建Ability對象。type Subjects = typeof Article | typeof User | Article | User | 'all';export type AppAbility = Ability<[Action, Subjects]>;@Injectable()export class CaslAbilityFactory { createForUser(user: User) { const { can, cannot, build } = new AbilityBuilder< Ability<[Action, Subjects]> >(Ability as AbilityClass<AppAbility>); if (user.isAdmin) { can(Action.Manage, 'all'); // read-write access to everything } else { can(Action.Read, 'all'); // read-only access to everything } can(Action.Update, Article, { authorId: user.id }); cannot(Action.Delete, Article, { isPublished: true }); return build(); }}
all是CASL的關(guān)鍵詞,代表任何對象。
Ability,AbilityBuilder,和AbilityClass從@casl/ability包中導(dǎo)入。
在上述例子中,我們使用AbilityBuilder創(chuàng)建了Ability實(shí)例,如你所見,can和cannot接受同樣的參數(shù),但代表不同含義,can允許對一個對象執(zhí)行操作而cannot禁止操作,它們各能接受4個參數(shù),參見CASL文檔。
最后,將CaslAbilityFactory添加到提供者中,并在CaslModule模塊中導(dǎo)出。
import { Module } from '@nestjs/common';import { CaslAbilityFactory } from './casl-ability.factory';@Module({ providers: [CaslAbilityFactory], exports: [CaslAbilityFactory],})export class CaslModule {}
現(xiàn)在,只要將CaslModule引入對象的上下文中,就可以將CaslAbilityFactory注入到任何標(biāo)準(zhǔn)類中。constructor(private caslAbilityFactory: CaslAbilityFactory) {}
在類中使用如下:const ability = this.caslAbilityFactory.createForUser(user);if (ability.can(Action.Read, 'all')) { // "user" has read access to everything}
Ability類更多細(xì)節(jié)參見CASL 文檔。
例如,一個非管理員用戶,應(yīng)該可以閱讀文章,但不允許創(chuàng)建一篇新文章或者刪除一篇已有文章。
const user = new User();user.isAdmin = false;const ability = this.caslAbilityFactory.createForUser(user);ability.can(Action.Read, Article); // trueability.can(Action.Delete, Article); // falseability.can(Action.Create, Article); // false
雖然Ability和AlbilityBuilder類都提供can和cannot方法,但其目的并不一樣,接受的參數(shù)也略有不同。
依照我們的需求,一個用戶應(yīng)該能更新自己的文章。
const user = new User();user.id = 1;const article = new Article();article.authorId = user.id;const ability = this.caslAbilityFactory.createForUser(user);ability.can(Action.Update, article); // truearticle.authorId = 2;ability.can(Action.Update, article); // false
如你所見,Ability實(shí)例允許我們通過一種可讀的方式檢查許可。AbilityBuilder采用類似的方式允許我們定義許可(并定義不同條件)。查看官方文檔了解更多示例。import { AppAbility } from '../casl/casl-ability.factory';interface IPolicyHandler { handle(ability: AppAbility): boolean;}type PolicyHandlerCallback = (ability: AppAbility) => boolean;export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback;
如上所述,我們提供了兩個可能的定義策略處理程序的方式,一個對象(實(shí)現(xiàn)了IPolicyHandle接口的類的實(shí)例)和一個函數(shù)(滿足PolicyHandlerCallback類型)。export const CHECK_POLICIES_KEY = 'check_policy';export const CheckPolicies = (...handlers: PolicyHandler[]) => SetMetadata(CHECK_POLICIES_KEY, handlers);
現(xiàn)在創(chuàng)建一個PoliciesGuard,它將解析并執(zhí)行所有和路徑相關(guān)的策略程序。@Injectable()export class PoliciesGuard implements CanActivate { constructor( private reflector: Reflector, private caslAbilityFactory: CaslAbilityFactory, ) {} async canActivate(context: ExecutionContext): Promise<boolean> { const policyHandlers = this.reflector.get<PolicyHandler[]>( CHECK_POLICIES_KEY, context.getHandler(), ) || []; const { user } = context.switchToHttp().getRequest(); const ability = this.caslAbilityFactory.createForUser(user); return policyHandlers.every((handler) => this.execPolicyHandler(handler, ability), ); } private execPolicyHandler(handler: PolicyHandler, ability: AppAbility) { if (typeof handler === 'function') { return handler(ability); } return handler.handle(ability); }}
在本例中,我們假設(shè)request.user包含了用戶實(shí)例。在你的應(yīng)用中,可能將其與你自定義的認(rèn)證守衛(wèi)關(guān)聯(lián)。參見認(rèn)證章節(jié)。
我們分析一下這個例子。policyHandlers是一個通過@CheckPolicies()裝飾器傳遞給方法的數(shù)組,接下來,我們用CaslAbilityFactory#create方法創(chuàng)建Ability對象,允許我們確定一個用戶是否擁有足夠的許可去執(zhí)行特定行為。我們將這個對象傳遞給一個可能是函數(shù)或者實(shí)現(xiàn)了IPolicyHandler類的實(shí)例的策略處理程序,暴露出handle()方法并返回一個布爾量。最后,我們使用Array#every方法來確保所有處理程序返回true。
為了測試這個守衛(wèi),我們綁定任意路徑處理程序,并且注冊一個行內(nèi)的策略處理程序(函數(shù)實(shí)現(xiàn)),如下:
@Get()@UseGuards(PoliciesGuard)@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Article))findAll() { return this.articlesService.findAll();}
我們也可以定義一個實(shí)現(xiàn)了IPolicyHandler的類來代替函數(shù)。export class ReadArticlePolicyHandler implements IPolicyHandler { handle(ability: AppAbility) { return ability.can(Action.Read, Article); }}
并這樣使用。@Get()@UseGuards(PoliciesGuard)@CheckPolicies(new ReadArticlePolicyHandler())findAll() { return this.articlesService.findAll();}
由于我們必須使用 new關(guān)鍵詞來實(shí)例化一個策略處理函數(shù),CreateArticlePolicyHandler類不能使用注入依賴。這在ModuleRef#get方法中強(qiáng)調(diào)過,參見這里)?;旧希娲ㄟ^@CheckPolicies()裝飾器注冊函數(shù)和實(shí)例,你需要允許傳遞一個Type<IPolicyHandler>,然后在守衛(wèi)中使用一個類型引用(moduleRef.get(YOUR_HANDLER_TYPE)獲取實(shí)例,或者使用ModuleRef#create方法進(jìn)行動態(tài)實(shí)例化。
import { createCipheriv, randomBytes } from 'crypto';import { promisify } from 'util';const iv = randomBytes(16);const password = 'Password used to generate key';// The key length is dependent on the algorithm.// In this case for aes256, it is 32 bytes.const key = (await promisify(scrypt)(password, 'salt', 32)) as Buffer;const cipher = createCipheriv('aes-256-ctr', key, iv);const textToEncrypt = 'Nest';const encryptedText = Buffer.concat([ cipher.update(textToEncrypt), cipher.final(),]);
接下來,解密encryptedText值。import { createDecipheriv } from 'crypto';const decipher = createDecipheriv('aes-256-ctr', key, iv);const decryptedText = Buffer.concat([ decipher.update(encryptedText), decipher.final(),]);
$ npm i bcrypt$ npm i -D @types/bcrypt
依賴安裝后,可以使用哈希函數(shù)。import * as bcrypt from 'bcrypt';const saltOrRounds = 10;const password = 'random_password';const hash = await bcrypt.hash(password, saltOrRounds);
使用genSalt函數(shù)來生成哈希需要的鹽。const salt = await bcrypt.genSalt();
使用compare函數(shù)來比較/檢查密碼。const isMatch = await bcrypt.compare(password, hash);
更多函數(shù)參見這里。要在全局使用Helmet,需要在調(diào)用app.use()之前或者可能調(diào)用app.use()函數(shù)之前注冊。這是由平臺底層機(jī)制中(EXpress或者Fastify)中間件/路徑的定義決定的。如果在定義路徑之后使用helmet或者cors中間件,其之前的路徑將不會應(yīng)用這些中間件,而僅在定義之后的路徑中應(yīng)用。
$ npm i --save helmet
安裝完成后,將其應(yīng)用為全局中間件。import * as helmet from 'helmet';// somewhere in your initialization fileapp.use(helmet());
如果在引入helmet時返回This expression is not callable錯誤。你可能需要將項(xiàng)目中tsconfig.json文件的allowSyntheticDefaultImports和esModuleInterop選項(xiàng)配置為true。在這種情況下,將引入聲明修改為:import helmet from 'helmet'。
$ npm i --save fastify-helmet
fastify-helmet需要作為Fastify插件而不是中間件使用,例如,用app.register()調(diào)用。import * as helmet from 'fastify-helmet';// somewhere in your initialization fileapp.register(helmet);
在使用apollo-server-fastify和fastify-helmet時,在GraphQL應(yīng)用中與CSP使用時可能出問題,需要如下配置CSP。
app.register(helmet, { contentSecurityPolicy: { directives: { defaultSrc: [`'self'`], styleSrc: [`'self'`, `'unsafe-inline'`, 'cdn.jsdelivr.net', 'fonts.googleapis.com'], fontSrc: [`'self'`, 'fonts.gstatic.com'], imgSrc: [`'self'`, 'data:', 'cdn.jsdelivr.net'], scriptSrc: [`'self'`, `https: 'unsafe-inline'`, `cdn.jsdelivr.net`], }, },});// If you are not going to use CSP at all, you can use this:app.register(helmet, { contentSecurityPolicy: false,});
const app = await NestFactory.create(ApplicationModule);app.enableCors();await app.listen(3000);
enableCors()方法使用一個可選的配置對象參數(shù)。該對象的可用屬性在其官方CORS文檔中有所描述。const app = await NestFactory.create(ApplicationModule, { cors: true });await app.listen(3000);
$ npm i --save csurf
正如 csurf 中間件頁面所解釋的,csurf 模塊需要首先初始化會話中間件或 cookie 解析器。有關(guān)進(jìn)一步說明,請參閱該文檔。
安裝完成后,將其應(yīng)用為全局中間件。
import * as csurf from 'csurf';// somewhere in your initialization fileapp.use(csurf());
$ npm i --save fastify-csrf
安裝完成后,將其注冊為fastify-csrf插件。import fastifyCsrf from 'fastify-csrf';// somewhere in your initialization fileapp.register(fastifyCsrf);
$ npm i --save express-rate-limit
安裝完成后,將其應(yīng)用為全局中間件。import * as rateLimit from 'express-rate-limit';// somewhere in your initialization fileapp.use( rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs }),);
如果在服務(wù)器和以太網(wǎng)之間存在負(fù)載均衡或者反向代理,Express可能需要配置為信任proxy設(shè)置的頭文件,從而保證最終用戶得到正確的IP地址。要如此,首先使用NestExpressApplication平臺接口來創(chuàng)建你的app實(shí)例,然后配置trust proxy設(shè)置。const app = await NestFactory.create<NestExpressApplication>(AppModule);// see https://expressjs.com/en/guide/behind-proxies.htmlapp.set('trust proxy', 1);
如果使用 FastifyAdapter,用 fastify-rate-limit替換。
關(guān)鍵詞:更新,安全
客戶&案例
營銷資訊
關(guān)于我們
微信公眾號
版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。