๐ŸŽ‰ berenickt ๋ธ”๋กœ๊ทธ์— ์˜จ ๊ฑธ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค. ๐ŸŽ‰
Back
NestJs
34-RBAC-Role Based Access Control

1. RBAC

  • RBAC์€ Role Based Access Control์˜ ์•ฝ์ž๋‹ค.
  • ์—ญํ•  (Role) ๊ธฐ๋ฐ˜์œผ๋กœ ๊ถŒํ•œ (Permission)์„ ๋‚˜๋ˆ ์„œ ํŠน์ • ๋ฆฌ์†Œ์Šค์— CRUD ์ž‘์—…์„ ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•œ๋‹ค

1.1 ๊ตฌํ˜„ ๋ฐฉ์‹

1
export enum Role {
2
ADMIN = 'admin',
3
USER = 'user',
4
Guest = 'guest',
5
}
1
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
2
import { Reflector } from '@nestjs/core'
3
import { Role } from '../const/role.enum'
4
5
@Injectable()
6
export class RolesGuard implements CanActivate {
7
constructor(private reflector: Reflector) {}
8
9
canActivate(context: ExecutionContext): boolean {
10
const roles = this.reflector.get<Role[]>('roles', context.getHandler())
11
if (!roles) {
12
return true
13
}
14
const request = context.switchToHttp().getRequest()
15
const user = request.user
16
return roles.some(role => user.roles?.includes(role))
17
}
18
}

2. Roles Decorator ์ž‘์—…

users/decorator/roles.decorator.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

users/decorator/roles.decorator.ts
1
import { SetMetadata } from '@nestjs/common'
2
import { RolesEnum } from '../const/roles.const'
3
4
export const ROLES_KEY = 'user_roles'
5
6
// @Roles(RolesEnum.ADMIN)
7
export const Roles = (role: RolesEnum) => SetMetadata(ROLES_KEY, role)

posts/posts.controller.ts ํŒŒ์ผ์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•œ๋‹ค.

posts/posts.controller.ts
1
// **** (6) DELETE /posts/:id : id์— ํ•ด๋‹นํ•˜๋Š” post๋ฅผ ์‚ญ์ œ
2
@Delete(':id')
3
@UseGuards(AccessTokenGuard)
4
@Roles(RolesEnum.ADMIN)
5
deletePost(@Param('id', ParseIntPipe) id: number) {
6
return this.postsService.deletePost(id)
7
}

3. RolesGuard ์ƒ์„ฑํ•˜๊ณ  ์ ์šฉ

users/guard/roles.guard.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

