우선 나는 JWT 방식으로 백엔드의 인증 부분을 구현했기 때문에 이를 적극 활용할 것이다. 이를 위해서 백엔드에게 로그인 API를 요청하면 토큰을 넘겨주고 해당 토큰을 받아 Auth.js가 관리할 수 있도록 연결해주면 된다.
access token을 관리하는 것은 크게 어렵지 않았으나 refresh token 기능을 넣으면서 access token을 계속해서 갱신시켜주고 유효성을 검증하는 과정이 상당히 복잡하다는 사실을 느꼈다.
사실 Auth.js에서 만들어주는 session은 default값으로 특정 시간이 지나면 만료되도록 설정이 되어있고 물론 사용자가 그 시간을 설정해줄 수도 있다. 하지만 access token만으로 인증을 하게 되면 탈취 시 해당 토큰으로 내 명의로 어떤 짓을 할 지 모르는 사태가 벌어질 수도 있기 때문에 refresh token을 통해 access token을 주기적으로 갱신시켜주는 전략을 사용해야 한다.
위 코드는 jwt콜백과 session 콜백의 코드로 각 콜백은 그것과 관련된 이벤트가 발생 시 호출되는 함수이다.
jwt: JWT 토큰이 만들어질 때, 혹은 업데이트될 때 호출됨. 반환 값은 암호화되며 쿠키에 저장됨
session: session이 확인되는 모든 순간에 호출됨
CredentialsProvider에 대한 코드는 정형화되어있기 때문에 첨부하지는 않았지만 해당 코드에서 중요한 부분은 백엔드로 로그인 요청(/auth/sign-in)을 보내고 서버로부터 사용자에 대한 정보와 더불어 두 토큰(access, refresh)에 대한 값을 응답으로 반환받으면, jwt 콜백에서 'user' 이름으로 값을 전달 받아 사용할 수가 있다.
하지만 jwt 콜백에서 'user' 인자가 존재하는 경우는 첫 로그인 시일 때가 유일하기 때문에 그 경우에 token에 user 정보를 저장하도록 해준다.
user가 존재하지 않는 경우는 처음에 로그인을 하고 나서 다시 해당 콜백이 호출된 경우이기 때문에 이 경우에는 앞서 token에 user 정보를 저장했으니 이 token 값을 그대로 반환하도록 해주었다.
하지만 이때, user가 존재하지 않으면서도 user 정보에 포함되어 있던 토큰의 유효 시간을 계산해서 이 토큰이 유효한 경우(else if문)에만 토큰을 바로 넘겨주고 그렇지 않은 경우(else문) 백엔드에 token을 refresh 하는 요청을 보내 새 access token을 받아 token에 다시 넣어주도록 하는 과정을 진행해주어야 한다.
유효하지 않은 경우 백엔드로부터 error response가 넘어오기 때문에 response.ok가 false값을 가지게 된다.
백엔드에 refresh 요청을 보낼 때 token으로부터 refreshToken을 가져와 header의 cookie 필드에 담아주어야 한다.
그래서 이때 백엔드 측에서 Unauthroized 401 에러를 반환했다면 쿠키에 담아 보낸 refresh token이 만료되었다는 의미이므로 이를 명시하기 위해 session에 저장될 객체에 error 속성을 추가해주도록 하였다.
4. middleware로 라우트 보호하기
middleware는 Next13 버전 이후에서 사용할 수 있는 기능으로 root 디렉터리에 middlware.ts(or .js)파일을 만들어 그 안에 middleware 함수를 구현하면 적용이 된다.
미들웨어는 페이지를 렌더링하기 전에 서버 측에서 실행되는 함수로, 특정 요청 전에 무언가를 수행할 수 있게 하는 기능을 가진다.
Middlware에서는 NextRequest 객체와 NextResponse 객체에 접근할 수 있어 다양한 기능을 구현할 수 있다.
위 미들웨어 코드는 생각보다 단순하여 request와 response 객체를 적절히 활용하여 현재 요청의 url을 가져오거나 응답 전에 리다이렉트 시키거나 하는 작업이 가능하다.
우리는 next-auth로 session에 대한 정보를 서버, 클라이언트에서 모두 가져올 수 있기 때문에 session을 가져와 이를 바탕으로 페이지 리다이렉트를 결정하게 하였다.
물론 모든 라우트에 접근 시 이 미들웨어가 호출되는 것이 아니라 아래 정의된 config의 matcher 배열에 이 미들웨어를 실행하게 할 디테일한 url을 명시하도록 할 수 있다.
또, 원하는 경로를 일일히 다 적기는 힘든 일이기 때문에 위 코드와 같이 정규 표현식을 적절히 활용할 수가 있다.
내 경우에 /admin 으로 시작하는 모든 경로에 대해, '/course' 는 제외하고 /course로 시작하는 모든 경로에 대해 실행될 수 있도록 한 것이다.
5. 세션 만료시키기
앞서 3장에서 refresh에 실패한 경우에는 다시 session을 존재하지 않는 상태로 만들어 4장에 명시했던 것처럼 '/login' 페이지로 리다이렉트 되도록 유도해야 하기 때문에 refresh 실패 시 session에 넣어주었던 error 속성의 유무로 이를 가능하게 할 수 있다.
이번 프로젝트에서는 로그인의 유무에 따라 헤더 부분의 구성이 달라지게 되는데 분리시킨 헤더 컴포넌트(@/components/Header.tsx)에서 useEffect 함수를 통해 session.user.error를 확인하여 next-auth에 정의된 signOut() 함수를 호출할 수가 있다.
그러면 놀랍게도 next.js에서 세션을 확인하기 위해 jwt 콜백이 실행되는 순간 refresh token의 유효하지 않은 경우 자동으로 signOut이 되어 쿠키에 토큰이 사라지고 세션도 만료되어 4장에서 정의한 미들웨어 matcher 주소에 접근하려는 경우 이를 막을 수 있게 되는 것이다.
이번 프로젝트를 진행함에 있어 개발을 할 때는 로컬 컴퓨터에 node와 npm modules을 직접 설치하는 방식으로 진행을 했지만 클라우드 서버에 올릴 때는 Dockerfile을 따로 만들어 컨테이너를 띄우는 방식으로 진행했다.
그렇기 때문에 로컬 개발 시에는 .env 파일로부터 환경 변수를 가져왔지만 docker를 사용하면 Dockerfile에 이를 명시해주어야 접근이 가능하다. 그렇기 때문에 이를 잘 명시해야 프로덕션 단계에서 문제없이 진행될 수 있다.
ENVNODE_ENV production
ENVPORT80ENVAUTH_SECRET secret
ENVNEXT_PUBLIC_BASE_URL=http://00.000.000.000:3000ENVNEXT_PUBLIC_API_URL=http://00.000.000.000:3000ENVNEXT_PUBLIC_BASE_HOST=http://00.000.000.00:80EXPOSE80
6-2. NextAuth option 설정
next-auth 라이브러리를 설정할 때는 auth.ts에 작성하게되는데 NextAuth라는 함수에 각종 옵션 속성을 지정할 수가 있다.
그 중에 trustHost를 true로 지정해주어야 API 호출을 백엔드와 '믿을 수 없는 출처'와 같은 오류 없이 가능하다.