새로운 강의는 이제 https://memi.dev 에서 진행합니다.
memi가 Vue & Firebase로 직접 만든 새로운 사이트를 소개합니다.

바로가기


NEMBV 20 jwt를 이용한 인증

4 분 소요

이 강좌는 종료되었습니다.
새로운 강좌로 시작하세요~
모던웹(NEMV) 제작 강좌

이제 임시 토큰이 아닌 실제 토큰을 사용한 인증 구현을 해보겠다.

준비 과정

jwt 설치

back-end(이하 be)에 jwt를 설치한다.

$ npm i jsonwebtoken --save

cfg.js 에 secret-key 등록

module.exports = {
  web: {
    host: 'xxx.com',
    secret_key: 'veryhardultrakey',
  },
};    
  • 발급자로 사용될 host를 넣어준다.
  • 패스워드를 평문으로 저장하지 않고 시크릿키로 만든 해쉬값으로 저장하기 위함
  • jwt 발급할 때도 필요
  • 당연히 어렵게 만들어야 한다.

app.js 에 key set

app.set('jwt-secret', cfg.web.secret_key);  // add
app.use('/api', require('./routes/api'));

api route 위에 키셋을 해준다.

토큰 발급

로그인 성공시 토큰을 발급해준다.

fe/src/components/auth/sign/ctrls.js

User.findOne()
    .where('id').equals(id)
    .then((r) => {
      if (!r) throw new Error('id not exists');
      if (pwd !== r.pwd) throw new Error('password diff');

      const secret = req.app.get('jwt-secret');
      const p = new Promise((resolve, reject) => {
        jwt.sign(
          {
            _id: r._id,
            id: r.id,
            email: r.email
          },
          secret,
          {
            expiresIn: '2m',
            issuer: cfg.web.host,
            subject: 'user-token'
          }, (err, token) => {
            if (err) reject(err);
            resolve(token);
          })
      })
      return p;
    })
    .then((tk) => {
      res.send({ success: true, token: tk });
    })
  • set 해두었던 jwt-secret을 이용해 토큰을 만든다.
  • 시험용으로 만기시간(expiresIn)을 2분으로 낮게 잡았다.
  • 유효기간 2분짜리 토큰을 전송한다.

발급된 토큰 저장 및 페이지 이동

토큰을 받고 어딘가에 저장해두고 매번 요청할 때 마다 헤더에 넣어 보낸다.

vue-cookie 를 이용 해서 저장한다.

$ cd fe
$ npm i vue-cookie --save

저장할 수 있는 아무 위치나 상관 없다. 호환성 이슈가 그나마 덜한 쿠키로 선택했을 뿐..

전역 axios 응답 가로채서 저장 및 페이지 이동

로그인 페이지에서 쿠키를 저장하고 페이지 이동할 수도 있지만.. 다른 페이지도 감안해서 전역으로 처리하였다.

fe/src/main.js

import VueCookie from 'vue-cookie';

const token = VueCookie.get('token');
if (token) axios.defaults.headers.common.Authorization = VueCookie.get('token');
axios.interceptors.response.use((res) => {
  if (res.data.token) {
    VueCookie.set('token', res.data.token, { expires: '2m' });
    axios.defaults.headers.common.Authorization = VueCookie.get('token');
  }
  return Promise.resolve(res);
}, (err) => {
  if (err.response.status === 401) {
    location.href = '/#/sign';
    return;
  }
  return Promise.reject(err);
});
  • axios의 모든 응답은 이곳을 거친다.
  • cookie를 가져와서 있다면 공용헤더 인증에 넣어준다.
  • sign.vue에서 요청을 할 경우 응답에 토큰이 있으니, cookie에 저장하고 공용헤더 인증에 넣어준다.
  • 쿠키 만료시간도 역시 2분으로 주었다. 1년으로 준다 하더라도 api에서 리젝당하니 별 의미는 없다.
  • 다른 페이지 요청시 토큰이 없을 경우 그대로 res를 넘겨준다.
  • 다른 페이지 요청시 응답에러에 401이 있을 경우 로그인 페이지로 이동 시킨다.

로그인 페이지 응답 완료 후

