next-auth로 Role-based access control 구현: 회원별 권한 분기 개선
기존: 특정 Email인 유저라면 해당 권한을 수행하거라.
기존에는 회원별 권한 분기를 구분하는 방법이 단순했습니다.
유저의 email로 구분지었죠.
adminEmailList에 포함되는 경우에는 admin 권한을 수행할 수 있고,
testerEmailList에 포함되는 경우에는 tester 권한을 수행하는 방식이었습니다.
하지만 이러한 방식은 확장성도 떨어지며, 만일 tester가 1억명으로 늘어난다면?
1억개의 요소를 지닌 배열에서 Email이 포함되어 있는지를 뒤져봐야 하는 문제가 생깁니다.
수정이나 삭제를 하려고 해도 index를 찾고, 해당 index를 삭제하는 등 불필요한 작업들이 추가됩니다.
그렇다면 유저에 role을 추가하면 되지 않을까?
이를 개선하기 위해 사실은 처음부터 Role-based 방식으로 다음과 같이 구현하고 싶었습니다.
A user.role = "admin"
B user.role = "tester"
위의 코드를 보면 Role-based가 어떤 방식인지 바로 알 수 있을 겁니다.
A는 admin 이라는 role 속성을 가지고, B는 tester라는 role 속성을 가지고 있는거죠.
DB에 저장할 때 이렇게 지정을 해준다면 user의 정보를 조회하는 것만으로 해당 user의 role을 알 수 있겠죠.
그런데 왜 구현을 못했었냐...
Role-based 방식을 어디서 들어본 게 아니라 혼자서 이렇게 구현하면 되겠다! 라고 생각했었고,
next-auth를 잘 만지작 거려서 어떻게든 꾸역꾸역 구현을 해보았지만,
JWT에 저장된 user의 데이터를 읽는 것이 아니라, DB에서 user 데이터를 조회하는 방식으로 구현했었죠.
아래는 당시 구현해 본 로직을 글로 쓴 것입니다.
callbacks: {
// jwt 생성 시 실행되는 코드
jwt: ({ token }: { token: JWT }) => {
if (token) {
// 1. token의 정보로 해당 유저의 DB 정보를 조회
// 2. 해당 유저의 role 가져오기
// 3. role을 token에 추가
}
// 4. role이 추가된 token을 반환
return token;
},
},
token은 cookie에 저장되기 때문에 로그인하는 과정에서 role을 추가해주면 된다고 생각했죠.
역할을 수행하는 것도 지켜보았지만, 결국 이 role 속성을 추가하는 과정은 또 수동이었습니다.
회원을 생성할 때 role을 주입할 수 있는 방법을 연구해보다가 타입 지정도 실패하고, 저것도 실패하고 결국 실패했습니다.
Next.js를 처음 다루는데 하나하나 완벽하게 하려고 하지 말고 일단 다른 것부터 하자는 생각이 들었습니다.
그래서 그냥 해당 레포지토리는 시도만 해보고 폐기처리 했었습니다.
next-auth로 Role-based access control 구현하기
Next.js로 기능 구현하는 것에도 차츰 익숙해졌고,
블로그가 블로그답게 보일 만큼 기능 구현을 마치고 나니 다시 Role-based 방식으로 구현해보고 싶어졌습니다.
이제부터는 구현 방법입니다.
https://authjs.dev/guides/basics/role-based-access-control
next-auth로 소셜 로그인을 구현하려면 다음과 같이 providers 속성에
발급받은 소셜 로그인 API의 clientId와 clientSecret을 객체로 전달하면 됩니다.
// next-auth 로그인 설정입니다.
export const authOptions: NextAuthOptions = {
providers: [
GithubProvider({
clientId: githubSocial.clientId,
clientSecret: githubSocial.clientSecret,
profile(profile) {
const { id, name, email, image } = profile;
return { id, name, email, image, role: 'tester' };
},
}),
],
adapter: MongoDBAdapter(connectDB),
};
profile() callback로 유저 정보 수정하기
소셜 로그인을 하는 유저의 정보를 DB에 저장할 때 해당 유저의 정보를 수정할 수 있습니다.
GithubProvider({
clientId: githubSocial.clientId,
clientSecret: githubSocial.clientSecret,
profile(profile) {
const { id } = profile;
return { id };
},
}),
profile을 직접 출력해보면 다양한 정보가 들어 있는데, 필수로 전달해야 하는 속성은 id입니다.
{
//... 이런 정보들
type: 'User',
site_admin: false,
name: null,
company: null,
blog: '',
location: null,
//... 저런 정보들
}
id를 전달했다면 나머지는 커스텀하면 되는 속성들입니다.
저는 어떻게 했는지 코드로 바로 살펴봅시다.
GithubProvider({
clientId: githubSocial.clientId,
clientSecret: githubSocial.clientSecret,
profile(profile) {
const { id, name, email, image, login } = profile;
return { id, name, email, image, login, role: 'tester', created_at: new Date() };
},
}),
소셜 로그인을 최초로 한 회원의 github정보 중에서 id, name, email, image, login을 가져오도록 했습니다.
github를 만들고 프로필 정보를 아무것도 수정하지 않은 회원의 경우에는
온통 null이기 때문에 어떤 활동을 하는 회원인지 DB의 정보만으로는 구분할 방법이 없습니다.
name이 null이기 때문에 댓글을 작성해보면 다음과 같이 닉네임이 들어가야할 자리가 null인 상태네요.
그래서 github ID인 login 속성을 추가하여 name이 존재하지 않는 경우에는 깃허브에서 사용하는 아이디를 그대로 블로그에서도 이용했습니다.
이제 callback 에서 데이터를 저장하면 됩니다.
JWT를 생성할 때 token에 user 정보를 추가하여 반환하고
callbacks: {
// 4. jwt 생성 시 실행되는 코드
jwt: ({ token, user }: { token: JWT; user: CustomUser }) => {
if (user) {
token.role = user.role;
token.github = user.login;
}
return token;
},
},
session을 생성할 때는 바로 위에서 token에 추가했던 정보를 담아 반환하도록 합니다.
callbacks: {
//...
// 5. 유저 세션이 조회될 때 session에 user 정보를 저장하여 이용할 수 있도록 함
session: ({ session, token }: { session: CustomSession; token: CustomJWT }) => {
if (session?.user) {
// session에 정보 추가
session.user.role = token.role;
session.user.name = session.user.name || token.github;
return session;
}
return session;
},
},
이제 session을 가져와보면 role이 추가되고, null이었던 name에도 github에서 사용하는 네임도 저장이 됐습니다.
{
user: {
name: 'uririu',
email: 'email@email',
image: null,
role: 'tester'
},
expires: '2023-11-07T01:54:42.269Z'
}
댓글 작성 요청을 할 때 DB에 해당 정보를 추가해주면 다음과 같이 이름이 나타나고, role도 이용할 수 있게 됐습니다.
후기
기능 구현 자체는 쉽지만 리팩토링 과정이 훨씬 어려웠습니다.
제대로 동작하는지 다 찾아서 수작업으로 테스트도 해보고... 리팩토링 할 때마다 수작업 테스트가 정말 쉽지 않네요
다음에는 test를 공부하여 적용시키도록 해봐야겠습니다.