[HTTPS / JWT] 로그인 - 로그아웃 로직 구현하기

young·2022년 7월 17일
3

Learn more

목록 보기
14/22

지난번 <쿠키를 이용한 로그인-로그아웃 로직 구현하기>에서 사용한 client 코드를 그대로 사용했다.

토큰의 일종인 JWT를 이용하여 토큰인증 방식을 구현한다.

아래 명령어로 발급받은 HTTPS 인증서 key.pem, cert.pem를 서버 index.js와 같은 디렉토리에 저장한다.

$ brew install mkcert
$ mkcert -install
$ mkcert -key-file key.pem -cert-file cert.pem localhost 127.0.0.1 ::1

//server/index.js
//mkcert에서 발급한 인증서를 사용하기 위한 코드
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

key.pem, cert.pem, .env (secret key) 는 절대 절대 절대 비밀이므로 주의하자




📌 서버 환경 변수 설정

🧩 .env

Node.js의 process.env는 사용자 환경 변수를 담는 객체다.
value는 문자열로 자동 형변환된다.

ACCESS_SECRET=secret key
REFRESH_SECRET=secret key

위와 같이 jwt.sign 함수에서 사용할 시크릿 키를 설정해주었다.

jwt.sign(payload, secretOrPrivateKey, [options, callback])
: Returns the JsonWebToken as string

jwt.sign function takes the payload, secret and options as its arguments. The payload can be used to find out which user is the owner of the token. Options can have an expire time until which token is valid. The generated token will be a string.-출처-




📌 서버 CORS 및 프로토콜 설정

🧩 server/index.js

CORS 요청을 받기 위해 credentials = true를 작성하는 것에 대해서는 지난번 글에 작성해두었다.

const express = require('express');
const cors = require('cors');
const app = express();

app.use(
  cors({
    origin: 'http://localhost:3000',
    methods: ['GET', 'POST', 'OPTIONS'],
    credentials: true, //client에서도 withCredentials=true 설정해야 함
  })
);

인증서 파일이 존재하는 경우에만 HTTPS 프로토콜을 사용하는 서버 실행
없는 경우 HTTP 프로토콜을 사용하는 서버 실행

let server;
if (fs.existsSync('./key.pem') && fs.existsSync('./cert.pem')) {
  const privateKey = fs.readFileSync(__dirname + '/key.pem', 'utf8');
  const certificate = fs.readFileSync(__dirname + '/cert.pem', 'utf8');
  const credentials = {
    key: privateKey,
    cert: certificate,
  };

  server = https.createServer(credentials, app);
  server.listen(HTTPS_PORT, () => console.log(`🚀 HTTPS Server is starting on ${HTTPS_PORT}`));
} else {
  server = app.listen(HTTPS_PORT, () => console.log(`🚀 HTTP Server is starting on ${HTTPS_PORT}`));
}



📌 서버 로그인 기능 구현

🧩 server/controllers/login.js

입력한 정보가 DB의 정보와 일치하는 경우 클라이언트에게 로그인 성공에 대한 응답 전송 (/userinfo로 리다이렉트)
이때, 함께 보내는 쿠키에 JWT를 담아 전송한다.

  • 로그인 유지하기: access token & refresh token 전송
  • 로그인 유지 안함: access token만 전송
    
    입력한 정보가 DB에 없을 경우 로그인 실패에 대한 응답 전송

access token: 접근 권한 부여
refresh token: 사용자 편의를 위한 토큰 (e.g. 로그인 유지)

//userInfo = 사용자 입력값과 일치하는 DB의 유저 정보를 담은 객체
//빈 객체일 경우 일치하는 유저 정보가 없는 것
if(!userInfo.userId) {
    return res.status(401).send("Not Authorized")
};

//async로 구현된 토큰 생성자 generateToken 실행
//generateToken = checkedKeepLogin여부에 따른 (accesstoken만 또는 accesstoken + refreshtoken) 생성자
const { accessToken, refreshToken } = await generateToken(userInfo, checkedKeepLogin);

  //로그인 유지를 체크했을 경우 refresh token 생성됨
  if (refreshToken) {
  res.cookie('refresh_jwt', refreshToken, {
    domain: 'localhost',
    path: '/',
    sameSite: 'none',
    httpOnly: true,
    secure: true,
    //⭐️ refreshToken의 expires는 클라이언트가 받은 쿠키가 언제까지 유지될 것인지 설정한다.
    expires: new Date(Date.now() + 24 * 3600 * 1000 * 7)  //7일 후 소멸되는 persistent cookie
  })
}

//로그인 유지 안함
//access token 기본제공
  res.cookie('access_jwt', accessToken, {
    domain: 'localhost',
    path: '/',
    sameSite: 'none',
    httpOnly: true,
    secure: true,
    //expires 옵션이 없는 session cookie
  })

  return res.redirect('/userinfo')

토큰 만료기간 !== 쿠키 만료기간

refresh token은 영속성 쿠키로 보내고 (expires)
access token은 session 쿠키로 보낸다. (expires 옵션 없음)




