2019. 06. 22 수정


1. JWT ( Json Web Token )

많은 웹 서비스들은 사용자 인증을 구현하기 위해서 쿠키와 세션을 이용해왔습니다.

그런데 쿠키와 세션에는 여러 문제들이 있어서, 최근에는 OAuth와 JWT 같은 토큰 기반의 인증 방식이 주로 사용되고 있습니다.


OAuth는 쉽게 말해서 "페이스북으로 로그인하기", "구글로 로그인하기"와 같이 다른 애플리케이션으로 사용자 인증을 인가하는 것입니다.

그러면 페이스북, 구글 같이 OAuth 인증을 허가하는 서버에서 토큰을 발급해주고 그 토큰을 얻음으로써 어떤 권한을 얻게 되는 것이죠.

( OAuth에 대한 개념은 여기를 참고해주세요 ! )


이번 글에서 살펴볼 JWT는 사용자 정보를 JSON 객체에 담아 이를 암호화하고 해싱 작업을 거쳐 문자열 토큰을 생성하는 기술입니다.

클라이언트는 이 토큰을 HTTP Header에 추가하여 요청을 보냄으로써 사용자 인증을 얻게 됩니다.


JWT는 서버에 저장되지 않기 때문에 서버 부하를 일으키지 않으며, 해싱을 통해 데이터의 무결성을 보장하는 인증 방식입니다.

간단히 말하면 토큰에 사용자 정보와 권한을 명시하는 것입니다.

( JWT에 대한 개념은 여기를 참고해주세요 ! )



JWT는 많은 프로그래밍 언어에서 라이브러리로 지원하고 있으며, Node.js에서도 jsonwebtoken이라는 모듈로 제공하고 있습니다.

이제 이 모듈을 이용해서 JWT를 이용한 사용자 인증을 구현해보도록 하겠습니다.

( jsonwebtoken에 대한 깃헙 문서는 여기를 참고해주세요 ! )

( jwt-simple 이라는 모듈을 사용해서 구현할 수도 있지만, 이 글에서는 jsonwebtoken 모듈을 사용하겠습니다. )





2. 준비작업

예제에서는 express-generator로 프로젝트를 생성했고 sequelize-cli를 사용하여, 두 명의 회원을 DB에 추가한 상태입니다.



그리고 JWT를 사용하기 위해서 jsonwebtoken 모듈을 설치합니다.

# npm install jsonwebtoken





3. 예제

이번 예제는 로그인을 했을 경우 JWT를 브라우저에 넘겨줘서 어떤 API를 호출할 수 있도록 권한을 부여하는 예제입니다.

즉, 로그인이 되지 않았다면 토큰이 없기 때문에 API를 요청할 수 없는 상황입니다.

세션의 기능을 JWT를 이용해서 구현했다고 보시면 되는데, 단지 세션이 아니라 토큰을 이용한 것이죠.


전체적인 흐름은 다음과 같습니다.

사용자가 로그인을 하면 토큰을 생성해서 브라우저의 쿠키에 보관합니다.

그리고 이 사용자가 어떤 API를 호출했을 때, 토큰이 있는지 확인해서 올바른 토큰이 있을 경우 API를 실행하고, 없으면 실행하지 않습니다.



1) 비밀키 모듈 생성

JWT에서는 서명을 생성하기 위해 비밀키를 필요로 하는데, 비밀키는 외부에 노출이 되면 안되므로 .gitignore를 할 목적으로 모듈을 만들겠습니다.


config/jwt.js

let jwtObj = {};

jwtObj.secret = "apple"

module.exports = jwtObj
  • 비밀키는 apple입니다.
  • .gitignore 파일에 config/jwt.js 파일을 작성해주면, remote repository에 업로드 되지 않으므로 비밀 키(apple)을 안전하게 보관할 수 있습니다.




2) 로그인 라우터 함수 구현 - 토큰 생성

다음은 로그인 처리 및 토큰을 생성하는 라우터 미들웨어를 구현해보겠습니다.


