🎉 berenickt 블로그에 온 걸 환영합니다. 🎉
Back
NestJs
15-Guard

1. Guard 이론 & 구현 스펙 정리

  • 공식문서 : https://docs.nestjs.com/guards
  • Guard란? @injectable 데코레이터된 클래스다.
    • CanActivate 인터페이스를 상속(implements)받는다.
  • 잘못된 요청으로 인한 에러를 막기 위한 Guard를 만들 수 있다.
  • auth/guard/basic-token.guard.ts 파일을 만든다.
basic-token.guard.ts
1
/*** 구현할 기능
2
* 1) 요청객체(request)를 불러오고,
3
* authorization header로부터 토큰을 가져온다.
4
* 2) authService.extractTokenFormHeader를 이용해서
5
* 사용할 수 있는 형태의 토큰을 추출한다.
6
* 3) authService.decodedBasicToken을 실행해서
7
* email과 password를 추출한다.
8
* 4) email과 password를 이용해서 사용자를 가져온다.
9
* authService.authenticateWithEmailAndPassword
10
* 5) 찾아낸 사용자를 (1) 요청 객체에 붙여준다.
11
* req.user = user;
12
*/

2. BasicTokenGuard 구현

Guard를 구현하는 건 Pipe를 구현하는 것과 매우 유사하다. NestJS에서 일부러 이렇게 만들었다.

basic-token.guard.ts
1
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'
2
import { AuthService } from '../auth.service'
3
4
/*** 구현할 기능
5
* 1) 요청객체(request)를 불러오고,
6
* authorization header로부터 토큰을 가져온다.
7
* 2) authService.extractTokenFormHeader를 이용해서
8
* 사용할 수 있는 형태의 토큰을 추출한다.
9
* 3) authService.decodedBasicToken을 실행해서
10
* email과 password를 추출한다.
11
* 4) email과 password를 이용해서 사용자를 가져온다.
12
* authService.authenticateWithEmailAndPassword
13
* 5) 찾아낸 사용자를 (1) 요청 객체에 붙여준다.
14
* req.user = user;
15
*/
16
@Injectable()
17
export class BasicTokenGuard implements CanActivate {
18
constructor(private readonly authService: AuthService) {}
19
20
async canActivate(context: ExecutionContext): Promise<boolean> {
21
const req = context.switchToHttp().getRequest()
22
const rawToken = req.headers['authorization'] // {authorization : 'Basic asdfafa'}
23
24
if (!rawToken) throw new UnauthorizedException('토큰이 없습니다!')
25
26
const token = this.authService.extractTokenFromHeader(rawToken, false)
27
const { email, password } = this.authService.decodeBasicToken(token)
28
const user = await this.authService.authenticateWithEmailAndPassword({
29
email,
30
password,
31
})
32
req.user = user
33
34
return true
35
}
36
}

auth 컨트롤러에 생성한 가드를 추가한다.

auth.controller.ts
1
// auth.controller.ts 생략
2
@Post('login/email')
3
@UseGuards(BasicTokenGuard) // 추가
4
postLoginEmail(@Headers('authorization') rawToken: string) {
5
const token = this.authService.extractTokenFromHeader(rawToken, false)
6
const credentials = this.authService.decodeBasicToken(token)
7
return this.authService.loginWithEmail(credentials)
8
}

포스트맨에 authorization 토큰을 넣지 않는 경우를 확인한다.

guard에 작성한 req가 컨트롤러에 그대로 남는다.

auth.controller.ts
1
// auth.controller.ts 생략
2
@Post('login/email')
3
@UseGuards(BasicTokenGuard)
4
postLoginEmail(
5
@Headers('authorization') rawToken: string, //
6
@Request() req, // 가드에 생성한 req를 가져와서 쓰기(지우거나 주석처리)
7
) {
8
const token = this.authService.extractTokenFromHeader(rawToken, false)
9
const credentials = this.authService.decodeBasicToken(token)
10
return this.authService.loginWithEmail(credentials)
11
}

req가 남아있는 걸 봤다면, 지우거나, 주석처리한다.


3. BearerTokenGuard 구현

auth/guard/bearer-token.guard.ts 파일을 만든다.

auth/guard/bearer-token.guard.ts
1
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'
2
import { AuthService } from '../auth.service'
3
import { UsersService } from 'src/users/users.service'
4
5
@Injectable()
6
export class BearerTokenGuard implements CanActivate {
7
constructor(
8
private readonly authService: AuthService,
9
private readonly usersService: UsersService,
10
) {}
11
12
async canActivate(context: ExecutionContext): Promise<boolean> {
13
const req = context.switchToHttp().getRequest()
14
const rawToken = req.headers['authorization'] // {authorization : 'Basic asdfafa'}
15
16
if (!rawToken) throw new UnauthorizedException('토큰이 없습니다!')
17
18
const token = this.authService.extractTokenFromHeader(rawToken, true)
19
const result = await this.authService.verifyToken(token)
20
21
/** request에 넣을 정보
22
* 1) 사용자 정보 - user
23
* 2) token - token
24
* 3) tokenType - access | refresh
25
*/
26
const user = await this.usersService.getUserByEmail(result.email)
27
req.user = user
28
req.token = token
29
req.tokenType = result.type
30
31
return true
32
}
33
}
34
35
@Injectable()
36
export class AccessTokenGuard extends BearerTokenGuard {
37
async canActivate(context: ExecutionContext): Promise<boolean> {
38
await super.canActivate(context)
39
const req = context.switchToHttp().getRequest()
40
41
if (req.tokenType !== 'access') {
42
throw new UnauthorizedException('Access Token이 아닙니다.')
43
}
44
45
return true
46
}
47
}
48
49
@Injectable()
50
export class RefreshTokenGuard extends BearerTokenGuard {
51
async canActivate(context: ExecutionContext): Promise<boolean> {
52
await super.canActivate(context)
53
const req = context.switchToHttp().getRequest()
54
55
if (req.tokenType !== 'refresh') {
56
throw new UnauthorizedException('Refresg Token이 아닙니다.')
57
}
58
59
return true
60
}
61
}

생성한 가드들을 auth 컨트롤러에 추가한다.

auth.controller.ts
1
// 생략
2
@Controller('auth')
3
export class AuthController {
4
constructor(private readonly authService: AuthService) {}
5
6
@Post('token/access')
7
@UseGuards(RefreshTokenGuard) // 추가
8
postTokenAccess(@Headers('authorization') rawToken: string) {
9
// 생략
10
}
11
12
@Post('token/refresh')
13
@UseGuards(RefreshTokenGuard) // 추가
14
postTokenRefresh(@Headers('authorization') rawToken: string) {
15
// 생략
16
}
17
}
18
19
@Post('login/email')
20
@UseGuards(BasicTokenGuard) // 추가
21
postLoginEmail(
22
@Headers('authorization') rawToken: string, //
23
// @Request() req, // 가드에 생성한 req를 가져와서 쓰기
24
) {
25
// 생략
26
}
27
}

이제 포스트맨에서 확인해본다.