1. RBAC
- RBAC์ Role Based Access Control์ ์ฝ์๋ค.
- ์ญํ (Role) ๊ธฐ๋ฐ์ผ๋ก ๊ถํ (Permission)์ ๋๋ ์ ํน์ ๋ฆฌ์์ค์ CRUD ์์ ์ ํ ์ ์๋์ง ์ฌ๋ถ๋ฅผ ๊ฒฐ์ ํ๋ค
1.1 ๊ตฌํ ๋ฐฉ์
1export enum Role {2ADMIN = 'admin',3USER = 'user',4Guest = 'guest',5}
1import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'2import { Reflector } from '@nestjs/core'3import { Role } from '../const/role.enum'45@Injectable()6export class RolesGuard implements CanActivate {7constructor(private reflector: Reflector) {}89canActivate(context: ExecutionContext): boolean {10const roles = this.reflector.get<Role[]>('roles', context.getHandler())11if (!roles) {12return true13}14const request = context.switchToHttp().getRequest()15const user = request.user16return roles.some(role => user.roles?.includes(role))17}18}
2. Roles Decorator ์์
users/decorator/roles.decorator.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { SetMetadata } from '@nestjs/common'2import { RolesEnum } from '../const/roles.const'34export const ROLES_KEY = 'user_roles'56// @Roles(RolesEnum.ADMIN)7export const Roles = (role: RolesEnum) => SetMetadata(ROLES_KEY, role)
posts/posts.controller.ts ํ์ผ์ ๋ค์๊ณผ ๊ฐ์ด ์์ ํ๋ค.
1// **** (6) DELETE /posts/:id : id์ ํด๋นํ๋ post๋ฅผ ์ญ์ 2@Delete(':id')3@UseGuards(AccessTokenGuard)4@Roles(RolesEnum.ADMIN)5deletePost(@Param('id', ParseIntPipe) id: number) {6return this.postsService.deletePost(id)7}
3. RolesGuard ์์ฑํ๊ณ ์ ์ฉ
users/guard/roles.guard.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { CanActivate, ExecutionContext, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common'23import { Reflector } from '@nestjs/core'4import { ROLES_KEY } from '../decorator/roles.decorator'56@Injectable()7export class RolesGuard implements CanActivate {8constructor(private readonly reflector: Reflector) {}910async canActivate(context: ExecutionContext): Promise<boolean> {11/** Roles annotation์ ๋ํ metadata๋ฅผ ๊ฐ์ ธ์์ผํ๋ค.12* Reflector.getAllAndOverride() : ํค์ ๋ํ ์ ๋ ธํ ์ด์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์์ overrideํด์ค13*/14const requiredRole = this.reflector.getAllAndOverride(15ROLES_KEY, //16[context.getHandler(), context.getClass()],17)1819// Roles Annotation ๋ฑ๋ก ์๋ผ์์20if (!requiredRole) return true2122const { user } = context.switchToHttp().getRequest()2324if (!user) {25throw new UnauthorizedException(`ํ ํฐ์ ์ ๊ณต ํด์ฃผ์ธ์!`)26}2728if (user.role !== requiredRole) {29throw new ForbiddenException(`์ด ์์ ์ ์ํํ ๊ถํ์ด ์์ต๋๋ค. ${requiredRole} ๊ถํ์ด ํ์ํฉ๋๋ค.`)30}3132return true33}34}
app.module.ts ํ์ผ์ guard๋ฅผ ๋ฑ๋กํ๋ค.
1@Module({2// ์๋ต3providers: [4AppService,5{6provide: APP_INTERCEPTOR,7useClass: ClassSerializerInterceptor,8},9// ์๋ ์ฝ๋ ์ถ๊ฐ10{11provide: APP_GUARD,12useClass: RolesGuard,13},14],15}
4. ๋ชจ๋ Route ๊ธฐ๋ณธ Private๋ก ๋ง๋ค๊ณ IsPublic Annotation ์์
app.module.ts ํ์ผ์ AccessTokenGuard๋ฅผ ๋ฑ๋กํ๋ค.
1@Module({2// ์๋ต3providers: [4AppService,5{6provide: APP_INTERCEPTOR,7useClass: ClassSerializerInterceptor,8},9{10provide: APP_GUARD,11useClass: AccessTokenGuard,12},13{14provide: APP_GUARD,15useClass: RolesGuard,16},17],18}
์ด์ AccessTokenGuard๊ฐ ์ ์ฒด ์ ์ฉ๋๋, posts ์ปจํธ๋กค๋ฌ์์ ์ ์ฉํ AccessTokenGuard๋ค์ ์ญ์ ํ๋ค.
๊ทธ๋ฆฌ๊ณ common/decorator/is-public.decorator.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { SetMetadata } from '@nestjs/common'23export const IS_PUBLIC_KEY = 'is_public'45export const IsPublic = () => SetMetadata(IS_PUBLIC_KEY, true)
auth/guard/bearer-token.guard.ts ํ์ผ์ ๋ค์๊ณผ ๊ฐ์ด ์์ ํ๋ค.
1@Injectable()2export class BearerTokenGuard implements CanActivate {3constructor(4private readonly authService: AuthService,5private readonly usersService: UsersService,6private readonly reflector: Reflector,7) {}89async canActivate(context: ExecutionContext): Promise<boolean> {10const isPublic = this.reflector.getAllAndOverride(11IS_PUBLIC_KEY, //12[context.getHandler(), context.getClass()],13)1415const req = context.switchToHttp().getRequest()1617if (isPublic) {18req.isRoutePublic = true19return true20}2122// ์๋ต23}24}2526@Injectable()27export class AccessTokenGuard extends BearerTokenGuard {28async canActivate(context: ExecutionContext): Promise<boolean> {29await super.canActivate(context)3031const req = context.switchToHttp().getRequest()3233if (req.isRoutePublic) {34return true35}3637// ์๋ต38}39}4041@Injectable()42export class RefreshTokenGuard extends BearerTokenGuard {43async canActivate(context: ExecutionContext): Promise<boolean> {44await super.canActivate(context)45const req = context.switchToHttp().getRequest()4647if (req.isRoutePublic) {48return true49}5051// ์๋ต52}53}
์ด์ ๋ชจ๋ API๋ฅผ ๋น๊ณต๊ฐ๋ก ๋ฐ๊ฟจ๋ค. ๊ณต๊ฐ API๋ก ๋ง๋ค API๋ฅผ ์ฐพ์์ IsPublic ์ด๋ ธํ ์ด์ ์ ๋ถ์ฌ์ฃผ๋ฉด ๋๋ค.
posts ์ปจํธ๋กค๋ฌ์ ๋ค์๊ณผ ๊ฐ์ด IsPublic ์ด๋ ธํ ์ด์ ์ ๋ถ์ฌ์ค๋ค.
1// **** (1) GET /posts : ๋ชจ๋ post ์กฐํ2@Get()3@IsPublic()4getPosts(@Query() query: PaginatePostDto) {5return this.postsService.paginatePosts(query)6}78// **** 2) GET /posts/:id : id์ ํด๋นํ๋ post ์กฐํ9// @Param('id') ๋ป : ๊ฐ์ ธ์ค๋ ํ๋ผ๋ฏธํฐ์ ์ด๋ฆ์ id์ด๋ค10@Get(':id')11@IsPublic()12getPost(@Param('id', ParseIntPipe) id: number) {13return this.postsService.getPostById(id)14}
auth ์ปจํธ๋กค๋ฌ์ ๋ค์๊ณผ ๊ฐ์ด IsPublic ์ด๋ ธํ ์ด์ ์ ๋ถ์ฌ์ค๋ค.
1@Post('token/access')2@IsPublic()3@UseGuards(RefreshTokenGuard)4postTokenAccess(@Headers('authorization') rawToken: string) {5// ์๋ต6}78@Post('token/refresh')9@IsPublic()10@UseGuards(RefreshTokenGuard)11postTokenRefresh(@Headers('authorization') rawToken: string) {12// ์๋ต13}1415@Post('login/email')16@IsPublic()17@UseGuards(BasicTokenGuard)18postLoginEmail(19@Headers('authorization') rawToken: string, //20// @Request() req, // ๊ฐ๋์ ์์ฑํ req๋ฅผ ๊ฐ์ ธ์์ ์ฐ๊ธฐ21) {22// ์๋ต23}2425@Post('register/email')26@IsPublic()27postRegisterEmail(@Body() body: RegisterUserDto) {28return this.authService.registerWithEmail(body)29}
5. Public Route ์ ๋ฆฌ
๋ชจ๋ API๋ฅผ ๋น๊ณต๊ฐ๋ก ๋ฐ๊ฟจ์ผ๋, ๊ณต๊ฐ API๋ก ๋ง๋ค API๋ฅผ ์ฐพ์์ IsPublic ์ด๋ ธํ ์ด์ ์ ๋ถ์ฌ์ค๋ค.
1import { Controller, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'2import { CommonService } from './common.service'3import { FileInterceptor } from '@nestjs/platform-express'45@Controller('common')6export class CommonController {7constructor(private readonly commonService: CommonService) {}89@Post('image')10@UseInterceptors(FileInterceptor('image'))11postImage(@UploadedFile() file: Express.Multer.File) {12return {13fileName: file.filename,14}15}16}
๋๊ธ ํ์ด์ง๋ค์ด์ , ๋๊ธ 1๊ฐ ๊ฐ์ ธ์ค๊ธฐ์ ๊ณต๊ฐ API๋ก, ๋๋จธ์ง API๋ค์ accessGuard ์ด๋ ธํ ์ด์ ์ ์ญ์ ํ๋ค.
1@Controller('posts/:postId/comments')2export class CommentsController {3// ์๋ต4// **** (1) ๋๊ธ ํ์ด์ง๋ค์ด์ 5@Get()6@IsPublic()7getComments() {} // ์๋ต89// **** (2) ํน์ ๋๊ธ 1๊ฐ๋ง ๊ฐ์ ธ์ค๊ธฐ10@Get(':commentId')11@IsPublic()12getComment(@Param('commentId', ParseIntPipe) commentId: number) {13return this.commentsService.getCommentById(commentId)14}1516// **** (3) ๋๊ธ ์์ฑ17@Post()18@UseInterceptors(TransactionInterceptor)19async postComment() {} // ์๋ต2021// **** (4) ๋๊ธ ์์ 22@Patch(':commentId')23@UseGuards(IsCommentMineOrAdminGuard)24async patchComment() {} // ์๋ต2526// **** (5) ๋๊ธ ์ญ์ 27@Delete(':commentId')28async deleteComment() {} // ์๋ต29}
posts ์ปจํธ๋กค๋ฌ์ ์ด๋ ธํ ์ด์ ์ ๋ค์๊ณผ ๊ฐ์ด ์์ ํ๋ค.
1@Controller('posts')2export class PostsController {3// ์๋ต45// **** (1) GET /posts : ๋ชจ๋ post ์กฐํ6@Get()7@IsPublic()8getPosts(@Query() query: PaginatePostDto) {9return this.postsService.paginatePosts(query)10}1112// **** 2) GET /posts/:id : id์ ํด๋นํ๋ post ์กฐํ13// @Param('id') ๋ป : ๊ฐ์ ธ์ค๋ ํ๋ผ๋ฏธํฐ์ ์ด๋ฆ์ id์ด๋ค14@Get(':id')15@IsPublic()16getPost() {} // ์๋ต1718@Post()19@UseInterceptors(TransactionInterceptor)20async postPosts() {} // ์๋ต2122// (4) POST /posts/random : ๋ฌด์์ ํฌ์คํธ๋ฅผ ์์ฑ23@Post('random')24async postPostsRandom() {} // ์๋ต2526// **** (5) PATCH /posts/:id : id์ ํด๋นํ๋ post๋ฅผ ๋ถ๋ถ ๋ณ๊ฒฝ27@Patch(':id')28patchPost() {} // ์๋ต2930// **** (6) DELETE /posts/:id : id์ ํด๋นํ๋ post๋ฅผ ์ญ์ 31@Delete(':id')32@Roles(RolesEnum.ADMIN)33deletePost() {} // ์๋ต34}
users ์ปจํธ๋กค๋ฌ์ Roles ์ด๋ ธํ ์ด์ ์ ์ถ๊ฐํ๋ค.
1@Controller('users')2export class UsersController {3constructor(private readonly usersService: UsersService) {}45@Get()6@Roles(RolesEnum.ADMIN)7getUsers() {8return this.usersService.getAllUsers()9}10}