NextJS

Next JS URL 직접 접근 막기

FE 2022. 9. 8. 15:10

출처 : https://medium.com/@msj9121/next-js-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90-8727f76614c9

 

#1 들어가기

오늘 포스트는 Next JS에서 URL 직접 접근 막기와 관련하여 글을 써보도록 하겠습니다.

 

항상 기능을 고도화 하는 과정에서 빠질 수 없는 부분인 것 같습니다.

 

서비스를 출시할 때 내가 아닌 타 사용자들이 쓸 때 어떠한 환경이 좋은 환경인지 어떤 사용자 경험이 좋은 경험인지에 대해 지속적으로 고민을 해야 할 것 같습니다.

 

그런 부분에 있어 로그인을 해야만 접근할 수 있는 페이지에 URL을 직접 치고 들어갔을 때 접근이 된다면 보안에 큰 타격이 되겠죠?

 

그 전에 Next Docs를 한번 살펴봅시다 !

#2 NextJS DOCS

Next의 구동순서는 _app.js와 _document.js가 제일 처음 실행되며, 두 파일 모두 pages 폴더 안에 있어야 한다.

 

1. _app.js

  • 최초로 실행되는 파일로, Client에서 띄어지는 전체 컴포넌트의 레이아웃
  • 공통 레이아웃으로 최초에 실행되어 내부에 들어갈 컴포넌트들을 실행한다.

2. _document.js

  • 그 다음에 _document.js가 실행되는데, 이는 _app.js에서 구성한 HTML이 어떤 형태로 들어갈 지 구성해주는 것
import Document, { Html, Head, Main, NextScript } from "next/document";

export default class RootDocument extends Document {
  render() {
    return (
      <Html lang="ko">
        <Head>
          <link rel="shortcut icon" href="/favicon/xquare.ico" />
          <meta charSet="UTF-8" />
          <meta name="description" content="test" />
        </Head>
        <body id="body">
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

3. getInitialProps

  • getStaticProps, getServerSideProps들도 있지만 제가 했던 getInitialProps만 다루겠습니다.
  • Next에서 getInitialProps를 통해 데이터를 미리 불러와 한번에 렌더링이 가능하다.
  • 그러나 Next 9.3 이후로는 getStiticProps나 getSeverSideProps 사용을 권장한다.
  • 한 가지 의문인점은 Docs를 살펴보니 app.js에서는 getStaticProps나 getServerSideProps를 지원하지 않는다.
    ( app.js에서 말고 pages에서 사용하라는 말인가..? )
  • 그리고 getInitialProps는 context라는 단일 인자를 받는다. Context Object !
    • pathname : pages 폴더 안에 있는 현재 Route
    • query : 객체로 이루어진 queryString EX) /myInvestment?redirect_url=${router.pathname}에서 ? 이후의 {키='값'}
    • asPath : query를 포함한 String의 실제 경로 EX) /myInvestment?redirect_url='값' 전체 경로
    • req : HTTP request Object
    • res : HTTP response Object
    • err: Error OBject if any error is encountered during the rendering

출처: https://nextjs.org/docs/advanced-features/custom-app

App.getInitialProps = async props => {
  const { ctx, router } = props;
  await ssrAxios(ctx, router);

  return {};
};

// _app.js의 일부

#3 NextJS URL 직접 접근 막기 (내용 보완 - SNS 로그인)

자, 이렇게 Next Docs를 살펴보고 제가 적용한 URL 직접 접근을 막는 방법을 소개해보겠습니다.

 

바로 위 코드에서 getInitialProps로 ssrAxios 메소드에 ctx와 router를 params로 넣어줬습니다.

 

ssrAxios에서는 사이트 접근시 Login페이지 여부를 구분하게 됩니다.

 

export const AUTH_PATH = ["/invest"];

// routerGuard.js

routerGuard에 이런식으로 배열 형태로 계속 추가해 줄 수 있습니다.

export const isAuthPath = router => {
  const routerPath = router.pathname;
  return AUTH_PATH.includes(routerPath);
};

그리고 routerGuard에서 설정한 경로로 true, false로 값을 구분하여 로그인 페이지로 가게 하느냐 여부를 구분하게 됩니다.

const redirectLogin = (ctx, router) => {
  if (isAuthPath(router)) {
    let queryValue = getQueryParams(router);
    ctx.res.writeHead(302, {
      Location: `/login?redirect_url=${router.pathname}${queryValue}`,
    });
    ctx.res.end();
  }
};

// ssrAxios.js의 일부

 

여기서 writeHead는 response 객체의 메소드에서 헤더 정보를 응답에 작성해서 내보내는 것 입니다.

 

첫 번째 인자는 상태 코드(302는 임시적으로 주소가 바뀌었을 경우 사용 대부분의 리다이렉트는 302 코드 사용)를 지정하고, 두 번째 인수에 헤더 정보를 보냅니다.

 

res.end()는 컨텐츠 출력 완료 (응답 종료)로서 응답 처리는 종료되고, 그 요청의 처리가 완료됐단 뜻입니다.

 

Location에서는 /login으로 login페이지로 이동시키면서 Query Params를 이용합니다. 물음표 ?와 key=value쌍을 사용하여 URL 끝에 전달됩니다.

 

그리고 queryParms를 가져오는 것은 useRouter()라는 next.js의 Hooks를 사용할 수 있습니다.

 

이렇게 해서 로그인이 필요한 페이지는 SSR 환경이기 때문에 일시적으로 깜빡이는 페이지 없이 깔끔하게 로그인 페이지로 이동 후, 로그인에 성공했을 때 제가 접근하고자 했던 페이지로 접근할 수 있었습니다.

 

Docs를 읽는 습관과 지속해서 고도화 기능을 생각하며 이런 예외처리를 항상 고민해야 될 것 같습니다.

 

내용추가

그러나, 이렇게만 작업하게 되면 내부적으로 동작하는 일반 로그인(이메일 로그인, 아이디 로그인)에만 동작하게 됩니다.

예를 들어 SNS 로그인 같은 경우 해당 SNS 페이지에 갔다가 오기 때문에 url에 설정해준 query값이 날라가게 됩니다. (팝업으로 동작하게 되는 경우는 제외) 그렇기 때문에 추가 설정을 해줘야 하는데 저는 로컬 스토리지를 이용해서 작업을 추가적으로 해주었습니다.

 

  // 로그인이 필요한 페이지인지 확인 후, 로컬 스토리지에 해당 redirectUrl 저장 
  useEffect(() => {
    if (redirectUrl !== "/main") {
      setStorage("redirectUrl", redirectUrl as string);
    }
  }, []);
  useEffect(() => {
    if (loginData === 204 || snsLoginData === 204) {
      setLocalStorage("isLogged");
      
      // sns 로그인이라면 로컬스토리지에 있는 경로를 꺼내옴
      if (snsLoginData === 204) {
        router.push(`${getStorage("redirectUrl")}`);
      } else {
        router.push(redirectUrl as string);
      }
    }
  }, [loginData, snsLoginData]);