📌 서버 마이페이지 기능 구현

🧩 server/userinfo.js

먼저 access token을 만들때 사용한 secret을 사용한 건지 검증한다.
1. 검증이 성공하면 복호화된 payload를 이용하여 DB에서 유저 정보 조회 가능 => 로그인 성공에 대한 응답
2. access token이 만료되었다면 refresh token을 검증해 access token 재발급
3. access token과 refresh token 모두 만료되었다면 로그인 실패에 대한 응답 전송

module.exports = async (req, res) => {
  //쿠키를 받는 곳
  const accessToken = req.cookies['access_jwt'];
  const refreshToken = req.cookies['refresh_jwt'];

  //토큰을 만들때 사용한 secret을 가지고 온건지 검증한다.
  const accessPayload = await verifyToken('access', accessToken);
  
  //case access token이 검증됐을 경우
  if(accessPayload) {
    const userInfo = { ...USER_DATA.filter((user) => user.id === accessPayload.id)[0]};
    //복호화된 값을 DB와 비교
    if(!userInfo) {
      return res.status(401).send('Not Authorized')
    }
    //password는 delete등으로 삭제하여 보낸다
    return res.json({ ...userInfo, password: undefined})
  }
  
  //case access token이 만료되어 refresh token을 검증하는 경우
  else if(refreshToken) {
    const refreshPayload = await verifyToken('refresh', refreshToken);
    //refresh token 복호화 결과를 담은 refreshPayload
    if (!refreshPayload) {
      return res.status(401).send('Not Authorized')
    }
    //검증된 경우 userinfo로 accesstoken을 다시 발급
    //refresh token 유효시간이 이내일 경우 이와 같이 로그인 유지가 가능하다
    const userInfo = USER_DATA.filter((user) => user.id === refreshPayload.id)[0]
    const { accessToken } = await generateToken(userInfo);

    res.cookie('access_jwt', accessToken, {
      domain: 'localhost',
      path: '/',
      sameSite: 'none',
      httpOnly: true,
      secure: true,
      // Expires 옵션이 없는 Session Cookie
    });
    //password는 delete등으로 삭제하여 보낸다
    return res.json({ ...userInfo, password: undefined });
  }
  return res.status(401).send('Not Authorized');
};

verify token을 한 뒤에 문제가 없으면 accessPayload라는 결과가 나온다.
accessPayload 안에는 decoded 문자열이 있다.
decoded가 되지 않으면 accessPayload === null
따라서 if(accessPayload)는 access token이 검증된 경우가 된다.
이 조건문 내부에서 decoded 값을 DB와 비교해야 한다.




📌 서버 로그아웃 기능 구현

🧩 server/logout.js

res.clearCookie('쿠키의 key', cookieOptions) method로 쿠키를 삭제하는 것으로 로그아웃을 구현한다.
기본적으로 access token을 삭제하고 refresh token이 있는 경우 refresh token또한 삭제해야 한다.

module.exports = (req, res) => {
  const refreshToken = req.cookies['refresh_jwt'];
  
  if(refreshToken) {
    res.clearCookie('refresh_jwt', {
      domain: 'localhost',
      path: '/',
      sameSite: 'none',
      httpOnly: true,
      secure: true,
    })
  }
  
  res.clearCookie('access_jwt', {
    domain: 'localhost',
    path: '/',
    sameSite: 'none',
    httpOnly: true,
    secure: true,
  })
  
  return res.status(205).send('logged out!')
};


🏝 완성한 로그인-로그아웃 로직 정리

  1. OPTIONS /login CORS 요청

  2. POST /login

  • 로그인 성공: 302 redirect('/userinfo')
    • 로그인 유지시 refresh token(영속성쿠키) + access token(세션쿠키) 전달
    • 로그인 유지 안했을시 access token만 세션쿠키로 전달
  • 로그인 실패: 401 Not Authorized
  1. OPTIONS /userinfo CORS 요청

  2. GET /userinfo 304 (로그인된 상태로 브라우저 새로고침 및 재접속 했을시 여기부터 시작)

  • access token 검증
    • 검증됐을 경우 DB의 유저 정보와 대조
      • 맞을시 GET 요청 성공
  • access token 만료 && refresh token 있을시 검증
    • 검증됐을 경우 refresh token과 DB의 유저 정보와 대조
      • 맞을시 access token 생성하여 세션쿠키로 전달 && 로그인 유지 성공
  • 모든 검증에 실패(유효 시간 만료)했을 경우 401 Not Authorized
    (토큰이 만료되어 재로그인 해야한다)
  1. POST /logout 205
    access token(과 경우에 따라 refresh token까지) 포함하는 쿠키를 삭제한다. => 즉시 로그인이 해제됨으로 로그아웃 상태가 된다.



Learn more...

JWT 깃허브
JWT 홈페이지

profile
즐겁게 공부하고 꾸준히 기록하는 나의 프론트엔드 공부일지

0개의 댓글