users/guard/roles.guard.ts
1
import { CanActivate, ExecutionContext, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common'
2
3
import { Reflector } from '@nestjs/core'
4
import { ROLES_KEY } from '../decorator/roles.decorator'
5
6
@Injectable()
7
export class RolesGuard implements CanActivate {
8
constructor(private readonly reflector: Reflector) {}
9
10
async canActivate(context: ExecutionContext): Promise<boolean> {
11
/** Roles annotation์— ๋Œ€ํ•œ metadata๋ฅผ ๊ฐ€์ ธ์™€์•ผํ•œ๋‹ค.
12
* Reflector.getAllAndOverride() : ํ‚ค์— ๋Œ€ํ•œ ์• ๋…ธํ…Œ์ด์…˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์™€์„œ overrideํ•ด์คŒ
13
*/
14
const requiredRole = this.reflector.getAllAndOverride(
15
ROLES_KEY, //
16
[context.getHandler(), context.getClass()],
17
)
18
19
// Roles Annotation ๋“ฑ๋ก ์•ˆ๋ผ์žˆ์Œ
20
if (!requiredRole) return true
21
22
const { user } = context.switchToHttp().getRequest()
23
24
if (!user) {
25
throw new UnauthorizedException(`ํ† ํฐ์„ ์ œ๊ณต ํ•ด์ฃผ์„ธ์š”!`)
26
}
27
28
if (user.role !== requiredRole) {
29
throw new ForbiddenException(`์ด ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค. ${requiredRole} ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.`)
30
}
31
32
return true
33
}
34
}

app.module.ts ํŒŒ์ผ์— guard๋ฅผ ๋“ฑ๋กํ•œ๋‹ค.

app.module.ts
1
@Module({
2
// ์ƒ๋žต
3
providers: [
4
AppService,
5
{
6
provide: APP_INTERCEPTOR,
7
useClass: ClassSerializerInterceptor,
8
},
9
// ์•„๋ž˜ ์ฝ”๋“œ ์ถ”๊ฐ€
10
{
11
provide: APP_GUARD,
12
useClass: RolesGuard,
13
},
14
],
15
}

4. ๋ชจ๋“  Route ๊ธฐ๋ณธ Private๋กœ ๋งŒ๋“ค๊ณ  IsPublic Annotation ์ž‘์—…

app.module.ts ํŒŒ์ผ์— AccessTokenGuard๋ฅผ ๋“ฑ๋กํ•œ๋‹ค.

app.module.ts
1
@Module({
2
// ์ƒ๋žต
3
providers: [
4
AppService,
5
{
6
provide: APP_INTERCEPTOR,
7
useClass: ClassSerializerInterceptor,
8
},
9
{
10
provide: APP_GUARD,
11
useClass: AccessTokenGuard,
12
},
13
{
14
provide: APP_GUARD,
15
useClass: RolesGuard,
16
},
17
],
18
}

์ด์ œ AccessTokenGuard๊ฐ€ ์ „์ฒด ์ ์šฉ๋˜๋‹ˆ, posts ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ ์šฉํ•œ AccessTokenGuard๋“ค์„ ์‚ญ์ œํ•œ๋‹ค.

๊ทธ๋ฆฌ๊ณ  common/decorator/is-public.decorator.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

common/decorator/is-public.decorator.ts
1
import { SetMetadata } from '@nestjs/common'
2
3
export const IS_PUBLIC_KEY = 'is_public'
4
5
export const IsPublic = () => SetMetadata(IS_PUBLIC_KEY, true)

auth/guard/bearer-token.guard.ts ํŒŒ์ผ์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•œ๋‹ค.

auth/guard/bearer-token.guard.ts
1
@Injectable()
2
export class BearerTokenGuard implements CanActivate {
3
constructor(
4
private readonly authService: AuthService,
5
private readonly usersService: UsersService,
6
private readonly reflector: Reflector,
7
) {}
8
9
async canActivate(context: ExecutionContext): Promise<boolean> {
10
const isPublic = this.reflector.getAllAndOverride(
11
IS_PUBLIC_KEY, //
12
[context.getHandler(), context.getClass()],
13
)
14
15
const req = context.switchToHttp().getRequest()
16
17
if (isPublic) {
18
req.isRoutePublic = true
19
return true
20
}
21
22
// ์ƒ๋žต
23
}
24
}
25
26
@Injectable()
27
export class AccessTokenGuard extends BearerTokenGuard {
28
async canActivate(context: ExecutionContext): Promise<boolean> {
29
await super.canActivate(context)
30
31
const req = context.switchToHttp().getRequest()
32
33
if (req.isRoutePublic) {
34
return true
35
}
36
37
// ์ƒ๋žต
38
}
39
}
40
41
@Injectable()
42
export class RefreshTokenGuard extends BearerTokenGuard {
43
async canActivate(context: ExecutionContext): Promise<boolean> {
44
await super.canActivate(context)
45
const req = context.switchToHttp().getRequest()
46
47
if (req.isRoutePublic) {
48
return true
49
}
50
51
// ์ƒ๋žต
52
}
53
}

์ด์ œ ๋ชจ๋“  API๋ฅผ ๋น„๊ณต๊ฐœ๋กœ ๋ฐ”๊ฟจ๋‹ค. ๊ณต๊ฐœ API๋กœ ๋งŒ๋“ค API๋ฅผ ์ฐพ์•„์„œ IsPublic ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์—ฌ์ฃผ๋ฉด ๋œ๋‹ค.

posts ์ปจํŠธ๋กค๋Ÿฌ์— ๋‹ค์Œ๊ณผ ๊ฐ™์ด IsPublic ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์—ฌ์ค€๋‹ค.

posts.controller.ts
1
// **** (1) GET /posts : ๋ชจ๋“  post ์กฐํšŒ
2
@Get()
3
@IsPublic()
4
getPosts(@Query() query: PaginatePostDto) {
5
return this.postsService.paginatePosts(query)
6
}
7
8
// **** 2) GET /posts/:id : id์— ํ•ด๋‹นํ•˜๋Š” post ์กฐํšŒ
9
// @Param('id') ๋œป : ๊ฐ€์ ธ์˜ค๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ์ด๋ฆ„์€ id์ด๋‹ค
10
@Get(':id')
11
@IsPublic()
12
getPost(@Param('id', ParseIntPipe) id: number) {
13
return this.postsService.getPostById(id)
14
}

auth ์ปจํŠธ๋กค๋Ÿฌ์— ๋‹ค์Œ๊ณผ ๊ฐ™์ด IsPublic ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์—ฌ์ค€๋‹ค.

auth.controller.ts
1
@Post('token/access')
2
@IsPublic()
3
@UseGuards(RefreshTokenGuard)
4
postTokenAccess(@Headers('authorization') rawToken: string) {
5
// ์ƒ๋žต
6
}
7
8
@Post('token/refresh')
9
@IsPublic()
10
@UseGuards(RefreshTokenGuard)
11
postTokenRefresh(@Headers('authorization') rawToken: string) {
12
// ์ƒ๋žต
13
}
14
15
@Post('login/email')
16
@IsPublic()
17
@UseGuards(BasicTokenGuard)
18
postLoginEmail(
19
@Headers('authorization') rawToken: string, //
20
// @Request() req, // ๊ฐ€๋“œ์— ์ƒ์„ฑํ•œ req๋ฅผ ๊ฐ€์ ธ์™€์„œ ์“ฐ๊ธฐ
21
) {
22
// ์ƒ๋žต
23
}
24
25
@Post('register/email')
26
@IsPublic()
27
postRegisterEmail(@Body() body: RegisterUserDto) {
28
return this.authService.registerWithEmail(body)
29
}

5. Public Route ์ •๋ฆฌ

๋ชจ๋“  API๋ฅผ ๋น„๊ณต๊ฐœ๋กœ ๋ฐ”๊ฟจ์œผ๋‹ˆ, ๊ณต๊ฐœ API๋กœ ๋งŒ๋“ค API๋ฅผ ์ฐพ์•„์„œ IsPublic ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์—ฌ์ค€๋‹ค.

common.controller.ts
1
import { Controller, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'
2
import { CommonService } from './common.service'
3
import { FileInterceptor } from '@nestjs/platform-express'
4
5
@Controller('common')
6
export class CommonController {
7
constructor(private readonly commonService: CommonService) {}
8
9
@Post('image')
10
@UseInterceptors(FileInterceptor('image'))
11
postImage(@UploadedFile() file: Express.Multer.File) {
12
return {
13
fileName: file.filename,
14
}
15
}
16
}

๋Œ“๊ธ€ ํŽ˜์ด์ง€๋„ค์ด์…˜, ๋Œ“๊ธ€ 1๊ฐœ ๊ฐ€์ ธ์˜ค๊ธฐ์€ ๊ณต๊ฐœ API๋กœ, ๋‚˜๋จธ์ง€ API๋“ค์€ accessGuard ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ญ์ œํ•œ๋‹ค.

comments.controller.ts
1
@Controller('posts/:postId/comments')
2
export class CommentsController {
3
// ์ƒ๋žต
4
// **** (1) ๋Œ“๊ธ€ ํŽ˜์ด์ง€๋„ค์ด์…˜
5
@Get()
6
@IsPublic()
7
getComments() {} // ์ƒ๋žต
8
9
// **** (2) ํŠน์ • ๋Œ“๊ธ€ 1๊ฐœ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ
10
@Get(':commentId')
11
@IsPublic()
12
getComment(@Param('commentId', ParseIntPipe) commentId: number) {
13
return this.commentsService.getCommentById(commentId)
14
}
15
16
// **** (3) ๋Œ“๊ธ€ ์ƒ์„ฑ
17
@Post()
18
@UseInterceptors(TransactionInterceptor)
19
async postComment() {} // ์ƒ๋žต
20
21
// **** (4) ๋Œ“๊ธ€ ์ˆ˜์ •
22
@Patch(':commentId')
23
@UseGuards(IsCommentMineOrAdminGuard)
24
async patchComment() {} // ์ƒ๋žต
25
26
// **** (5) ๋Œ“๊ธ€ ์‚ญ์ œ
27
@Delete(':commentId')
28
async deleteComment() {} // ์ƒ๋žต
29
}

posts ์ปจํŠธ๋กค๋Ÿฌ์— ์–ด๋…ธํ…Œ์ด์…˜์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•œ๋‹ค.

posts.controller.ts
1
@Controller('posts')
2
export class PostsController {
3
// ์ƒ๋žต
4
5
// **** (1) GET /posts : ๋ชจ๋“  post ์กฐํšŒ
6
@Get()
7
@IsPublic()
8
getPosts(@Query() query: PaginatePostDto) {
9
return this.postsService.paginatePosts(query)
10
}
11
12
// **** 2) GET /posts/:id : id์— ํ•ด๋‹นํ•˜๋Š” post ์กฐํšŒ
13
// @Param('id') ๋œป : ๊ฐ€์ ธ์˜ค๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ์ด๋ฆ„์€ id์ด๋‹ค
14
@Get(':id')
15
@IsPublic()
16
getPost() {} // ์ƒ๋žต
17
18
@Post()
19
@UseInterceptors(TransactionInterceptor)
20
async postPosts() {} // ์ƒ๋žต
21
22
// (4) POST /posts/random : ๋ฌด์ž‘์œ„ ํฌ์ŠคํŠธ๋ฅผ ์ƒ์„ฑ
23
@Post('random')
24
async postPostsRandom() {} // ์ƒ๋žต
25
26
// **** (5) PATCH /posts/:id : id์— ํ•ด๋‹นํ•˜๋Š” post๋ฅผ ๋ถ€๋ถ„ ๋ณ€๊ฒฝ
27
@Patch(':id')
28
patchPost() {} // ์ƒ๋žต
29
30
// **** (6) DELETE /posts/:id : id์— ํ•ด๋‹นํ•˜๋Š” post๋ฅผ ์‚ญ์ œ
31
@Delete(':id')
32
@Roles(RolesEnum.ADMIN)
33
deletePost() {} // ์ƒ๋žต
34
}

users ์ปจํŠธ๋กค๋Ÿฌ์— Roles ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

users.controller.ts
1
@Controller('users')
2
export class UsersController {
3
constructor(private readonly usersService: UsersService) {}
4
5
@Get()
6
@Roles(RolesEnum.ADMIN)
7
getUsers() {
8
return this.usersService.getAllUsers()
9
}
10
}