안녕하세요?
이번 시간에는 Supabase로 구현한 유저 로그인 상태에 대한 보안적인 측면에 대해 알아보겠습니다.
전편 링크는 아래와 같습니다.
https://cpro95.tistory.com/617
https://cpro95.tistory.com/618
https://cpro95.tistory.com/619
https://cpro95.tistory.com/620
https://cpro95.tistory.com/621
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가 서버사이드 방식에서는 아예 보이지 않게 되었습니다.
결과론적으로 서버사이드 방식의 로그인 보안이 좀 더 완벽하고 안전한 걸로 판단되어지네요.
'코딩 > React' 카테고리의 다른 글
ElectronJS 일렉트론 강좌 4편 SQLite3 sql.js (2) | 2022.01.15 |
---|---|
Supabase로 로그인 구현하기 with NextJS 7편 (0) | 2022.01.09 |
Supabase로 로그인 구현하기 with NextJS 5편 (0) | 2022.01.07 |
Supabase로 로그인 구현하기 with NextJS 4편 (0) | 2022.01.07 |
Supabase로 로그인 구현하기 with NextJS 3편 (1) | 2022.01.06 |