본문 바로가기
Back-end/nest.js

Nest.js : Nest.js로 Redis와 연동하여 세션 로그인 구현하기 (2)

by hsloth 2023. 2. 23.

 

이번 글은 Nest.js에서 Swagger를 사용하여 로그인 인증을 하는 방법을 찾으면서 쓴 글이다!

 

JWT를 사용할 때에는 ApiBearerAuth를 추가해서 Login해서 나오는 토큰값을 추가해주면 되었는데, 세션은 어떤식으로 해야할지 감이 잡히지 않았다. 그래서 세션은 정확히 어떤식으로 동작하는지 공부할 필요가 있었다.

 

우선, 세션은 Cookie를 이용해서 정보를 주고 받는다.

클라이언트에서 로그인 요청을 보내면, 서버 측에서 session Id를 클라이언트에 전달하고 클라이언트에서는 session Id를 쿠키에 저장해서 보관한다.

그 후, 클라이언트는 서버에게 요청을 보낼 때 마다 HTTP 헤더의 Cookie에 해당 session Id를 넣어 함께 전달한다.

이것을 서버가 받으면 해당 session Id가 있는지 서버측에서 확인을 하는 것으로 로그인 여부를 판단한다.

 

자, 일단 Swagger 부터 설정해보자.

import { INestApplication } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

// init.swagger.ts
export function setUpSwagger(app: INestApplication): void {
  const options = new DocumentBuilder()
    .addCookieAuth('connect.sid')
    .setTitle('Test API docs')
    .setDescription('테스트 문서입니다.')
    .setVersion('0.0.1')
    .build();

  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api', app, document);
}

// main.ts
async function bootstrap() {
	...
    setUpSwagger(app);
    ...
}
bootstrap();
  • DocumentBuilder에 addCookieAuth('connect.sid')를 추가한다. 여기서 connect.sid는 고유 식별자이다. express-session은 디폴트로 connect.sid 를 식별자로 사용하기 때문에 따로 설정해주지 않았다면, connect.sid 그대로 적자.

그 후, 로그인 여부를 판단하기 위해 LoggedInGuard를 생성한다. (IsAuthenticatedGuard 등 이름은 마음대로 짓자)

// auth/logged-in.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';

@Injectable()
export class LoggedInGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest() as Request;

    return request.isAuthenticated();
  }
}
  • 여기서 request.isAuthenticated() 함수를 통해 로그인이 되어 있는지 알 수 있다.
  • request.isAuthenticated()는 boolean을 리턴하는데, true면 로그인이 되어있다는 것을 나타낸 것이고, false면 로그인이 되어있지 않다는 말이다.
  • 그리고, request.isAuthenticated() 를 사용하기 위해서는 session 설정시에 app.use(passport.initialize()) 를 해주어야한다.
    • 이 부분은 아래 session 설정에서 설명한다.

그리고 컨트롤러에서 로그인 함수와 로그인 확인을 위한 함수를 작성한다.

// auth.controller.ts

  // 로그인
  @UseGuards(LocalAuthGuard)
  @Post('login')
  async logIn(@Users() user, @Session() session) {
    console.log(session.id);
    return { user, sessionId: session.id };
  }
  
  // 로그인이 되어 있을 경우 hi를 리턴하는 함수, 로그인이 되어있지 않으면 403 forbidden 이 발생한다.
  @ApiCookieAuth()
  @UseGuards(LoggedInGuard)
  @Get('hi')
  async hi() {
    return 'hi';
  }
  • @Session() 은 @nestjs/common에서 import 해와야 한다.
    • req.session을 불러오는 데코레이터이다.
    • 처음에는 console.log(session) 을 찍었을 때, id값이 안나온다. 하지만, session.id에 세션 아이디가 담겨있으니 안심하자.
  • 그리고 임시로 hi라는  함수를 만들어서 LoggedInGuard를 적용해보자.
  • ApiCookieAuth() 를 달아서 해당 api를 스웨거에서 실행할 때, HTTP 헤더에 Cookie 값이 들어갈 수 있도록 하자.

 

여기까지 했는데, 나는 로그인이 되지 않고 계속 403 Forbidden이 떴다. 그래서 알아본 결과... session 세팅에 문제가 있다는 걸 깨달았다.

// init.session.ts
import { INestApplication } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as session from 'express-session';
import * as connectRedis from 'connect-redis';
import { Redis } from 'ioredis';
import * as passport from 'passport';

export function setUpSession(app: INestApplication): void {
  const configService = app.get<ConfigService>(ConfigService);

  const port = configService.get('REDIS_PORT');
  const host = configService.get('REDIS_HOST');

  const RedisStore = connectRedis(session);

  const client = new Redis({
    host,
    port,
  });

  app.use(
    session({
      secret: configService.get('SESSION_SECRET'),
      saveUninitialized: false,
      resave: false,
      store: new RedisStore({
        client: client,
        ttl: 30,
      }),
      cookie: {
        httpOnly: true,
        secure: false,
        maxAge: 1000 * 60 * 30, // 쿠키 유효기간: 30분
        path: '/',
      },
    }),
  );

  app.use(passport.initialize());
  app.use(passport.session());
}
  • 일단, 다른건 보지말고 app.use(session({ secret : ... })) 이 부분을 봐보자.
    • 나는 cookie의 secure 부분이 true로 되어 있어서 안되었던 것이었다. secure : true 로 설정을 하면 https 프로토콜만 적용이 되어서 로컬에서 실행하는 http 프로토콜에는 적용이 되지 않는다.
  • 그리고 app.use(passport.initialize()) 가 있어야 req.isAuthenticated() 함수가 동작하므로 꼭 써주자!

 

Swagger

이제 DB에 테스트 유저를 미리 하나 등록해놓고, 서버를 실행시킨 후, Swagger에 들어가자.

일단 먼저, 로그인 api를 이용하여 세션 ID를 얻자.

세션 ID를 얻은 후 Authorize에서 해당 세션 ID를 입력한다.

로그인 후

미리 만들어놨던 로그인 여부를 테스트할 함수를 실행시키면, Curl 부분에 Cookie: connect.sid=세션ID 가 자동으로 입력되는걸 볼 수 있다. 그리고 Response body에 hi가 정상적으로 출력된다.

로그인 되지 않았을 때 뜨는 화면

회고

하다보니 실수한 게 있다.

- 어차피 Redis를 이용해서 세션 로그인을 하는 거면, 브라우저 자체의 cookie에 session Id가 자동적으로 들어가서 굳이 addCookieAuth가 필요 없다.

- Redis를 연동하긴 했는데, Redis를 연동 하지 않고 서버를 실행시켜서 세션 아이디를 얻는 방법을 생각해 봐야겠다 (Redis 의존적이지 않게 swagger에서 로그인이 되도록 해봐야겠다는 뜻이다)

- 혹은 Docker를 이용해서 그냥 addCookieAuth 없이 컨테이너를 파서 테스트용 Redis를 컨테이너로 가동시켜서 실제 Redis는 건드리지 않도록 해도 될 것 같다.

 

혹시 세션 로그인을 사용할 때, 레디스와 스웨거를 어떤식으로 처리하는지 (분리하는지) 아시는 분 있으면 답변 주시면 감사하겠습니다.