웹서버에서 중요한 것 중의 하나는 보안 관련한 로직일 것이다. NestJS에서는 허용된 User가 아니면 query나 Mutation, Subscription을 요청하지 못하도록 하는 Middleware를 제공한다. 이것을 UseGuard라는 decorator로 제공하고 있다. 이번에 Query, Mutation, Subscription에 관해서 Guard를 제작한 경험을 작성하고자 한다.
Guard 개념
Guard 개념은 간단하다. 허용된 유저가 아니면 요청 자체를 막아버리는 것이다. 예를 들어 모든 사용자가 서버에 요청을 할 수 있다면, DDos 같은 엄청난 트래픽이 들어올 때 모든 요청에 대하여 응답을 하게 될 것이다. 이러한 상황이 지속되면 서버의 자원에 낭비가 올 수 밖에 없으며, 결국에는 서버 다운 같은 치명적인 문제로 다가올 것이다.
위와 같은 불상사를 막기 위해서 HTTP Header에 User의 정보가 담긴 Token을 보내면, 서버의 Middleware단에서 유요한 유저인지 판별하여 불필요한 자원 낭비를 막게 한다. 유요한 유저일 경우에는
Guard는 decorator 문법을 사용하며, 현재 ts에서는 실험 기능에 포함되어 있다. (tsconfig.json에 experimentalDecorator : true 옵션 주면 사용이 가능함.) 하지만 직관적인 코드 덕분에 나름 쓸만하다고 생각한다.
여하튼, UserGuard 사용에 대한 설명은 NestJS 공식 문서에 자세히 나와 있다. 나의 경우에는 GraphQL을 이용할 것이기 때문에 조금 다르게 적용을 할 예정이다.
https://docs.nestjs.com/guards
https://docs.nestjs.com/security/authentication
로직 구상
기본 로직 구상은 다음과 같다.
회원 가입 -> 유저와 1대1 대응하는 token 생성 -> User가 서버에 요청을 날릴 때 HTTP Header에 token 담아서 날림 -> Guard에서 유효한 token인지 확인 -> 유효하다면, 서버는 요청에 응답함.
Guard 제작하기
일단은 회원가입 시에 user와 상응하는 token을 제작해야 한다. token 제작은 어렵지 않다. nestjs에서 기본적으로 제공하는 jwtService, jwtModule을 이용하면 decode와 sign은 바로 제작이 가능하다. (sign은 입력->token, decode는 token->입력) 아래 링크를 접속하면 자세히 설명이 되어있다.
https://docs.nestjs.com/security/authentication#jwt-functionality
그러므로 회원가입 로직을 제작할 때 token을 만들어 주면 user에 상응하는 token을 제작할 수 있다. 이제 서버에 요청을 할 때 header에 이 정보를 담아주면 된다. (전송할 때 {"Autorization" : "Bearer 토큰"} 형식으로 보내줘야 한다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// gql-guard.ts
@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
// user.resolver.ts
@Resolver()
export class UserResolver {
constructor(private userService: UserService) {}
@UseGuards(GqlAuthGuard)
@Query((_) => Boolean, { description: '테스트 쿼리' })
getBoolean(@Args('input', { type: () => Boolean }) input: boolean) {
return this.userService.getBoolean(input);
}
}
|
cs |
공식문서를 참고한 graphql에서 Guard를 사용한 모습이다. 위와 같은 형태로 작성하면 Query나 Mutation은 커버가 가능하다. 하지만 Subscription은 얘기가 다르다. 그거는 차차 알아보자.
WebSoket, Subscription 대응하기
일단은 공식문서를 한번 보고 오자.
https://docs.nestjs.com/graphql/subscriptions
http 요청에는 여러 가지가 있지만, post 방식은 입력이 있어야 출력이 있는 관계이다. 즉 내가 아무것도 안하면 어떠한 요청도 받지 못한다는 것이다. 하지만 실시간으로 정보를 받아야 하는 방식이라면 애기가 다르다. 한번 요청하면 지속적으로 응답이 와야 하는데, 이러한 기술은 webSocket이라는 기술에 기반한다. 간단히 말하면 한번 요청을 보내면 서버의 상태가 바뀔 때마다 (물론 적절한 세팅을 해야한다.) 응답이 오게 된다. 즉 '실시간 응답' 구현이 가능하다는 것이다. nestjs는 subscription에 이러한 기능을 담아 두었다. 이것을 이용하면 채팅 서버 같은 실시간 응답 서버 구현이 가능하다.
이제부터 socket 통신에 대한 응답을 분석할 에정이다. 공식 문서에서는 GraphqlModule의 옵션에 반환값을 주어야 한다고 나온다. 여기서는 connection의 옵션으로 볼 수 있다고 나온다.
1
2
3
4
5
6
7
8
9
10
11
|
GraphQLModule.forRoot({
autoSchemaFile: true,
installSubscriptionHandlers: true,
subscriptions: {
'subscriptions-transport-ws': {
onConnect: (connectionParams) => {
return connectionParams;
},
},
},
}),
|
cs |
app.module.ts 파일 안의 GraphModule의 옵션을 다음과 같이 정해준다. 예전 같으면 context를 확인하겠지만, NestJS와 ApolloServer는 친하지만 그렇게 사이가 좋지는 않아 보인다. ApolloServer가 3 버전으로 올라 가면서 위와 같이 바뀌었다. 아래 링크를 참고하면 좋을 것 같다.
https://www.apollographql.com/docs/apollo-server/data/subscriptions/#enabling-subscriptions
여기서 connectionParams를 log로 찍어 보면 {"Autorization" : "Bearer 토큰"} 가 보일 것이다. 이제 이것은 GqlAuthGuard로 넘어갈 것이다. 이제 넘어간 토큰을 파싱 후 유효성 검사를 하는 일만 남았다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Injectable()
export class GqlAuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const gqlContext = GqlExecutionContext.create(context).getContext();
// res (query, mutation logic)
if (gqlContext?.Authorization) {
// gqlContext.Authorization.toString().replace('Bearer ', '') 이 유효한 토큰인지 확인
}
// connection (subscription logic)
// gqlContext.req.headers.authorization.toString().replace('Bearer ', '') 이 유효한 토큰인지
}
}
|
cs |
GqlAuthGuard를 다음과 같이 바꾸었다. canActive 메소드에서는 입력받은 정보를 이용해서 유효한 토큰인지 판별 후 boolean 값을 반환한다. 당연하겠지만, true는 접속 허용이고 false는 접속 비허용이다. 내가 작성한 코드에서는 galContext에 req 또는 Autorization이 있는지로 확인하지만, 가장 확실한건 gqlContext를 log 찍어서 토큰이 있는 곳을 직접 확인해서 추출한 후에 token의 유효성을 판별하는 것이다. 사용자마다 토큰 판별법은 다를 수 있으므로 주석으로 남겨 두었다. 나의 경우에는 jwtService.decode 함수를 통해서 추출한 정보를 이용했다. false 처리에 대해서 조금 더 확실하게 하고 싶으면 UnautorizeException() 을 사용하면 된다. 여기서 false를 반환하게 된다면 인증되지 않은 사용자이므로 nestjs에서 제공하는 에러를 이용해서 어떠한 로그인지 알 수 있게 하자.
후기
로직 작성에 며칠을 소모했는지 모르겠다. 가장 난해했던 것은 header와 요청에 대한 정보였다. 나의 기초 지식이 부족했던 것도 시간을 소모한 것에 한 몫 했다고 생각한다. 다행히도 인증 로직 제작에 성공했지만, 아직은 갈 길이 멀어 보인다. 서버 개발은 계속 나를 생각하게 만든다.
'프레임워크 > NestJS' 카테고리의 다른 글
[NestJS] Prisma db 여러개 연결하기 (0) | 2022.01.08 |
---|---|
[NestJS] DB 캐시 Redis 사용하기 (0) | 2021.12.16 |
[NestJS] 네이버 문자인증 로직 제작하기 (0) | 2021.11.06 |
[NestJs] PayloadTooLargeError: request entity too large 해결하기 (0) | 2021.10.27 |
[NestJs] 환경변수 설정하기 (0) | 2021.10.18 |