코딩/React

Supabase로 로그인 구현하기 with NextJS 6편

드리프트 2022. 1. 7. 22:18
728x170

 

안녕하세요?

 

이번 시간에는 Supabase로 구현한 유저 로그인 상태에 대한 보안적인 측면에 대해 알아보겠습니다.

 

전편 링크는 아래와 같습니다.

 

https://cpro95.tistory.com/617

 

Supabase로 로그인 구현하기 with NextJS 1편

안녕하세요? 최근 NextJS로 여러가지 로그인 구현 웹앱을 만들려고 노력하고 있는데요. 최근에는 NextAuth와 Prisma를 이용해서 카카오 로그인, 네이버 로그인, 구글 로그인 등 다방면으로 구현해 봤

cpro95.tistory.com

https://cpro95.tistory.com/618

 

Supabase로 로그인 구현하기 with NextJS 2편

안녕하세요? 이번 시간에는 지난 회부터 시작한 Supabase를 이용한 로그인 구현 2편을 이어나가도록 하겠습니다. 1편은 아래 링크 참조바랍니다. https://cpro95.tistory.com/617 Supabase로 로그인 구현하기 w

cpro95.tistory.com

https://cpro95.tistory.com/619

 

Supabase로 로그인 구현하기 with NextJS 3편

안녕하세요? 지난 시간에 이어 Supabase 서비스를 이용한 NextJS 로그인 구현을 이어 나가도록 하겠습니다. 1, 2편 링크는 아래를 참조해 주시기 바랍니다. https://cpro95.tistory.com/617 Supabase로 로그인 구.

cpro95.tistory.com

https://cpro95.tistory.com/620

 

Supabase로 로그인 구현하기 with NextJS 4편

안녕하세요? Supabae 로그인 구현 4편입니다. 전편 링크는 아래와 같습니다. https://cpro95.tistory.com/617 Supabase로 로그인 구현하기 with NextJS 1편 안녕하세요? 최근 NextJS로 여러가지 로그인 구현 웹앱..

cpro95.tistory.com

https://cpro95.tistory.com/621

 

Supabase로 로그인 구현하기 with NextJS 5편

안녕하세요? Supabase로 로그인 구현하기 5편입니다. 일단 이전 편 못 보신 분들을 위해 전편 링크 걸어두겠습니다. https://cpro95.tistory.com/617 Supabase로 로그인 구현하기 with NextJS 1편 안녕하세요? 최..

cpro95.tistory.com

 

NextJS Pages에 대한 보안은 여러 방법이 있지만 Client Side / Server Side 두 가지로 다뤄 볼 수 있는데요.

 

먼저, 클라이언트 측면에서 살펴보겠습니다.

 

Supabase는 onAuthStateChange라는 아주 훌륭한 이벤트 시스템을 제공합니다.

 

우리는 클라이언트 측면에서는 단순히 useAuth 훅을 이용해서 페이지 로딩 때 useEffect 훅을 이용해 페이지 접근을 막는 겁니다.

 

/pages/profile.tsx

import { useEffect } from "react";
import Router from "next/router";

