티스토리 뷰

728x90
반응형

vue 정리 - login (로그인) part2

 

 

login par1.  에 이어서 jwt 즉 json web token 설정에 대해서 정리해보고자 한다.

 

이제 vue 에서 token 정보 저장 후 바로 headers 의 Authorization 에 token 을 담아서 사용자 정보 호출하는

것을 살펴 보자.

 

우선 보통은 axios 로 통신할 때 headers 설정을 아래와 같이 하게 된다.

axios.post(`통신할 api 주소`, { id, password }, 
{
  headers: {
       'Authorization': `Bearer 토큰값`
});

하지만 통신할 api 들이 많은데 매번 저렇게 headers 부분을 삽입해서 전송하기엔 좀 거시기 하다...

그래서 axios 의 intercepters 를 활용해서 해당 부분을 해결할 수 있다.

 

아래는 axios 설정에 같이 로드하여 쓸 수 있게 intercepters 만 별도의 클래스를 만들었다.

 

jwtService.ts

import store from '@/store';
import router from '@/router';
import AuthConfig from '@/core/auth/config';
import { AxiosRequestConfig, AxiosResponse, AxiosStatic } from 'axios';
import { getToken } from '@/core/auth/utils';
import { AuthService } from '@/restApi';
import { UserMutationType } from '@/store/moduleType/AuthMutationTypes';
import { TokenActionType } from '@/store/moduleType/AuthActionTypes';

export default class JwtService{

    private readonly axiosInstance: AxiosStatic;
    //대기요청 상태인지 체크 toggle 변수
    private isTokenRefreshCheck: boolean=false;
    //콜백함수 타입의 배열
    private refreshSubscribers: Array<(token: string)=>void>=[];

    constructor(axiosInstance: AxiosStatic ) {

        this.axiosInstance=axiosInstance;
        /**
         * request interceptor
         */
        this.axiosInstance.interceptors.request.use( (config: AxiosRequestConfig)=>{
            const token=getToken();
            //토큰이 localstorage 에 있을 때만 header 에 토큰을 심어둔다.
            if (token) {
                //config.headers.Authorization 과 axios.defaults.headers.common.Authorization 은 서로 다르다.
                config.headers.Authorization=`${AuthConfig.TOKEN_TYPE}${token}`;
            }
            return config;
        }, (error: any)=>{
            return Promise.reject( error );
        } );


        /**
         * response interceptor
         */
        this.axiosInstance.interceptors.response.use( (response: AxiosResponse)=>{
            return response;
        }, async (error: any)=>{
            const { status,  config, data }=error.response;
            const responseConfig=config;

            if(status === 401 ){
                //로그인 실패시~
                if (String( config.url ).includes( 'auth/login' ) && data.code === 700) {
                    store.commit( `Auth/${ UserMutationType.LOG_IN }`, false );
                }else if(data.code===611){
                    if (!this.isTokenRefreshCheck) {
                        // isTokenRefreshing 이 false 인 경우에만 token refresh 요청
                        this.isTokenRefreshCheck=true;
                        // refresh token 요청 
                        AuthService.fetchRefreshToken().then( (res: any)=>{
                            this.isTokenRefreshCheck=false;
                            const { accessToken, refreshToken }=res;
                            this.setTokens( accessToken, refreshToken );

                            setTimeout( async ()=>{
                                // 새로운 토큰으로 지연되었던 요청 진행
                                await this.getTokenRefreshed( accessToken );
                                //저장 배열 초기화
                                await this.removeRefreshSubscribers();
                            }, 700 );

                        } ).catch( (error: any)=>{
                            const { code, message, status }=error.data;
                            // console.log( error, code, message );
                            // refresh token 정보도 만료 되었을 때 로그인 페이지로 보낸다.
                            if (status === 401 && message === 'token expired') {
                                alert( '사용자 정보가 만료되었습니다.\\n 다시 로그인 해주세요' );
                                //로그아웃
                                this.shouldUnAuthorized();
                            }
                        } );

                    }

                    //  token 이 재발급 되는 동안의 요청은 refreshSubscribers 에 저장
                    return new Promise( (resolve)=>{
                        //getTokenRefreshed 에서 전달된 token 을  내부에서 refreshSubscribers( 콜백함수 저장한 배열 ) 를 forEach 로 순환 대입( 전달된 token) 실행시킨다.
                        this.addRefreshSubscriber( (token: string)=>{
                            responseConfig.headers.Authorization=`${ AuthConfig.TOKEN_TYPE }${ token }`;
                            resolve( this.axiosInstance( responseConfig ) );
                        } );
                    } );
                }else if (data.code === 613) { //613 : 갱신 토큰 만료 >> '화면 이동 (로그인)'
                    alert( '사용자 정보가 만료되었습니다. 로그인 해 주세요~' );
                    //로그아웃 시키기.
                    await this.shouldUnAuthorized();
                }else if (data.code === 612) {
                    alert( '인증되지 않은 사용자 입니다. 로그인 해 주세요~' );
                    await this.shouldUnAuthorized();
                }
            }
            // Do something with response error
            return Promise.reject( error );
        } );
    }


    /**
     * 새로 발급 받는 token 재지정.
     * @param token
     * @param refreshToken
     */
    public async setTokens( token: string, refreshToken: string){
        await store.dispatch( `Auth/${ TokenActionType.SIGN_IN_BY_TOKEN }`, { token, refreshToken } );
    };

    /**
     * 콜백함수 타입의 배열 초기화
     */
    private removeRefreshSubscribers(){
        this.refreshSubscribers=[];
    };

    /**
     * 실행 콜백함수 배열 대입.
     * @param callback
     */
    private addRefreshSubscriber( callback: (token: string)=> void){
        this.refreshSubscribers.push( callback );
    };

    /**
     * 배열에 저장된 콜백함수( addRefreshSubscriber ) 실행.
     * @param token
     */
    private getTokenRefreshed(token: string){
        this.refreshSubscribers.forEach( (callback: (token: string)=>void)=>callback( token ) );
    };

    /**
     * 로그아웃 시키기
     */
    private shouldUnAuthorized(){
        ///login?rPath=${encodeURIComponent(location.pathname)}
        store.commit( `Auth/${ UserMutationType.LOGOUT }` );
        router.push( { path: '/login', query:{rPath:new Date().getTime().toString()}} )
            .then( ()=>{
                console.log( '로그아웃' );
            } );
    }
}

- 해당  interseptors 를 이용해 api 통신마다 headers.Authorization 을 설정할 필요가 없게 되었다.

 

 그리고 마지막으로 서버에서 Authorization 체크하여 해당 사용자 정보 client ( vue ) 에 전송은 이전에 AuthModule.ts 에서

store action 부분인 UserActionType.LOGIN 에서 처리가 된다.

 

아래는 AuthModule.ts 에서 해당 부분만 발췌했다.

    /**
     * 로그인~
     * @param payload
     */
    @Action( { rawError: true } )
    public [UserActionType.LOGIN](payload: { id: string, password: string }): Promise<any> {
        const { id, password }=payload;
        //LOGIN_ACTION
        return AuthService.login( id, password )
            .then( (data: any)=>{
               
                this.context.commit( TokenMutationType.GET_TOKEN, data.accessToken );
                this.context.commit( TokenMutationType.GET_REFRESH_TOKEN, data.refreshToken );

                return AdminService.getMe()
                    .then( (data: any)=>{
                        this.context.commit( UserMutationType.INFO, data );
                        return Promise.resolve( 'signin status' );
                    } );
            } ).catch( (error: any)=>{
                return Promise.reject( error );
            } );
    }

 

728x90
반응형
댓글