본문 바로가기
source-code/Next JS

[next js] app directory에서 token 저장하기 - (2)

by mattew4483 2024. 8. 5.
728x90
반응형

Backgrounds

2024.02.13 - [source-code/Next JS] - [next js] app directory에서 token 저장하기

 

[next js] app directory에서 token 저장하기

next js 13 버전에서 app directory가 등장하면서, app directory내 모든 컴포넌트들은 기본적으로 server component로 동작하게 되었습니다. https://nextjs.org/docs/app/building-your-application/rendering/server-components Renderi

23life.tistory.com

결론적으로, 아래 방법을 사용해 app directory에서 token을 관리하기로 했습니다.

1) server action을 통해 server에서 전달받은 token을 cookie에 저장한다.
2) middleware에서 cookie를 조회, 로그인 여부를 판단한다. (자동 로그인)

 

Solutions

1. server action을 통한 cookie 내 token 저장

현재 로그인 api 호출 시, server 측에서는 set-cookie 대신 응답 데이터로 token을 전달하고 있습니다.

export async function loginApi() {
  const res = await fetch(`/login`, {
    method: "POST",
  });

  // data가 {accessToken:string, refreshToken:string}으로 정의된 상황
  const data = await res.json();
}

FE 측에서 반환받은 accessToken과 refreshToken을 cookie에 설정하고 싶은 상황!

 

document.cookie = `accessToken=${accessToken}`;

물론 위와 같이 작성할 수 있기는 하지만

해당 cookie값을 자바스크립트에서 접근하지 못하게 하기 위해서는(보안 취약점 방지) HttpOnly 옵션을 설정해야 하는데,

HttpOnly은 서버에서만 설정 가능하며, FE에서는 직접 설정할 수 없습니다.

 

이를 해결하기 위해 server action을 사용, BE에서 반환받은 token을 next js 서버에서 설정해 줬습니다.

https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations

 

Data Fetching: Server Actions and Mutations | Next.js

Learn how to handle form submissions and data mutations with Next.js.

nextjs.org

// utils/cookie.ts
import { cookies } from "next/headers";

export function setCookie(
  key: string,
  value: string,
  options?: Partial<ResponseCookie>
) {
  return cookies().set(key, value, options);
}

next/headers의 cookies API를 사용한 유틸 함수를 생성한 뒤,

 

"use server";

import { setCookie } from "../../utils/cookies";

export async function loginApi() {
  const res = await fetch(`/login`, {
    method: "POST",
  });

  const { accessToken } = await res.json();

  setCookie("accessToken", accessToken, {
    httpOnly: true,
    secure: true,
    maxAge: 60 * 60 * 24 * 14, // 14d,
  });
}

loginApi를 server action으로 만들어, cookie를 설정해 준 모습!

next js 서버에서 해당 로직이 실행되므로 다른 쿠키 옵션(secure, maxAge 등)도 문제없이 지정할 수 있습니다.

 

그렇다면, token이 필요한 다른 API들을 요청할 때는 어떻게 해야 할까요?

// utils/cookie.ts
import { cookies } from "next/headers";

export function getCookie(key: string) {
  return cookies().get(key)?.value;
}

마찬가지로 cookie 관련 util 함수를 만든 후

"use server";

import { getCookie } from "@/utils/cookies";

export async function getDataApi() {
  const accessToken = getCookie("accessToken"); // cookie에서 token값 조회
  const res = await fetch(`/data`, {
    method: "GET",
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  });

  const data = await res.json();
  return data;
}

server action을 통해 cookie에서 token 값을 조회한 뒤, 원하는 곳에서 사용해 주면 되겠죠!

 

2. middleware를 통한 자동 로그인

사용자 token이 cookie에 저장되어 있으므로,

이제 모든 서버 컴포넌트에서 cookie 내 사용자 token의 존재 여부를 판단하는 것으로 인증 로직을 처리할 수 있겠죠!

 

이때 인증 로직을 처리한다... 는

1) 인증되지 않은 사용자는, 로그인 페이지로 이동

2) 인증된 사용자는, 서비스 이용 할 수 있음을 뜻합니다.

→ next js middleware를 통해 간편하게 구현할 수 있습니다!

 

https://nextjs.org/docs/pages/building-your-application/routing/middleware

 

Routing: Middleware | Next.js

Learn how to use Middleware to run code before a request is completed.

nextjs.org

 

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
  const accessToken = request.cookies.get('accessToken')?.value;
  const refreshToken = request.cookies.get('refreshToken')?.value;

  // cookie내 token관련 값이 존재하는지로, 인증 여부 판단 가능
  const isAuthorized = accessToken === undefined || refreshToken === undefined
  if (!isAuthorized) {
    return NextResponse.redirect(new URL("/sign-in", request.url));
  }
  
  return NextResponse.next();
}

middleware 역시 next 서버에서 실행되므로, httpOnly인 cookie 값에도 접근이 가능한 모습!

이를 통해, 로그인 시 저장한 token관련 값이 cookie에 존재하는지로 인증 여부를 판단할 수 있습니다.

인증되지 않은 사용자는 → NextResponse의 redirect 메서드를 통해, 로그인 페이지로 강제 이동시켜 주면 되겠죠.

 

그런데, 요구사항을 잘 살펴보면...

이를 반대로 말하자면  사용자 토큰이 만료되면 로그인 페이지로 강제 이동되어야 한다는 뜻!

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
  /// (...)
  
  // 사용자 토큰이 만료된 경우
  if (isExpired) {
    // 로그인 페이지로 redirect
    const response = NextResponse.redirect(
      new URL("/sign-in", request.url)
    );
    return response;
  }
}

 

이때 단순히 페이지만 이동시켜 버리면,

로그인 페이지 접속 시, 만료 여부 검사가 실행되고, 만료된 경우 다시 로그인 페이지로 이동하고, 만료 여부 검사가 실행되고 가 반복되면서

무한 redirect 오류가 발생하고 맙니다.

 

이를 방지하기 위해서는, 쿠키를 삭제하면 되겠죠.

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
  /// (...)
  
  // 사용자 토큰이 만료된 경우
  if (isExpired) {
    // 로그인 페이지로 redirect
    const response = NextResponse.redirect(
      new URL("/sign-in", request.url)
    );
    
    // 해당 시점에서 response.cookies에는, 아무 값도 존재하지 않습니다!
    response.cookies.delete('accessToken')
    return response;
  }
}

하지만 middleware에서 redirect를 위해 생성된 Response 객체에는, 어차피 cookie가 존재하지 않습니다.

즉 위 코드처럼 delete 메서드를 작성해도... redirect 된 페이지의 request에는 만료된 token이 계속해서 실리게 되는 것이죠.

 

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
  /// (...)
  
  // 사용자 토큰이 만료된 경우
  if (isExpired) {
  	// 로그인 페이지로 redirect
    const response = NextResponse.redirect(
      new URL("/sign-in", request.url)
    );
    
    // maxAge 0 token을 set해줌으로써 cookie 삭제
    response.cookies.set('accessToken', "", {
      maxAge: 0,
    });
    response.cookies.set('refreshToken', "", {
      maxAge: 0,
    });
    return response;
  }
}

 

cookie의 max-age를 0으로 설정해, redirect된 페이지에서 더 이상 cookie가 설정되지 않게 해주면 됩니다!

 

728x90
반응형