const ProfilePage = ({}) => {
  const { user, signOut, userLoading, loggedIn } = useAuth();

  useEffect(() => {
    if (!userLoading && !loggedIn) {
      Router.push("/auth");
    }
  }, [userLoading, loggedIn]);

기존의 ProfilePage에서 추가된 부분만 코드를 작성했습니다.

 

클라이언트 사이드에서 useEffect로 유저가 로그인됐는지 체크해서 아니면 바로 "/auth" 라우팅으로 보내버리게 됩니다.

 

클라이언트 사이드에서의 useEffect 방식은 useEffect가 실행될 때까지 기존 UI가 아주 잠깐 보인다는 단점이 있습니다.

 

한번 테스트해보시면 쉽게 알 수 있습니다.

 

 

 

두 번째 방식은 서버사이드에서의 보안인데요.

 

NextJS 만이 가지고 있는 getServerSideProps 함수 때문에 가능한 방식입니다.

 

페이지가 로드되기 전에 getServerSideProps를 이용해서 정보를 가져오고 그걸 이용해서 클라이언트에 뿌려주게 됩니다.

 

그래서 getServerSideProps를 이용하게 되면 아까 위에서 했던 useEffect 함수가 필요 없어지게 되죠.

 

그리고 useAuth에서 가져올 함수도 signOut 한 개 밖에 없어집니다.

 

import Link from "next/link";
import { GetServerSideProps } from "next";
import { useAuth } from "../lib/auth";
import { supabase } from "../lib/supabase";
import { User } from "@supabase/supabase-js";

const ProfilePage = ({ user }) => {
  const { signOut } = useAuth();

  // useEffect(() => {
  //   if (!userLoading && !loggedIn) {
  //     Router.push("/auth");
  //   }
  // }, [userLoading, loggedIn]);

  // if (userLoading) {
  //   return <div>Loading...</div>;
  // }

  return (
    ...
    ...
    ...
    ...
  );
};

export default ProfilePage;

export type NextAppPageUserProps = {
  props: {
    user: User;
    loggedIn: boolean;
  };
};

export type NextAppPageRedirProps = {
  redirect: {
    destination: string;
    permanent: boolean;
  };
};

export type NextAppPageServerSideProps =
  | NextAppPageUserProps
  | NextAppPageRedirProps;

export const getServerSideProps: GetServerSideProps = async ({
  req,
}): Promise<NextAppPageServerSideProps> => {
  const { user } = await supabase.auth.api.getUserByCookie(req);

  if (!user) {
    return {
      redirect: {
        destination: "/auth",
        permanent: false,
      },
    };
  }

  return {
    props: {
      user,
      loggedIn: !!user,
    },
  };
};

 

UI 부분은 코드가 길어져서 뺐습니다.

 

getServerSideProps 함수에서 처음 실행하고 user 정보를 얻어오는 함수가 supabase.auth.api.getUserByCookie 함수입니다.

 

supabase는 우리의  NextJS앱과 원격으로 소통하고 User 정보와 세션을 쿠키를 통해 저장합니다.

 

즉, 유저가 로그인되어있다면 그 쿠키에서 user 정보만 빼내 오는 함수인 거죠.

 

그럼, 로그인했을 때 쿠키 설정은 어떻게 할까요?

 

AuthContext.tsx 파일을 먼저 살펴봅시다.

 

import { AuthChangeEvent, User, Session} from "@supabase/supabase-js";

  const signOut = async () => await supabase.auth.signOut();

  const setServerSession = async (event: AuthChangeEvent, session: Session) => {
    await fetch("/api/auth", {
      method: "POST",
      headers: new Headers({ "Content-Type": "application/json" }),
      credentials: "same-origin",
      body: JSON.stringify({ event, session }),
    });
  };

  useEffect(() => {
    const user = supabase.auth.user();
....
....
}

 

기존 AuthContext.tsx 파일에서 signOut 함수와 useEffect 앞에 setServerSession 함수 선언을 했습니다.

 

그리고 supabase.auth.onAuthStateChange 이벤트 등록에 setServerSession 함수를 실행시키면 됩니다.

 

const { data: authListener } = supabase.auth.onAuthStateChange(
      async (event, session) => {
        const user = session?.user! ?? null;
        setUserLoading(false);
        await setServerSession(event, session);
        if (user) {
          setUser(user);
          setLoggedIn(true);
          Router.push("/profile");
        } else {
          setUser(null);
          setLoading(false);
          setLoggedIn(false);
          Router.push("/auth");
        }
      }
    );

AuthContext.tsx 파일에서의 수정은 다 끝났습니다.

 

그럼 남은 게 뭔가요?

 

바로 우리가 위에서 작성한 setServerSession 함수에서 API CALL을 하고 있는 "/api/auth" 엔드포인트에 대한 API 핸들러를 만들어야 합니다.

 

fetch("/api/auth")라고 네트워크 상에서 Request를 한 건데요.

 

주소가 없습니다. 네, 이렇게 쓰는 방식은 NextJS의 특별한 방식인데요.

 

NextJS는 "/pages/api/" 폴더 밑에 있는 자바스크립트 파일을 API 핸들러로 인식합니다.

 

그래서 자체적으로 위와 같이 fetch("/api/auth") 라고 API Request 할 수 있죠.

 

setServerSession 함수는 바로 "/api/auth" 엔드포인트로 event , session을 POST 방식으로 넘겨주고 있습니다.

 

그럼 "/api/auth" 엔드포인트에 대한 API Request 로직을 만들어 볼까요?

 

/pages/api/auth.ts 파일을 만듭시다.

 

꼭 /pages/api 폴더 밑에 위치해야 합니다.

 

/pages/api/auth.ts

import { NextApiRequest, NextApiResponse } from "next";
import { supabase } from "../../lib/supabase";

export default function handler(req: NextApiRequest, res: NextApiResponse) {
    supabase.auth.api.setAuthCookie(req, res);
}

API 핸들러는 간단합니다.

 

아까 setServerSession 함수에서 넘어온 req를 res와 같이 supabase.auth.api.setAuthCookie 함수에 전달하고 실행하는 거뿐입니다.

 

이렇게 하면 supabase가 쿠키를 설정하게 되는 거죠.

 

이제 다시 실행해 보면 "/profile" 페이지가 아까 클라이언트 방식 때는 잠깐 보였던 Loading 관련 UI가 서버사이드 방식에서는 아예 보이지 않게 되었습니다.

 

결과론적으로 서버사이드 방식의 로그인 보안이 좀 더 완벽하고 안전한 걸로 판단되어지네요.

 

 

 

그리드형