routes/index.js

let express = require('express');
let models = require("../models");
let router = express.Router();

let jwt = require("jsonwebtoken");
let secretObj = require("../config/jwt");


router.get("/login", function(req,res,next){
// default : HMAC SHA256
let token = jwt.sign({
email: "foo@example.com" // 토큰의 내용(payload)
},
secretObj.secret , // 비밀 키
{
expiresIn: '5m' // 유효 시간은 5분
})


models.user.find({
where: {
email: "foo@example.com"
}
})
.then( user => {
if(user.pwd === "1234"){
res.cookie("user", token);
res.json({
token: token
})
}
})
})

module.exports = router;

/login 요청 처리의 대략적인 흐름은 다음과 같습니다.

  1. jwt 객체가 sign() 메서드를 호출해서 토큰을 생성합니다.
  2. sequelize를 사용해서 요청한 이메일 주소에 해당하는 정보를 DB에서 조회합니다.
  3. 해당 객체의 비밀번호가 맞으면 쿠키에 user라는 이름으로 token값을 저장합니다.
  4. 쿠키는 위변조의 위험이 있지만, JWT는 암호화와 해싱을 거치기 때문에 무결성을 보장합니다.


다음으로 sign() 메서드에 대해 구체적으로 알아보겠습니다.


( sign() 메서드에 대한 API는 여기를 참고해주세요 ! )

  • sign() 메서드는 기본 값으로 HMAC SHA256 알고리즘을 사용하는데요, 위의 링크를 참고하시면 해싱하는 알고리즘을 변경할 수도 있습니다.
  • 첫 번째 인자로는 payload, 즉 내용을 작성합니다.
    • 그런데 claim(권한)에 대한 정보는 암호화를 하지 않으므로 중요한 정보는 작성하지 않는 것이 좋습니다.
    • 따라서 저는 email에 대한 정보만 작성했습니다.
  • 두 번째 인자에는 비밀키를 전달합니다.
    • 비밀키는 .gitignore로 숨겨놓을 것이기 때문에, 저는 첫 번째 단계에서 생성했던 모듈의 속성을 호출했습니다.
  • 세 번째 인자는 토큰에 대한 정보를 객체로 전달합니다.
    • 여기서는 유효시간이 5분이라는 정보만 추가했습니다.
  • 네 번째 인자에는 콜백 함수를 작성합니다.
    • 주의할 것은 콜백함수를 작성하지 않으면 동기처리가 된다는 것입니다.




3) /login 요청

이제 브라우저에서 localhost:3000/login 으로 요청을 보낸 후, 개발자 도구에서 쿠키를 확인해보겠습니다.



그러면 위와 같이 user라는 이름으로 token 값이 쿠키로 저장된 것을 확인할 수 있습니다.




4) Token 디버거

다음으로 이 token 값이 정말 잘 작성된 것인지 디버거를 해보겠습니다.


eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BleGFtcGxlLmNvbSIsImlhdCI6MTUxNjYwNTEzNSwiZXhwIjoxNTE2NjA1NDM1fQ._Bt8E0QnWOjEeyOp7jYOKTgehf2F-XuyVy9XTmhjAac

  • 위의 토큰 값을 복사하고, 사이트에서 좌측의 Encoded 부분에 붙여넣기 합니다.
  • 그리고 우측의 VERIFY SIGNATURE의 네모 박스 부분에 secret key 를 작성해주면 됩니다.
    • 저는 아까 apple이라고 했으므로 apple을 작성했습니다.
  • 그러면 payload 부분에 email에 대한 정보가 조회되는 것을 확인할 수 있습니다.
    • 또한 exp - iat 뺄셈의 결과는 300인데, 이는 300초를 의미합니다. 그 이유는 JWT의 유효시간을 5분으로 설정했기 때문이죠.


지금까지 /login을 요청하면, JWT토큰을 생성해서 브라우저의 쿠키에 저장까지 해보았습니다.




