Notice
Recent Posts
Recent Comments
Link
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

개발자가 될래요

Next + Socket.io 로 채팅 구현(6) 본문

프로젝트

Next + Socket.io 로 채팅 구현(6)

Youcan 2024. 8. 30. 17:46

자체 회원가입 기능까지 완성했다.

그럼 로그인을 시도한다.

 

회원가입한 이메일로 로그인을 하고 그 정보를 콘솔로 출력했다.

 

근데 새로고침을 하면 로그인 된 정보가 사라진다.

로그인 유지가 될 수 있도록, Access Token과 Refresh Token을 쿠키에 저장해보자.

 


일단 로그인 상태 유지를 위한 방법으로는 두 가지가 있다.

  • Session id를 발급하는 방법
  • JWT를 발급하는 방법

Session방식의 장단점

장점

  1. 유저의 인증 정보를 서버에서 관리하기 때문에, 보안적으로 좋고 인증 정보를 클라이언트에서 저장하고 관리하는 토큰 방식과 다르게 탈취될 가능성이 적다
  2. 새로고침이나 브라우저 종료 등으로 인한 세션 만료 기간 설정이 가능하다.

단점

  1. 클라이언트가 요청할 때마다 서버에서 세션 정보를 확인해야 하기 때문에, 서버의 부하가 증가 할 수 있다.
  2. 서버의 메모리나 데이터베이스에 세션 정보를 저장하기 때문에, 확장성이 떨어질 수 있다.

 

 

JWT방식의 장단점

장점

  1. 클라이언트 측에서 인증 정보를 저장하고 관리하기 때문에, 세션방식보다 서버의 부하가 적고 서버와의 통신이 감소한다.
  2. 토큰 내에 필요한 정보를 담을 수 있다.

단점

  1. 토큰을 악성 유저에게 탈취 당하면, 탈취한 사람이 해당 유저처럼 서비스 이용을 할 수 있다. 따라서 토큰은 절대 유출이 되지 않을 안전한 방식으로 저장되어야 한다.
  2. 세션 방식과 달리, 토큰 만료 시간 설정이 쉽지 않다. 즉 만료 시간이 지나면, 다시 로그인을 해야한다.

로그인을 하면 사용자 정보를 포함한 JWT access Token과 refresh Token을 발급한다.

 

api/auth/signin/route.ts


    const session = data.session;

    // 액세스 토큰 가져오기
    const accessToken = session?.access_token;
    const refreshToken = session?.refresh_token;

    const response = NextResponse.json(
      { user },
      { status: 200 }
    );

    if (accessToken) {
      response.cookies.set("accessToken", accessToken, {
        httpOnly: true, 
        secure: true, 
        maxAge: 60 * 60,
        sameSite: "lax",
      });

      response.cookies.set("refreshToken", refreshToken, {
        httpOnly: true,
        secure: true,
        maxAge: 60 * 60 * 24 * 7,
        sameSite: "lax",
      });
    }

    return response;

 

이렇게 엑세스토큰과 리프레쉬 토큰을 가져와 쿠키에 저장해주었다.

 

 

각 속성의 의미는 이렇다.

  1. httpOnly : XSS공격을 방지하기 위한 보안 기능으로 클라이언트 측 JavaScript에서 접근할 수 없도록 설정함으로써 쿠키 탈취문제를 예방할 수 있다. 
  2. secure : 쿠기가 HTTPS 연결을 통해서만 전송되도록 '보장'한다. 네트워크를 통한 전송 중에 쿠키가 탈취되지 않도록 하기 위해 사용된다. HTTPS는 데이터가 암호화된 상태로 전송되므로, 공격자가 쿠키를 훔치거나 변조하는 위험을 줄일 수 있습니다.
  3. maxAge : 쿠키의 수명을 초단위로 지정. 쿠키가 유효한 만료 시간을 정함. 설정한 시간이 지나면 쿠키는 자동으로 만료되어 삭제. 
  4. sameSite : 쿠키가 같은 사이트 내에서만 전송되도록 제한. CSRF(교차 사이트 요청 위조) 공격을 방지하기 위해 사용된다. 

