원티드 온보딩: 세션기반 로그인 구현과 JWT와의 비교 및 인프라스트럭쳐

윤뿔소·2023년 3월 13일
0

Wanted

목록 보기
3/4

전 시간대에는 로컬스토리지를 사용해 JWT을 받아오고, JWT를 사용해 다른 페이지에 유저정보를 가져오는 방식을 배워봤다.

이번 시간엔 세션을 비롯한 로그인 기술에 관련해서 배워보자

세션이란?

세션은 다양한 곳에 쓰일 수 있다.

일정 시간동안 같은 사용자로 부터 들어오는 일련의 요구를 하나의 상태로 보고 그 상태를 일정하게 유지시키는 기술

이 강의는 로그인 관련에 한해서 설명하기에 좀더 축약하면

'사용자의 로그인 이후 로그아웃 혹은 로그인 만료까지의 기간'

즉, 세션을 활용한 로그인 방식은

사용자 로그인이 유효한 시간 동안 서버에 세션 아이디를 기록해 두고 인증에 사용하는 방식
사용자가 로그인하고 로그아웃 할때 까지의 시간

이라는 것.

여기서 쿠키도 같이 써 저장소처럼 활용하여 로그인을 할 수 있다.

쿠키란?

이렇게 세션을 통해 로그인 권한을 부여받을 수 있다면 서버가 쿠키로 보낼 것이다.

Set-Cookie: connect.sid=s%3A2TLZZGfYYQ1zC9K6KaykGrP41cshrpqu.SRyHdHZIPwncLOIan6G0VTQSWBqUon pemtB2w5LOuw; Path=/; Expires-Mon, 13 Mar 2023 05:01:52 GMT; HttpOnly; SameSite=Lax

이러한 문자열로 서버는 쿠키에 넣어줘 토큰처럼 활용할 수 있다는 것! 브라우저도 쿠키를 가져와 서버에 보내줄 수 있다. 요청 시 헤더에 담아!

쿠키 / 세션 차이

CookieSession
저장위치ClientServer
저장형식TextObject
만료시점쿠키 저장시 설정
(설정 없으면 브라우저 종료 시)
정확한 시점 모름
리소스클라이언트의 리소스서버의 리소스
용량제한한 도메인 당 20개, 한 쿠키당 4KB제한없음

즉, 세션으로만 쓴다면 서버 부하가 커져서 쿠키도 같이 저장소로 활용하며 서버 부담을 줄여줘야한다.

하지만 쿠키는 아무나 가져다 쓸 수 있는 이랏샤이마세 라서 보안성이 똥이기 때문에 보안 설정을 해줘야한다. 그것도 보자!

동일 출처

웹 구성 요소들이 같을 때만 동일 출처라 볼 수 있다.

프로토콜 : http와 https
도메인 : domain.com과 other-domain.com
포트 번호 : 8080포트와 3000포트

등등

CORS

동일 출처을 검증하기 위해 사용하는 방법 중 이러한 정책이 나왔다.

Cross-Origin Resource Sharing
추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제

동일 출처 원칙을 지키지 않아 뜨는 오류로 아주 악명 높은 놈이다. 그러면 프론트에선 어떻게 해야할까?

해결점

바로 해결할 수 있는 기술은 없다. ㅋㅋ;; HTTPS 설정을 하든가 해야된다. 아니면 확장프로그램을 깔던가

$ open -na Google\ Chrome http://localhost:5173/
 --args --disable-web-security --user-data-dir=/tmp/chrome_dev"

이런 식으로 로컬에서 입력해 CORS 검증 해제를 하는 수 밖에 없다. 백엔드가 CORS 허락을 해주기 전까지 이렇게 땜빵해 개발을 이어가자!

참고: Node.js로 쓴 CORS 관련 설정 코드

쿠키 관련 정책 지정 및 httpOnly 설정 등

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(cookieParser());

  app.enableCors({
    origin: /^(http:\/\/localhost:[0-9]{4})|(http:\/\/127.0.0.1:[0-9]{4})$/,
    methods: ['GET', 'POST', 'OPTIONS'],
    credentials: true
  });

  app.use(session({
    secret: '원티드 3월 FE 프리온보딩 코스 실습용 예제',
    resave: false,
    saveUninitialized: true,
    cookie: {
      maxAge: 24 * 6 * 60 * 10000,
      sameSite: 'lax',
      httpOnly: true
    },
  }));

  app.use(passport.initialize());
  app.use(passport.session());

  await app.listen(4000);
}

SameSite(None, Lax, Strict), httpOnly, Secure(HTTP 설정)

과제

과제로 세션기반 로그인 호출하기 & 라우터 등록을 하고, 로그인 상태기반 페이지 분기 & 확인을 진행해 과제를 수행했다.

핵심

하나 핵심을 적어본다. 오늘 너무 많은 양을 했고 어제 한거랑 다 비슷해서 핵심만 기록하겠다.

로그인을 하면 쿠키에 토큰을 준다. 그 토큰을 준다면 사이드바의 페이지 A, B, C를 이동할 수 있는 권리를 준다. 그 전에 사이드바의 메뉴를 보여주지 않는 과정을 적어보겠다.

기본 재료

사이드바의 메뉴를 보여주지 않기 전에 라우팅부터 설정한다. 이번 과제에서 remix 라이브러리를 써서 라우팅을 한다. 그리고 Link 방식이 아닌RouterProvider - createBrowserRouter로 라우팅했다.

코드 풀이

import { SidebarElement } from "./types/sidebar";
import { createBrowserRouter } from "react-router-dom";
import { Router as RemixRouter } from "@remix-run/router/dist/router";
import GeneralLayout from "./layout/GeneralLayout";
import Home from "./pages/Home";
import Login from "./pages/Login";
import PageA from "./pages/PageA";
...