5) 사용자 확인하기

이제 토큰을 발급 받는 방법을 알았으니 실제로 토큰을 사용하는 목적인, 사용자를 확인하는 방법에 대해 알아보겠습니다.


우선 같은 라우터 미들웨어에 API를 호출할 수 있도록 구현해봅니다.

routes/index.js

router.get("/someAPI", function(req, res, next){
let token = req.cookies.user;

let decoded = jwt.verify(token, secretObj.secret);
if(decoded){
res.send("권한이 있어서 API 수행 가능")
}
else{
res.send("권한이 없습니다.")
}
})

jsonwebtoken 모듈에서 권한을 확인하는 메서드는 verify( ) 메서드 입니다.


( API는 여기를 참고해주세요 ! )


  • 첫 번째 인자로는 token 값을 전달합니다.
    • 인증이 된 유효한 토큰인지 확인하기 위한 것이므로 토큰이 필요합니다.
    • 토큰은 쿠키에 저장되어 있으므로 요청 객체에서 cookies 속성을 참조하면 됩니다. ( express에서는 자동으로 cookieparser 미들웨어가 등록되어 있습니다. )
  • 두 번째 인자에는 secret key 값을 작성합니다.
    • 디코딩 하기 위해서는 인코딩 할 때와 같은 secret key가 필요합니다.


예제의 시나리오상 로그인을 한 사용자는 JWT 토큰이 있기 때문에, 어떤 API를 수행할 수 있는 권한이 있습니다.

예제에서는 그 API의 URL을 localhost:3000/someAPI 라고 가정했고 해당 URL로 요청을 보내면, 쿠키에서 토큰 값을 읽고 verify() 메서드를 호출하여 json 객체를 반환하게 됩니다.


즉, decoded 변수에는 아래와 같은 json 객체가 할당됩니다.



어쨋든 로그인을 했던 사용자는 토큰이 있으므로 실행결과는 "권한이 있어서 API 수행 가능"이 됩니다.


이상으로 토큰을 발급받은 사용자에 한 해 API를 호출할 수 있도록 하는 예제를 구현해보았습니다!




6) 토큰의 유효시간 만료

마지막으로, 토큰의 유효시간을 5분으로 설정했기 때문에 5분이 지나면 API를 요청할 수 있는 권한이 없어야 합니다.

실제로 5분이 지난 후 localhost:3000/someAPI에 요청을 보내면 아래와 같은 에러 메시지를 볼 수 있습니다.



이것으로 토큰에 대한 설정이 잘 되었음을 확인할 수 있습니다.





이상으로 JWT을 사용해서 사용자 인증을 구현해보았습니다.

어떤 API가 사용자 권한이 필요할 경우 요청한 클라이언트의 쿠키의 토큰을 확인해서 유효한 토큰인지 확인하여, API를 호출 할 수 있도록 하는 것이 이번 글의 주제였습니다.


구현 자체는 쉬울 수 있는데 내부적인 동작과정( jsonwebtoken 모듈 )을 이해하는 것이 조금 어려웠습니다. 


  1. utaha 2019.03.17 17:07

    쿠키에 토큰을 넣으셨는데 원래 쿠키에 토큰을 많이 넣나요?

    • Favicon of https://victorydntmd.tistory.com victolee 우르르응 2019.03.18 19:41 신고

      안녕하세요~

      네 그렇습니다.
      JWT는 브라우저에 저장되어야 하는데, 토큰을 local storage, session storage에 저장하게 되면 데이터를 탈취당할 위험이 큽니다.
      그래서 HttpOnly 옵션을 준 Cookie에 저장하는 것이 좋습니다

      https://logrocket.com/blog/jwt-authentication-best-practices#store-jwts-securely
      이 글을 참고하시면 좋을 것 같아요~

  2. utaha 2019.03.19 21:56

    답변 해주셔서 고맙습니다 ㅎㅎ

+ Recent posts