참고자료: 견고한 node.js 프로젝트 설계하기
참고자료: 쉽게 알아보는 서버 인증 1편(세션/쿠키, JWT)
참고자료: Express에서 JWT로 인증시스템 구현하기 (Access Token과 Refresh Token)
#3. JWT 사용하기 (1) 와 동일한 자료를 참고하여, 동일한 자료를 참고자료로 명시합니다 :)
이번 글에서는 refreshToken
을 활용하여 accessToken
갱신하기, 그리고 로그아웃을 정리할 예정!
이전 글에서 controller와 services에서 사용하는 utils 함수를 모두 언급했으므로,
이번 글에서는 이 부분을 생략할 예정!
토큰 갱신 부분에서는 하고 싶은 말이 많을 것 같아...ㅎㅎ
1. accessToken 갱신 관련 controllers
2. accessToken 갱신관련 services
3. 로그아웃 관련 controllers
4. 로그아웃 관련 services
exports.tokenRefresh = async(req, res, next) => {
// {
// "Authorization":'bearer access-token',
// "Refresh":"refresh-token"
// } // 아마 이런 형식 - Body 말고 Header에 담아 보내는 걸로 통일
try {
if (req.headers.authorization && req.headers.refresh) {
const accessToken = req.headers.authorization.split('Bearer ')[1];
const refreshToken = req.headers.refresh;
const refreshResult = await authServices.tokenRefresh(accessToken, refreshToken); //success, status, token을 받아 옴.
if (refreshResult.success) {
res.status(200).json({
message:'Access Token 신규 발급 성공',
status:refreshResult.status,
tokens:refreshResult.tokens
})
}
if (refreshResult.success === false) {
res.status(400).json({
msg:'Access Token 신규 발급 실패',
status:refreshResult.status,
tokens:refreshResult.tokens
})
}
} else {
res.status(400).json({
msg:'Access Token 신규 발급 실패',
status:'Refresh Token과 Access Token이 요청에 포함되지 않았습니다.'
})
}
} catch(error) {
console.log(error);
next(error);
}
}
tokenRefresh
는 headers에 accessToken
과 refreshToken
을 각각
Authorization, Refresh라는 키에 담아 보내면
accessToken
이 만료되고, refreshToken
이 유효한 상태일 경우 새로운 accessToken
을 발급해 주는 로직으로 구성했다.
프론트엔드 단에서 Authorization과 Refresh 키를 둘 다 대문자로 시작하도록 작성해서 요청을 보냈는데, 백엔드 단에서 받을 때는 소문자로 바뀌어 있었다.
그래서
if (req.headers.authorization && req.headers.refresh)
headers에서 위와 같이 accessToken
과 refreshToken
을 가져옴!
이전 글에서 언급했듯이, accessToken
은 Bearer {accessToken}
과 같은 형태로 오기 때문에
const accessToken = req.headers.authorization.split('Bearer ')[1];
이렇게 accessToken
만 추출하는 과정을 거쳐 주었다.
추출한 accessToken
과 refreshToken
은 services로 넘겨주어 필요한 로직을 수행하도록!
토큰을 갱신하는 비즈니스 로직에 있어서는 세 번째 참고자료에서 도움을 정말 많이 받았다..!!
두고두고 참고하면 좋을 듯 :)
토큰 갱신 로직에서, 크게 분류해야 하는 경우의 수는 세 가지 정도 된다고 한다.
1. accessToken이 만료되지 않은(유효한) 경우
2. accessToken과 refreshToken이 모두 유효하지 않은 경우
3. accessToken이 만료되었고, refreshToken이 유효한 경우 == 새 accessToken을 발급해야 함!
결국 상대적으로 유효 기간이 긴 refreshToken
으로 accessToken
을 갱신하는 흐름이기 때문에,
accessToken
이 유효한 시점에서 굳이 refreshToken
의 유효성을 추가로 검사해야 하지는 않는 것 같다.
위의 세 경우로 분류를 마쳤다면, 해당 경우들을 그대로 구현해 주면 된다고 한다!
const accessResult = verifyAccess(accessToken);
utils/auth.js
에 작성해 둔 함수로 accessToken
을 검증하여, 검증 결과에 따라
{
success:true,
message:'Token Verified',
userData: {
pk:verified.pk,
email:verified.email
}
}
{
success:false,
message:error.message,
userData:null
}
위 두 가지 경우 중 하나를 반환하도록 작성해 두었었다!
P.S. 자세한 로직은 이전 글 참고
verifyAccess
함수를 사용해서, 위의 세 가지 경우의 수에 따라 작성한 비즈니스 로직은 다음과 같다.
exports.tokenRefresh = async (accessToken, refreshToken) => {
const accessResult = verifyAccess(accessToken); //만료되지 않아야만 userData 반환함.
if (accessResult.userData) { //accessToken이 만료되지 않음.
return {
success:false,
status:'Access Token not expired',
tokens:{
'access':accessToken,
'refresh':refreshToken
}
}
}
//accessToken이 만료됨
if (accessResult.success === false && accessResult.message === 'jwt expired') { //accessToken은 만료되었고
const refreshResult = await verifyRefresh(refreshToken);
if (refreshResult.success === false) { //refreshToken도 유효하지 않음.
return {
success:false,
status:'No token valid. Re-login required.',
tokens:null
}
}
if (refreshResult.success === true) { //refreshToken은 유효함 == 새 accessToken 발급
const userData = await getUserWithRefresh(refreshToken);
const newAccess = signAccess({
pk:userData.pk,
email:userData.email
});
return {
success:true,
status:'New Access Token granted',
tokens:{
access:newAccess,
refresh:refreshToken
}
}
}
}
}
accessToken
이 유효한 경우accessResult
에 userData가 존재하는 경우는 곧, accessToken
이 만료되지 않아 사용자 정보가 정상적으로 추출되었다는 뜻이므로 accessToken
을 신규 발급할 필요가 없는 경우!success:false
로 두고, 입력받은 토큰들을 그대로 다시 반환해 주었다.1의 경우에 해당하지 않고,
accessResult.success === false && accessResult.message === 'jwt expired'
인 경우,
즉, accessToken이 만료되어 토큰 유효성 검사에 실패한 경우,
verifyRefresh
함수로 refreshToken
의 유효성을 검사하는 로직을 거친다.
P.S. verifyRefresh 함수의 로직도 이전 글 참고
refreshToken
유효성 검사를 거쳐,
accessToken
과 refreshToken
이 모두 유효하지 않은 경우success:false
로 두고, 재로그인이 필요하다는 상태 메시지를 보내준다.refreshToken
은 유효한 경우refreshToken
에서 사용자의 pk값을 추출하여, 사용자 정보를 찾고, 사용자 정보를 담아 새로운 accessToken
을 발급해 준다!refreshToken
은 아직 유효한 상태이므로, 입력받은 refreshToken
을 그대로 다시 반환한다. 로그아웃 로직은 상대적으로 간단한 편인 듯.
사실 로그인이 정상적으로 이루어지면, 프런트엔드 단에서 AsyncStorage
에 토큰들을 저장하고, 로그아웃 시에 AsyncStorage
에 저장된 토큰들을 삭제하는 로직이 있어,
굳이 DB에 refreshToken
을 저장할 필요는 없을 것 같아..
추후 수정하거나 참고자료처럼 redis를 써 보는 것도 고려 중..!
일단 지금 작성해 둔 controller는 다음과 같다.
exports.logOut = async(req, res, next) => {
try {
const refreshRemoved = await authServices.logOut(req.headers);
if (refreshRemoved.success) {
res.status(200).json({
message:'로그아웃 성공',
leftUser:refreshRemoved.userData,
status:refreshRemoved.message
})
}
if (refreshRemoved.success === false) {
res.status(400).json({
message:'로그아웃 실패',
leftUser:refreshRemoved.userData,
status:refreshRemoved.message
})
}
} catch(error) {
console.log(error);
}
}
accessToken
과 refreshToken
이 포함되어 있는 headers를 services 로직으로 보내준다.
exports.logOut = async({authorization, refresh}) => {
const refreshToken = refresh;
const refreshResult = await verifyRefresh(refreshToken);
try {
const user = await User.findOne({where:{pk:refreshResult.userPk}});
if (user) {
user.Refresh = null;
user.save()
return {
success:true,
userData:user,
message:'LogOut Success(Refresh Token removed)'
}
}
if (!user) {
return {
success:false,
userData:null,
message:'User not found'
}
}
} catch(error) {
console.log(error);
return {
success:false,
userData:null,
message:error.message
}
}
}
services에서는 각각 authorization
과 refresh
키에 담긴 accessToken
과 refreshToken
을 받아서
해당 refreshToken
을 발급받은 사용자를 찾아 refreshToken
을 DB에서 삭제하는 과정을 거친다.
그리고 로그아웃한 사용자 정보를 함께 반환해 줌!
해당 반환값을 넘겨 받은 controller에서 반환값의 success
여부에 따라 적절한 응답값을 보내주는 것으로 구분하였다.
비교적 급한 작업(?)들이 끝나면, redis 사용도 꼭 도전해 보고 싶다! :)