#4. JWT 사용하기 (2)

toto9602·2022년 2월 24일
0

첫 Express 프로젝트

목록 보기
6/7

참고자료: 견고한 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


accessToken 갱신과 로그아웃은 동일한 파일에 작성하였지만, 편의상 소제목을 구분해서 작성할 예정!

1. accessToken 갱신 관련 controllers

controllers/authController.js

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에 accessTokenrefreshToken을 각각
Authorization, Refresh라는 키에 담아 보내면
accessToken이 만료되고, refreshToken이 유효한 상태일 경우 새로운 accessToken을 발급해 주는 로직으로 구성했다.

프론트엔드 단에서 Authorization과 Refresh 키를 둘 다 대문자로 시작하도록 작성해서 요청을 보냈는데, 백엔드 단에서 받을 때는 소문자로 바뀌어 있었다.

그래서

if (req.headers.authorization && req.headers.refresh)

headers에서 위와 같이 accessTokenrefreshToken을 가져옴!


이전 글에서 언급했듯이, accessTokenBearer {accessToken}과 같은 형태로 오기 때문에

const accessToken = req.headers.authorization.split('Bearer ')[1];

이렇게 accessToken만 추출하는 과정을 거쳐 주었다.


추출한 accessTokenrefreshToken은 services로 넘겨주어 필요한 로직을 수행하도록!


2. accessToken 갱신관련 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 함수를 사용해서, 위의 세 가지 경우의 수에 따라 작성한 비즈니스 로직은 다음과 같다.

services/authServices.js

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
                }
            }
        }
    }
}
  1. accessToken이 유효한 경우
    accessResult에 userData가 존재하는 경우는 곧, accessToken이 만료되지 않아 사용자 정보가 정상적으로 추출되었다는 뜻이므로 accessToken을 신규 발급할 필요가 없는 경우!
    따라서 success:false로 두고, 입력받은 토큰들을 그대로 다시 반환해 주었다.

1의 경우에 해당하지 않고,
accessResult.success === false && accessResult.message === 'jwt expired' 인 경우,
즉, accessToken이 만료되어 토큰 유효성 검사에 실패한 경우,
verifyRefresh 함수로 refreshToken의 유효성을 검사하는 로직을 거친다.

P.S. verifyRefresh 함수의 로직도 이전 글 참고

refreshToken 유효성 검사를 거쳐,


  1. accessTokenrefreshToken이 모두 유효하지 않은 경우
    success:false로 두고, 재로그인이 필요하다는 상태 메시지를 보내준다.

  1. refreshToken은 유효한 경우
    refreshToken에서 사용자의 pk값을 추출하여, 사용자 정보를 찾고, 사용자 정보를 담아 새로운 accessToken을 발급해 준다!
    refreshToken은 아직 유효한 상태이므로, 입력받은 refreshToken을 그대로 다시 반환한다.

반환값에 따라, controller에서 적절한 응답을 보내주면 로직 마무리! :)

3. 로그아웃 관련 controllers

로그아웃 로직은 상대적으로 간단한 편인 듯.

사실 로그인이 정상적으로 이루어지면, 프런트엔드 단에서 AsyncStorage에 토큰들을 저장하고, 로그아웃 시에 AsyncStorage에 저장된 토큰들을 삭제하는 로직이 있어,
굳이 DB에 refreshToken을 저장할 필요는 없을 것 같아..
추후 수정하거나 참고자료처럼 redis를 써 보는 것도 고려 중..!

일단 지금 작성해 둔 controller는 다음과 같다.

controllers/authController.js

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);
    }
}

accessTokenrefreshToken이 포함되어 있는 headers를 services 로직으로 보내준다.

4. 로그아웃 관련 services

services/authServices.js

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에서는 각각 authorizationrefresh 키에 담긴 accessTokenrefreshToken을 받아서

해당 refreshToken을 발급받은 사용자를 찾아 refreshToken을 DB에서 삭제하는 과정을 거친다.

그리고 로그아웃한 사용자 정보를 함께 반환해 줌!

해당 반환값을 넘겨 받은 controller에서 반환값의 success 여부에 따라 적절한 응답값을 보내주는 것으로 구분하였다.

비교적 급한 작업(?)들이 끝나면, redis 사용도 꼭 도전해 보고 싶다! :)

profile
주니어 백엔드 개발자입니다! 조용한 시간에 읽고 쓰는 것을 좋아합니다 :)

0개의 댓글