🎯 Achievement Goals


  • Token에 대해 이해할 수 있다.
  • 회원가입 및 로그인 등의 유저 인증에 대해 구현하고 이해한다.





👉 Token 기반 인증 (Token-based Authentication)


토큰 기반 시스템은 stateless 무상태이다. 즉, 상태유지를 하지 않는다. 유저의 인증 정보를 서버나 세션에 담아두지 않으므로써 어제 배웠던 세션의 문제점이 해결된다.


token


토큰기반 인증 절차

1.클라이언트가 서버에 아이디/비밀번호를 담아 로그인 요청을 보낸다.


2.아이디/비밀번호가 일치하는지 확인하고, 클라이언트에게 보낼 암호화된 토큰을 생성한다.

  • access/refresh 토큰을 모두 생성한다.
  • 토큰에 담길 정보(payload)는 유저를 식별할 정보, 권한이 부여된 카테고리(사진, 연락처) 등이 될 수 있다.
  • 비밀번호는 담기지 않는다.
  • 두 종류의 토큰이 같은 정보를 담을 필요는 없다.


3.토큰을 클라이언트에게 보내주면, 클라이언트는 토큰을 저장한다.

  • 저장하는 위치는 local storage, cookie, react의 state 등 다양하다.


4.클라이언트가 HTTP 헤더(authorization)에 토큰을 담아 보낸다.

  • bearer authentication을 이용한다.


5.서버는 토큰을 해독하여 토큰이 맞다면 클라이언트의 요청을 처리한 후 응답을 보내준다.


토큰기반 인증의 장점

1.Statelessness & Scalability (무상태성 & 확장성)

  • 서버는 클라이언트에 대한 정보를 저장할 필요는 없다. (토큰 해독이 되는지만 판단)
  • 클라이언트는 새로운 요청을 보낼때마다 토큰을 헤더에 포함시키면 된다.
  • 서버를 여러개 가지고 있는 서비스라면 더더욱 좋다. (같은 토큰으로 여러 서버에서 인증 가능)


2.안전하다.

  • 암호화된 토큰을 사용하고, 암호화 키를 노출 할 필요가 없기 때문에 안전하다.


3.어디서나 생성 가능하다.

  • 토큰을 확인하는 서버가 토큰을 만들어야 하는 법은 없다.
  • 토큰 생성용 서버를 만들거나, 다른 회사에서 토큰관련 작업을 맡기는 것 등 다양한 활용이 가능하다.


4.권한 부여에 용이하다.

  • 토큰의 payload(내용물)안에 어떤 정보에 접근 가능한지 정할 수 있다.
  • ex) 서비스의 사진과 연락처 사용권한만 부여한다.




👉 JWT (jsonwebtoken)


오늘은 토큰중에서도 가장 많이 사용하는 jsonwebtoken을 활용해서 토큰 API를 구현했다.


JWT의 종류

  • Access Token
  • Refresh Token


Access Token은 보호된 정보들(유저의 이메일, 사진 등)에 접근할 수 있는 권한부여에 사용한다.

클라이언트가 처음 인증을 받게 될 때(로그인 시), access, refresh token 두가지를 다 받지만, 실제로 권한을 얻는데 사용하는 토큰은 Access Token 이다.



jwt


1.Header
Header는 이것이 어떤 종류의 토큰인지, 어떤 알고리즘으로 할지 적혀있다.

밑에 JSON 객체를 base64로 인코딩하면 JWT의 첫 번째 블럭이 완성 된다.

{
  "alg": "HS256",
  "typ": "JWT"
}


2.Payload
payload에는 정보가 담겨 있다. 어떤 정보에 접근 가능한지에 대한 권한을 담을 수도 있고, 사용자의 유저이름 등 필요한 데이터는 이곳에 담아 암호화 시킨다. 물론 암호화(헤더에서 정의한)가 될 정보지만, 민감한 정보는 담지 않는 것이 좋다.

밑에 JSON 객체를 base64로 인코딩하면 JWT의 두 번째 블럭이 완성 된다.

{
  "sub": "someInformation",
  "name": "useonglee",
  "iat": 151623391
}


3.Signature
base64로 인코딩 된 첫번째, 그리고 두번째 부분이 완성 되었다면, 원하는 비밀 키(암호화에 추가할 salt)를 사용하여 암호화를 한다.

HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);


JWT 사용 예시

JWT는 권한 부여에 굉장히 유용하다.

예시
새로 다운 받은 A라는 앱이 Gmail과 연동되어 이메일을 읽어와야 한다고 한다면,

  1. Gmail 인증서버에 로그인 정보(아이디, 비밀번호)를 제공한다.
  2. 성공적으로 인증시 JWT를 발급받는다.
  3. A앱은 JWT를 사용해 해당 유저의 Gmail 이메일을 읽거나 사용할 수 있다.





👉 Token 인증 구현