signin(evt) {
  evt.preventDefault();
  this.$axios.post(`${this.$cfg.path.api}${this.path}`, this.form)
    .then((res) => {
      if (!res.data.success) throw new Error(res.data.msg);
      return this.swalSuccess(`token: ${res.data.token}`);
    })
    .then(() => {
      location.href = '/#/';
    })
    .catch((err) => {
      this.swalError(err.message);
    });
},

토큰데이터 창을 한번 보여주고 메인페이지로 이동 시킨다.

토큰 확인

미들웨어 작성

모든 /api/ 요청에 대해 토큰을 검사하는 루틴을 추가한다.

routes/api/index.js

const check = require('./check');
router.all('*', check.verify);
  • 기존 작성해두었던 middleware 부분을 따로 check.js에 따로 정리했다.
  • verify는 헤더의 토큰 검사용이며, 다른 미들웨어를 추가 할 수도 있다.

routes/api/check.js

const jwt = require('jsonwebtoken');

exports.verify = (req, res, next) => {
  // console.log(req.headers.authorization);
  // console.log(req.path);
  if (req.path === '/auth/sign/in') return next();
  if (req.path === '/auth/register') return next();
  if (!req.headers.authorization) return res.status(401).send({ success: false, msg: 'authorization empty' });

  const token = req.headers.authorization;

  jwt.verify(token, req.app.get('jwt-secret'), (err, d) => {
    if (err) return res.status(401).send({ success: false, msg: 'your token expired' });
    console.log(new Date(d.exp*1000).toLocaleString());
    req.token = d;
    console.log(req.token);
    next();
  });
};
  • 로그인 페이지나 회원가입 페이지에서는 토큰이 없으므로 다음 라우터로 간다.
  • 토큰이 비었으면 401에러를 보내준다.
  • 시크릿키로 토큰을 디코드한다.
  • 만료되었거나 잘못된 문자열이면 에러에 걸린다.
  • req.token에 변환된 값을 저장해둔다. 다음 라우트에서 req.token의 값으로 커스텀한 응답을 줄수 있다. eg) req.token.lang === ‘ko’ 면 한글로
  • 변환된 값: 위에 console.log 된 부분
    { _id: '5abb2c234bed101776c74eb6',
    id: 'aaaa',
    email: 'a@a.aaa',
    iat: 1522238762, // 발급한 시간
    exp: 1522238882, // 만료 시간
    iss: 'localhost:3000', // 발급한 서버
    sub: 'user-token' }
    exp 변환값 : 2018-3-28 21:08:02
    
  • iat, exp는 unixtimestamp로 4바이트 정수이므로 시간을 찍어 볼 수 있었다.

테스트

토큰이 없을 경우 로그인 페이지 강제 이동

sign

로그인 성공후 개발용 토큰 확인 창

sign dev

확인창을 누른후 루트로 이동

intro

인트로 페이지는 api를 사용하지 않기 때문에 원래 로그인 안해도 들어와진다..

api를 사용하는 아무페이지나 갔을 때 토큰이 확인되어 제대로 동작

api use

토큰 만료시간인 2분이 지나고 api사용하는 아무페이지 누를 시 다시 로그인 페이지로 이동

api not use

전체 소스

https://github.com/fkkmemi/nembv

실제 테스트

만들어둔 id: aaaa, password: aaaa 로 아래 링크에서 시험해볼 수 있다.

http://fkkmemi.com:3000

결론

구현은 되었는데 생각난 데로 코딩 하면서 작성하다보니 구멍이 많아 보인다.

해야할 일들

  • sign/out
  • user list
  • 잘못된 에러메세지 처리
  • 패스워드 암호화
    현재 생텍스트
  • authorization 에 bearer 추가
    토큰 앞에 보통 붙히는데 스터디해봐야 안다. 현재 모름
  • vue route 전단에서 요청 보내서 맞는 페이지 라우팅하기
    현재 api를 안쓰는 페이지(eg: /#/test/hello등)가 그대로 노출되는데 어떤 방식이 맞는지는 사용해봐야 알 것 같다
  • 토큰 재발행
    요청시 적당한 시간을 봐서 다시 토큰을 줘야할텐데.. 정상적인 데이터와 함께 발행하는 방법을 생각중이다.
  • 현재 접속자 확인 디비
    요청시 세션처럼 접속자 수와 상호 연계가 필요할 것 같다

댓글남기기