강디너의 개발 일지

javascript - JWT 로그인 및 세션 유지 본문

Javascript/삽질

javascript - JWT 로그인 및 세션 유지

강디너 2021. 2. 7. 16:12
728x90

이전 포스팅과 이어집니다.

광고 보고 가시겠습니다.

 

목표

1. Access Token을 이용한 API 통신

2. Refresh Token을 이용한 Access Token 갱신

3. 사용자 모르게 토큰 갱신 후 API 호출

 

아래와 같은 Flow로 사용자에게 세션이 끊켰다는것을 모르게 데이터를 보여줄 것입니다.

 

준비

front - Vue 만든거 열기

back - API 서버 열기

docker - 만들어둔 컨네이너 시작해서 DB 열기

 

Git - github.com/DinnerKang/study_vue/tree/master/todo-list

Docker - kdinner.tistory.com/99?category=312484


 

Login API 입니다.

사용자는 로그인을 해서 토큰을 얻어왔습니다.

토큰을 쿠키에 저장해줍니다. 서버와 협의 후 쿠키에 토큰 유효기간도 넣어줍니다.

현재는 1분 후 토큰이 사라지게 해놨습니다. (테스트를 편하게 하기 위해서)

    // Front - login.js
    const emailLogin = async (email, password) => {
        const data = {
            email,
            password,
        };
        try {
            const { result } = (await axios.post('/emailLogin', data)).data;
            VueCookies.set('access-token', result.access_token, '60s');
            VueCookies.set('refresh-token', result.refresh_token, '3d');
            console.log(result);
            return result;  
        } catch (e) {
            return e;
        }
    };
    
    --------------------------------------------------
    // Back
    app.post('/emailLogin', (req, res) => {
        const { email, password } = req.body;
        if (!email || !password) return res.status(500).json({ result: '아이디나 비밀번호를 입력해주세요.' });
        pool.getConnection((err, conn) => {
            if (!err) {
                conn.query('SELECT * FROM user WHERE email = ?', [email], (err, rows) => {
                    if (err) return res.status(500).json({ result: err });
                    if (rows.length === 0) return res.status(500).json({ result: '아이디가 없습니다.' });
                    if (rows[0].password === password) {
                        const token = jwt.sign({ 
                            email: req.body.email,
                            info: '토큰에 넣고싶은거',
                         }, privateKey, { expiresIn: '60s' });
                         const refreshToken = jwt.sign({ 
                            email: req.body.email,
                            info: '리프레시토큰입니다',
                         }, refreshKey, { expiresIn: '3d' });

                         conn.query('UPDATE user SET access_token = ?, refresh_token = ? WHERE email = ?', [token, refreshToken, email], (err, rows) => {
                            if (err) return res.status(500).json({ result: err });
                         });

                        return res.json({ msg: '로그인 성공', result:  { access_token: token, refresh_token: refreshToken} });
                    } else {
                        return res.status(500).json({ result: '비밀번호가 틀렸습니다.' });
                    }
                });
            }
            conn.release();
        });
    });

 

 

Test API 입니다.

간단한 토큰 검증만 하고있습니다. 만약 토큰이 없을 경우 에러 메시지를 뱉습니다.

Back 코드를 보시면 토큰 검증 오류일 경우에는 401로 주게 해놨습니다. 이러한 이유는 곧 보여드립니다.

   // Front - login.js
   const test = async () => {
        try {
            const data = await axios.get('/testAPI');
            console.log('API 성공');
            return data;
        } catch (e) {
            console.log('API 실패');
            return e;
        }
    };
    
    -------------------------------------------------------------------
    // Back
    app.get('/testAPI', (req, res) => {
        const token = req.headers['access-token'];
        // 토큰 검증
        jwt.verify(token, privateKey, (err, decoded) => {
            if (err) return res.status(401).json({ result: err });
            return res.json({ msg: '성공', result:  { msg: 'success' } });
        });
    });

 

Refresh API 입니다.

Refresh API 는 토큰이 정상이면 쿠키에 다시 넣어주는 코드입니다. 

// Front
login.js
const refreshToken = async () => {
    try {
        const { result } = (await axios.get('/refreshToken')).data;
        VueCookies.set('access-token', result.access_token);
        console.log('Refresh API 성공', result);
        return result;
    } catch (e) {
        console.log(e);
    }
}
  
 ----------------------------------------------------
 // Back
 app.get('/refreshToken', (req, res) => {
    const token = req.headers['refresh-token'];
    console.log('refresh', token);
    // 토큰 검증
    jwt.verify(token, refreshKey, (err, decoded) => {
        if (err) return res.status(500).json({ result: err });
        const token = jwt.sign({ 
            email: req.body.email,
         }, privateKey, { expiresIn: '60s' });
         return res.json({ msg: '리프레쉬 성공', result:  { access_token: token } });
    });
});

 

axios.js 입니다.

사용자에게 토큰이 만료되었다고 보여주면 안되니 Refresh Token을 이용하여 토큰을 얻은 후 API를 통신합니다.

토큰이 없을 경우 Back에서 401 status를 보내주는데 axios interceptors에서 401을 잡아주어 refresh API를 타도록 합니다.

그 후 오류 났던 API를 다시 쏴서 통신이 끊키지 않은 것 처럼 사용자에게 성공 메시지를 보여줍니다.

추가적으로 axios interceptors에서 access-token 쿠키 값이 없을 경우 login 페이지로 보내버리는 기능을 할 수도 있습니다.

axios.js
 
import axios from 'axios';
import VueCookies from 'vue-cookies';
import { refreshToken } from './login';

axios.defaults.baseURL = 'http://localhost:5000';

// Add a request interceptor
axios.interceptors.request.use(function (config) {
    // Do something before request is sent
    config.headers['access-token'] = VueCookies.get('access-token');
    config.headers['refresh-token'] = VueCookies.get('refresh-token');
    return config;
  }, function (error) {
    // Do something with request error
    return Promise.reject(error);
  });

// Add a response interceptor
axios.interceptors.response.use(function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    return response;
  }, async function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    console.log('오류:', error.config);
    const errorAPI = error.config;
    if(error.response.status  === 401 && errorAPI.retry === undefined){
      errorAPI.retry = true;
      console.log('토큰이 이상한 오류일 경우');
      await refreshToken();
      return await axios(errorAPI);
    }
    return Promise.reject(error.response);
  });

  export default axios;

 

감사합니다.

반응형
Comments