오늘 스프린트 과제는 서버에서 Token 기반 API를 작성하고, 클라이언트에서 요청하고 응답을 받고 다시 헤더에 토큰을 넣어서 보내는 것 까지 하는 것이였다.



POST/login

로그인 요청에 응답하는 라우트이다.


const jwt = require('jsonwebtoken');
const data = await Users.findOne({
    where: { userId: req.body.userId, password: req.body.password },
  });

// 생략

if (data) {
  // payload를 만든다. 공식문서를 참고했다.
  const access_payload = {
      id: data.id,
      userId: data.userId,
      email: data.email,
      createdAt: data.createdAt,
      updatedAt: data.updatedAt,
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(Date.now() / 1000) + (60 * 60)
    }
    const refresh_payload = {
      id: data.id,
      userId: data.userId,
      email: data.email,
      createdAt: data.createdAt,
      updatedAt: data.updatedAt,
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(Date.now() / 1000) + (60 * 60)
    }
  // 토큰을 생성한다. jwt.sign 메소드 사용
    const accessToken = jwt.sign(access_payload, process.env.ACCESS_SECRET);
    const refreshToken = jwt.sign(refresh_payload, process.env.REFRESH_SECRET);

  // 성공 응답메세지를 보낸다.
    res
    .status(200)
    .cookie('refreshToken', refreshToken, {
      secure: true,
      httpsOnly: true,
      sameSite: 'none',
    })
    .send({ data: { accessToken: accessToken }, message: "ok" })
}



GET/accesstokenrequest

Access Token을 가지고 있는 클라이언트에서 보내는 유저정보 요청에 응답하는 라우트이다.


const jwt = require('jsonwebtoken');
const authorization = req.headers['authorization'];

// 생략..

if(authorization) {
  const token = authorization.split(' ')[1];

  // 암호화된 것을 해독한다.
  const data = jwt.verify(token, process.env.ACCESS_SECRET);

  const { id, userId, email, createdAt, updatedAt } = data;

  const userInfo = await Users.findOne({
    where: { userId: data.userId },
  });
  if (!userInfo) {
    return res.status(400).send({ data: null, message: "access token has been tempered" });
  }
  else {
    return res.status(200).send({ data: { userInfo: { id, userId, email, createdAt, updatedAt } }, message: "ok" })
  }
}



GET/refreshtokenrequest

Access Token이 만료되어 Refresh Token으로 새로운 Access Token을 발급받고, 유저가 요청한 정보를 반환하는 라우트이다.


// cookie에 refreshToken이 담겨있는지 확인한다.
const token = req.cookies.refreshToken;

// 생략..

if (token) {
  jwt.verify(token, process.env.REFRESH_SECRET, async (err, decoded) => {
    // refresh token이 유효한지,
    // 서버가 가지고 있는 비밀 키로 생성한 것이 맞는지 확인한다.
      if (err) {
        return res.status(400).send({ "data": null, "message": "invalid refresh token, please log in again" });
      }
      
      const userInfo = await Users.findOne({
        where: { userId: decoded.userId },
      });
    // JWT를 해독하여 얻은 payload안의 값으로 DB에 유저를 조회한다. 
      if (userInfo) {
        const { id, userId, email, createdAt, updatedAt } = decoded;
        const accessToken = jwt.sign({ id, userId, email, createdAt, updatedAt },  process.env.ACCESS_SECRET)
        return res.status(200).send({ data: { 
        accessToken: accessToken,
        userInfo: { id, userId, email, createdAt, updatedAt } }, message: "ok" })
      }
    });
}








🙌 느낀점


오늘 이유는 모르겠지만 하루종일 잠이 몰려오는 하루였다. 게다가 오늘 처음 배워본 token의 개념과 한 번에 많은 개념들을 익히려다 보니 쉽지 않았다. API문서를 구현하는 과정에서도 한 문제를 가지고 두시간동안 테스트 케이스를 통과하지 못하였는데, 그 이유 중의 하나가 오타였다….. 오타때문에 시간을 많이 허비해서 이머시브 과정 처음으로 제 시간에 못끝냈다.. 물론 수업이 끝나자마자 클라이언트 부분도 다 작성해서 바로 통과가 되었지만, 정말 집중이 안되는 하루였던 것 같다.

그리고 테스트 케이스 통과가 안 된 또 다른 하나는 GET/accesstokenrequest API문서를 작성할 때, authorization.split(‘ ‘)[1] 이렇게 헤더의 한 부분을 짤라오는 것을 if문 안에서 했어야 했는데, if문 밖에서 하는 바람에 계속 응답이 보내지질 않았다. 정말 기초적인 실수를 했다. 구현 자체는 금방했는데 이런 실수들로 인해서 시간을 많이 빼았긴 것 같다.

이런 실수를 최대한 줄이도록 천천히 로직을 생각해보면서 작성해보는 습관도 길러야겠다. 그래도 오늘 하루가 잘 마무리가 되었다. 다시 처음 부터 풀어보면서 복습해야겠다.





👊 내일의 TIW(today I Will)

OAuth