본문 바로가기
개발이야기

JWT 인증방식

by Chars4785 2022. 3. 29.

 

교회 인적관리 시스템을 만들게 될 기회가 생겨서 개발하게 되었다. 웹페이지를 처음 개발할때 중요하게 고려해야 할 부분은 로그인과 api 요청로직이었습니다. 그래서 로그인 유지를 위해서 사용한 상태 관리는 비즈니스 로직이 확실하고 미들웨어 장점을 갖고 있는 redux와 미들웨어인 redux-saga를 사용했습니다. api 요청의 보안 부분은 token 인증 방식인 jwt을 기반으로 개발을 시작했습니다. 해당 부분을 개발하면서 만났던 문제와 해결 방안에 대해서 작성해보았습니다.

로그인 로직은 우선 페이지에서 사용자가 입력한 정보가 서버에 있는지 확인후, access token과 refresh token을 발급받고 다시 사용자 정보를 가져와서 상태 값에 초기화 하도록 개발했습니다.

function* getUserInfoAction(){
    try{
        const data = yield call( request, {
            url: `${NetworkConfig.AUTH_URL}/userInfo`,
            method: 'GET',
        })
        yield put({
            type: userAction.signSuccessAction,
            user: data
        })
    }catch( e ){
        yield put( catchError( e ) )
    }
}

function* signInAction({ payload }){
    const { userId, password } = payload
    try{
        const data = yield call( requestNoAuth,{
            url: `${NetworkConfig.AUTH_URL}/sign/token`,
            method: 'GET',
            params: { userId, password } 
        })
        setItem( 'APP_TOKEN', JSON.stringify( data ) );
        GlobalDataManager.setAuthInfo( data );
        yield call( getUserInfoAction )
    }catch( e ){
        yield put( catchError( e ) )
    }
}

이와 같이 처음에 로그인 할때는 token 인증이 필요없는 signInAction로 실행된후에 access token 이 있는 getUserInfoAction으로 사용자 정보를 state에 초기화 시키도록 개발했습니다. 하지만 만료된 token으로 api 요청을 했을때 다시 재발급을 받는 로직이 없는 문제가 있었습니다. 그래서 access token이 만료가 되면 정보를 받아올수 없기 때문에 사용자가 갖고 있던 refresh token을 통해서 재발급을 받는 로직이 필요했습니다. 또한 이전에 요청했던 api를 재요청해서 token expire error 에 대한 대응도 필요했습니다.

export default async function request( option ){
    if ( !option.headers ) {
        option.headers = GlobalDataManager.getHeaders()
    }
    try{
        const { data } = await axios( option )
        return data
    }catch({ response, request }){
        if ( response  && response.status === 401 ){
            const data = await requestNoAuth({
                url: `${NetworkConfig.AUTH_URL}/sign/auth`,
                method: 'POST',
                data: option.headers
            })
            if( data?.accessToken && data?.refreshToken ){
                GlobalDataManager.setAuthInfo( data );
                request({
                               ...option,
                              headers:GlobalDataManager.getHeaders()
                })
                return
            }else{
                throw new Error( "refreshToken expire" )
            }
        }else{
            throw new Error( "login please" )
        }
    }
}

위와 같이 api를 쏘는 request 함수 안에 만료된 access token을 다시 재발급해주고 이전에 요청했던 api를 다시 요청하는 방식으로 개발했습니다. 정상적으로 작동하는것 같았지만 새로고침과 페이지 이동으로 인해서 state 안에 token 값이 없어지는 현상이 발생하게 되었습니다. 그래서 새로고침을 해도 유지할수 있도록 캐시를 적용하게 되었습니다. LocalStorage 로 가볍게 개발을 했지만, 캐시에서 값을 꺼내고 저장하는 속도가 느려서 api를 요청할때 마다 캐시에서 꺼내면 token 값이 없어 계속 오류가 발생했습니다.

export default class GlobalDataManager {
    static setAuthInfo( auth ){
        authInfo = auth
    }
    static getHeaders(){
        const headers = { ...defaultHeaders }
        if ( authInfo?.accessToken ) {
            headers.authorization = `Bearer ${ authInfo.accessToken }`
        }
        return headers
    }
}

그래서 global 변수를 통해서 header 값에 token정보를 저장하고 캐시에도 저장하는 방식으로 해서 속도를 이슈를 해결해 token 값으로 api를 재요청하도록 개발했습니다.

이렇게 로그인 유지와 jwt 인증방법에 대한 개발을 완료 했지만 아직 개선점이 필요했습니다. 실제 서비스를 하게 되면 token 만료시 로그인 페이지로 이동 및 웹 페이지 사용중 재발급 로직후 사용자 정보 다시 가져오는 부분 등.. 개선을 해야 하는 부분이 많습니다.

해당 웹 서비스는 서버, 앱 모두 같은 javascript 로 개발을 하게 되면서 서버와 앱, 웹을 모두 이해 할수 있는 좋은 프로젝트라고 생각이 듭니다. 처음부터 서비스의 기본을 구축하고 있어서 프로젝트에 대한 이해도가 높아진다는 점에서 고무적이라고 생각합니다. 또한 빠르게 서비스를 런칭해서 실제 사용자들(교회사람들)의 니즈로 개발을 하면 더 개선점이 많아 질것 같고, 공부하는 재미도 많아질것 같아서 기대하면서 개발하고 있습니다.

댓글