interface RouterElement {
  id: number; // 페이지 아이디 (반복문용 고유값)
  path: string; // 페이지 경로
  label: string; // 사이드바에 표시할 페이지 이름
  element: React.ReactNode; // 페이지 엘리먼트
  withAuth?: boolean; // 인증이 필요한 페이지 여부
}

const routerData: RouterElement[] = [
  // TODO 3-1: 로그인 페이지 라우터 등록하기 ('login', withAuth: false)
  // TODO 3-2: page a, b, c 등록하기
  {
    id: 0,
    path: "/",
    label: "Home",
    element: <Home />,
    withAuth: false,
  },
  {
    id: 1,
    path: "/login",
    label: "Login",
    element: <Login />,
    withAuth: false,
  },
  {
    id: 2,
    path: "/page-a",
    label: "PageA",
    element: <PageA />,
    withAuth: true,
  },
  ...
];

// TODO 3-1: 인증이 필요한 페이지는 GeneralLayout으로 감싸서 라우터에 전달
// GeneralLayou에는 페이지 컴포넌트를 children으로 전달
export const routers: RemixRouter = createBrowserRouter(
  routerData.map((router: RouterElement) => {
    if (router.withAuth) {
      return {
        path: router.path,
        element: <GeneralLayout>{router.element}</GeneralLayout>,
      };
    } else {
      return {
        path: router.path,
        element: router.element,
      };
    }
  })
);

// TODO 3-2: 라우터 객체에서 인증이 필요한 페이지만 필터링해 사이드바에 전달
// id, path, label을 전달하여 Sidebar에서 사용
export const SidebarContent: SidebarElement[] = routerData.reduce((prev, router) => {
  if (router.withAuth) return prev;

  return [
    ...prev,
    {
      id: router.id,
      label: router.label,
      path: router.path,
    },
  ];
}, [] as SidebarElement[]);

나는 Routes / RouteLink로만 했었는데 RouterProvider - createBrowserRouter로 라우팅을 하는데 router.tsx라는 라우터 전용 파일도 만들어서 따로 이렇게 관리하니 훨씬 깔끔했다. 나 다음엔 이렇게 해야겠다고 생각했다. 이거 방식 너무 좋다. Link도 고차함수로 사용할 수 있지만 이 방식이 더 깔끔했다. 또 공통레이아웃 컴포넌트인 GeneralLayout이 있어서 또 사이드바, 헤더 등을 관리하기 더 편했다.

쨌든 핵심은 router(s)라는 객체를 만들면서 타입이 불린데이터인 withAuth라는 변수를 만들며 SidebarContent를 이용해서 고차함수 reduce를 이용해 withAuth을 조건으로 이용해 권한이 없다면 아예 안뜨게 한 것이다. 내가 프로젝트를 할 때는 클릭하면 로그인 되게 리다이렉트가게 만들었는데 이렇게 그냥 안보여주면 훨씬 나았을 거 같다. 배웠다.

결과물

이렇게 쿠키에 저장되고, 쿠키로 저렇게 저장이 된다. 물론 HTTPOnly다!

위 과제의 순서이다. 세션으로 유효성 검사를 해 로그인이 되면 권한을 주고, 그 이후는 쿠키를 이용해 세션 id를 주고받으며 서버의 부하도 줄이는 모습도 볼 수 있다.

그 이후 로그아웃이나, 권한 관리를 통해 열심히 먹어보자.

결론

JWT와 세션+쿠키를 보면서 로그인 방식에 대한 기술에 대해 알아봤다.

각 장단점은 이렇다.

  • JWT
    • 서버/백엔드 비용 감소: 토큰 하나로 모든 걸 해결!
    • 프론트엔드 복잡도 높아짐: 프엔이 구현해야할 기술이 세+쿠보다 많음
    • 보안상 세션보다 조금 더 위험: 토큰 하나로 해결해야하니 뚫리면 속수무책
  • 세션+쿠키
    • 서버/백엔드 비용 대폭 증가: 세션을 통해 서버와 통신하는 횟수 증가
    • 프론트엔드 인증 쉬워짐: 프엔은 쉬운^^
    • 보안상 약간의 향상: 서버와 통신하니 보안을 거쳐야하는 단계 증가

결국 완벽한 점은 없고, 보안이란 것은 항상 어느 것이든 보완적이기 때문에 동시접속자 수, 서비스 규모, 앱/웹 동시운용 여부, 팀 내 인력구성, 일정 등에 따라 상황에 맞게 만들면 된다.

진리의 상황 파악!

profile
코뿔소처럼 저돌적으로

6개의 댓글

comment-user-thumbnail
2023년 3월 14일

마지막 결론까지 깔끔합니다! 노드로 설정한건 새롭네요 ㅎㅎ

답글 달기
comment-user-thumbnail
2023년 3월 19일

createBrowserRouter로 라우팅하는 방식이 신기하네요..! 저도 나중에 공부해서 사용해봐야겠어요 😄

답글 달기
comment-user-thumbnail
2023년 3월 19일

와 깔끔한 정리 잘 보고 갑니다 알찬 정보가 가득하네용 ㅎㅎ

답글 달기
comment-user-thumbnail
2023년 3월 19일

뭐든 정답은 없군요!

답글 달기
comment-user-thumbnail
2023년 3월 19일

쿠키 세션 차이 잘 보고 갑니다 !

답글 달기
comment-user-thumbnail
2023년 3월 19일

JWT 궁금했는데, 깔끔한 정리네요!

답글 달기