sameSite의 요소

  • None : 도메인을 검증하지 않음(타사이트에서 접근이 가능). 즉 모든 크로스 사이트 요청에도 쿠키가 포함될 수 있도록 한다. HTTPS 보안이 필수. 설정시, Lax, Strict가 아닌 값으로 설정시, None으로 처리됨.
    (google.com에서 설정된 쿠키가 있고, 다른 도메인에서(naver.com) AJAX 요청으로 'google.com'에 데이터를 요청하면, 이 요청에 쿠키가 포함됨)

  • Lax : 쿠키가 같은 사이트의 요청과, 일부 제한된 크로스 요청에 대해서만 전송함. 즉. 자사 도메인이 아니더라도, 일부 케이스(링크를 클릭해 이동하는 경우 등)에서는 접근을 허용함.(GET, HEAD, OPTIONS등의 안전한 HTTP 요청은 크로스 요청이 허용되지만, POST, PUT, DELETE 같은 상태를 변경하는 요청시 쿠키 전송이 불가)
    Strict보다는 덜 엄격하며, 사용자 경험과 보안의 균형을 맞춤. 
  • Strict : 쿠키가 같은 사이트의 요청에 대해서만 전송되도록 제한. 보안이 가장 높은 설정으로 사용자가 사이트를 방문하는 도중 발생한 내부 요청에만 쿠키가 포함되도록 함. 즉 현재 브라우저의 URL과 쿠키의 도메인이 일치하는 경우만 사용 가능하다.
    (사용자가 google.com에서 로그인을 하면, 같은 사이트 내에서는 쿠키가 계속 전송되지만, 다른 사이트에서 google.com으로의 링크를 클릭하여 방문할 때는 쿠키가 전송되지 않는다.)


설정을 마친 뒤에 다시 로그인을 해보면

 

로그인을 하면 쿠키에 accessToken과 refesh Token이 저장되었다.(HttpOnly, Secure, SameSite등 설정한 것도 확인이 된다.)

 


access token? refresh token? 무슨 차이지

  • access token의 유효기간은 짧고, refresh token의 유효기간은 길다.
  • 평소에 API통신을 할 때는 access token을 사용하고, refresh token은 access token이 만료되어 갱신될 때만 사용된다.
  • 즉, 통신과정에서 탈취당할 위험이 큰 access token의 만료 기간을 짧게 두고, refresh token으로 주기적으로 재발급함으로써 피해를 최소화한다.

두 토큰이 사용되는 과정을 자세히 살펴보자.

  1. 클라이언트가 로그인 인증에 성공한다면, 서버로부터 Refresh Token Access Token 두 개를 받는다.
  2. 클라이언트는 Refresh Token과 Access Token을 로컬에 저장해놓는다.
  3. 클라이언트는 헤더에 Access Token을 넣고 API 통신을 한다. (Authorization)
  4. 일정 기간이 지나 Access Token의 유효기간이 만료된다.
    4.1. Access Token은 이제 유효하지 않으므로 권한이 없는 사용자가 된다.
    4.2. 유효기간이 지난 Access Token을 받은 서버는 401 (Unauthorized) 에러 코드로 응답한다.
    4.3. 401를 통해 클라이언트는 invalid_token (유효기간이 만료되었음)을 알 수 있다.
  5. 헤더에 Access Token 대신 Refresh Token을 넣어 API를 재요청한다.
  6. Refresh Token으로 사용자의 권한을 확인한 서버는 응답쿼리 헤더 새로운 Access Token을 넣어 응답한다.
  7. 만약 Refresh Token도 만료되었다면 서버는 동일하게 401 error code를 보내고, 클라이언트는 재로그인해야한다.

 

어찌됐든 로그인 유지를 위해 최상위루트에 middleware.ts를 만들어보았다.

 

middleware는 Next에서 페이지를 렌더링 하기 전에 서버 측에서 실행되는 함수이다.

즉, 특정 요청 전에 무언가를 수행할 수 있게 해주는 기능으로 Request 객체와 Response 객체에 접근할 수 있으며 이를 활용해 요청 정보를 받아와 부가적인 처리를 하고 응답 객체를 추가, 변경할 수 있다.

 

middleware.ts


import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@supabase/supabase-js";
import cookie from "cookie";

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

export async function middleware(req: NextRequest) {
  const cookies = req.headers.get("cookie");
  const parsedCookies = cookie.parse(cookies || "");
  const accessToken = parsedCookies.accessToken;

  if (!accessToken) {
    return NextResponse.redirect(new URL("/login", req.url));
  }


  const { data: user, error } = await supabase.auth.getUser(accessToken);

  if (error || !user) {

    return NextResponse.redirect(new URL("/login", req.url));
  }


  return NextResponse.next();
}

// // 특정 경로에만 middleware 적용 (예: "/dashboard" 경로)
export const config = {
  matcher: ["/chat"],
};

 

그리고 로그인 된 상태가 아니라면, chat 페이지를 들어가도 login 페이지로 리다이렉트 되도록 설정했다.

 

 

 

 

그리고 채팅창에서 기존의 직접 ID를 입력하는 칸을 지우고, 현재 로그인한 유저의 닉네임이 들어가도록 코드를 수정했다.

 

현재 로그인한 아이디의 닉네임으로 채팅이 잘 쳐지는것을 볼 수 있다.