Next client와 Next server, Spring server를 이용한 프로젝트입니다
Next 14 를 이용하여 구현했습니다.
Next 14와 next-auth 를 이용하여 회원 관리를 구현하는 중 Reissue를 위해 axios instance를 구현하였다.
먼저, 클라이언트와 서버에서 사용할 Axios Instace를 구분해 주었고, 인증이 필요한 인스턴스와 필요없는 인스턴스를 구분해주었다.
// app/api/clientInstance.ts
'use client';
import { default as Axios } from 'axios';
import { getSession } from 'next-auth/react';
import clientReissue from './auth/reissue/client';
// 인증이 필요 없는 인스턴스
export const client = Axios.create({
baseURL: process.env.BASE_URL,
});
// 인증이 필요한 인스턴스
export const clientAuth = Axios.create({
baseURL: process.env.BASE_URL,
});
// 요청 인터셉터
clientAuth.interceptors.request.use(
async config => {
try {
const session = await getSession();
if (!session)
throw new Error('Axios client-auth interceptor error: no session');
const accessToken = session.accessToken;
config.headers.Authorization = `Bearer ${accessToken}`;
return config;
} catch (error) {
return Promise.reject(error);
}
},
error => Promise.reject(error),
);
// 응답 인터셉터
clientAuth.interceptors.response.use(
response => response,
async error => {
try {
const originalRequest = error.config;
// 재시도가 필요 없는 경우
if (error.status !== 401 || originalRequest._retry)
return Promise.reject(error);
const session = await getSession();
if (!session)
throw new Error('Axios client-auth interceptor error: no session');
// 재시도 여부 기록
originalRequest._retry = true;
// 토큰 갱신 후 재요청
const accessToken = await clientReissue();
originalRequest.headers['Authorization'] = `Bearer ${accessToken}`;
return clientAuth(originalRequest);
} catch (refreshError) {
return Promise.reject(refreshError);
}
},
);
const instances = { client, clientAuth };
export default instances;
// app/api/serverInstance.ts
// ...(클라이언트와 동일한 부분)
import { getServerSession } from 'next-auth';
import { authOptions } from './auth/authOptions';
import { PostReissueResponse } from './auth/reissue/route';
// ...
serverAuth.interceptors.request.use(
async config => {
// 서버 세션 조회
const session = await getServerSession(authOptions);
// ...
},
error => {
return Promise.reject(error);
},
);
serverAuth.interceptors.response.use(
response => response,
async error => {
// ...
const session = await getServerSession(authOptions);
if (!session)
throw new Error('Axios sever-auth interceptor error: no server session');
// ...
// 리프레시 토큰 갱신 (미작동)
try {
const res: AxiosResponse<PostReissueResponse> =
await axios.post('/api/auth/reissue');
session.accessToken = res.data.accessToken;
} catch (refreshError) {
console.log('Refresh error: clear server session');
session.accessToken = '';
session.user.id = -1;
session.user.email = '';
session.user.penName = '';
session.user.fieldImage = '';
return Promise.reject(refreshError);
}
},
);
const instances = { server, serverAuth };
export default instances;
대부분 코드는 공식문서인 Axios Docs-인터셉터에서 명시한 부분과 유사하다. 그러나 NextJS와 next-auth를 적용하며 특징적인 부분이 몇군데 있다.
토큰 관리를 위해 next-auth의 session을 사용했다. 따라서 클라이언트에서는 getSession(), 서버에서는 getServerSession()을 이용하여 AccessToken을 불러오는 것을 볼 수 있다.
Client-side에서 세션을 업데이트 하는 방법은 useSession().update({...session}) 을 이용하는 방법이 있다. 그러나 useSession()은 react hook이기 때문에 같은 hook 이나 component 안에서만 호출이 가능하다. 따라서 대용으로 next-auth의 signIn('credentials', options) 메소드를 사용해 주었다.
next-auth의 Credentials Provider에서 credentials를 통해 reissue 변수를 받고, 여기에 갱신할 session의 정보를 담아 처리했다.
이를 통해 클라이언트와 서버 세션을 동시에 갱신할 수 있다.
// app/api/auth/[...nextAuth]/route.ts
// ...imports
export const authOptions: NextAuthOptions = {
// 생략
providers: [
Credentials({
// 로그인 시 받을 데이터 정의
credentials: {
...loginData,
reissue: {...CredentialInput},
},
authorize: async credentials => {
// 리이슈인 경우 바로 인증 & 세션 갱신
if (credentials?.reissue) {
const newSession = JSON.parse(credentials.reissue) as User;
return { ...newSession };
}
// ...로그인 관련 코드
},
}),
],
// 생략
};
next-auth v4 에서는 서버에서 세션을 갱신할 메소드가 없다. @auth0/next-auth0라이브러리의 updateSession 메소드를 사용하거나 아직 베타버전인 next-auth v5의 update를 이용하면 갱신이 가능하다고 한다.
// ...
export const { handlers: { GET, POST }, auth, signIn, signOut, update } = NextAuth(authConfig);
그러나 불안정한 메소드를 이용하고싶지 않아 프로젝트에서는 제외하고, 서버사이드에서는 인증이 필요한 메소드를 사용하지 않도록 하였다.