웹서비스설계및실습(Cookie Session JWT)

정유빈·2024년 10월 18일

수업 내용 정리

목록 보기
3/4

수업 내용 정리

CS 용어

JWT(JSON Web Token)

  • JSON 형식의 데이터를 저장하는 토큰

  • API 요청을 인증하는 수단으로 사용

  • 쿠키, 세션의 단점을 보완

    • 쿠키 : 암호와 안됨, 사이즈 제한(4KB), 브라우저마다 형태 다름
    • 세션 : 서버 자원을 사용
    • JWT : 토큰 변조가 불가능하고, 내용에 포함해서(암호화 상태로) 전송하므로 서버에서 DB조회를 하지 않을 수 있음(DB조회를 하지 않고 검증 가능)
  • JWT를 활용한 로그인 사례
    (Token : 데이터 베이스의 호출이 없다, 토큰을 가지고 유효한지 유효하지 않은지 알 수 있다)

    1. Access Token(JWT) 발급 : node가 Access Token(접근 토큰)을 만든다
      노드(node)는 서버에 존재한다.
    2. Token과 함께 사용자에게 응답을 보낸다
    3. 사용자는 Token을 넣어서 함께 요청한다
    4. 회원 DB의 호출없이 스스로 검증이 가능하다
  • JWT의 구성(Token의 구성 모습)

    • header : 토큰 종류와 해시 알고리즘 정보가 들어있다
    • payload : 토큰의 내용물이 인코딩된 부분
      (실제 데이터를 담고 있다, JSON 형태)
    • signature : 일련의 문자열로, 이를 통해 토큰이 변조되었는지 여부 확인(암호화 보다 인증에 가깝다)
      • JWT 비밀키로 만들어지고, 비밀키(signature에서 관리)가 노출되면 토큰 위조 가능
      • 비밀키는 별도 개발환경 안에서 관리해야함 (공개된 곳에 올라가지 않도록 주의 한다)
  • Token 자체에 중요한 정보를 넣으면 안된다
    (token log를 통해서 data 내용을 알 수 있기 때문에)

  • 토큰 검증

    • 프론트에서는 header에 넣어서 호출
    • header 와 비밀키를 넣어서 검증
      • 비밀키는 dotenv 를 통해 관리
      • process.env
    • middleware 생성
      • 오류 발생시, 그에 대한 설명을 return
      • 오류 없으면, 실제 사용자의 호출이 맞다고 가정(변조 없음)

Refresh Token

  1. Access Token과 Refresh Token (서로 다른 2개의 Token 제작)
  2. Refresh Token을 DB에 저장
  3. Access Token의 유효기간 만료
  4. 만료가 되었음을 확인
  5. 사용자에게 만료 signal 보내기
  6. Access Token을 요청하면서, Refresh Token도 같이 보낸다
  7. Refresh Token을 DB에서 확인하고, 새로운 Access Token만 발행해서 넘겨준다
  8. 로그인이 끊기지 않고 계속해서 사용

CS 지식

쿠키의 이해

  • 쿠키 옵션, Set-Cookie 시 다양한 옵션이 있음

    • 쿠키명 = 쿠키값 : 기본적인 쿠키의 값이다. mycookie=test 와 같이 pair로 설정한다

    • Expires = 날짜 : 만료 기한이다. 이 기한이 지나면 쿠키가 제거된다. 기본값은 클라이언트가 종료될 때까지이다.

    • Max-age = 초 : Expires와 비슷하지만 날짜 대신 초를 입력할 수 있다. 해당 초가 지나면 쿠키가 제거되며 Expires보다 우선시한다.

    • Domain = 도메인명 : 쿠키가 전송될 도메인을 특정할 수 있다. 기본값은 현재 도메인이다

    • Path = URL : 쿠키가 전송될 URL을 특정할 수 있다. 기본값은 '/' 이며 이 경우 모든 URL에서 쿠키를 전송할 수 있다

    • Secure : HTTPS일 경우에만 쿠키가 전송된다

    • HttpOnly : 설정시 자바스크립트에서 쿠키에 접근할 수 없다. 쿠키 조작을 방지하기 위해 설정하는 것이 좋다

세션의 이해

  • 쿠키의 정보는 노출되고 수정되는 위험이 있음

    • 중요한 정보는 서버에서 관리하고 클라이언트에는 세션 키만 제공
    • 서버에 세션 객체(session) 생성 후, uniqueInt(키)를 만들어 속성명으로 사용
    • 속성 값에 정보 저장하고 uniqueInt를 클라이언트에 보냄
  • 세션의 동작 방식

    • 클라이언트가 서버에 접속 시 세션 ID를 발급 받음
    • 클라이언트는 세션 ID에 대해 쿠키를 사용해서 저장하고 가지고 있음
    • 클라이언트는 서버에 요청할 때, 이 쿠키의 세션 ID를 같이 서버에 전달해서 요청
    • 서버는 세션 ID를 전달 받아서 별다른 작업없이 세션 ID로 세션에 있는 클라이언트 정보를 가져와서 사용
    • 클라이언트 정보를 가지고 서버 요청을 처리하여 클라이언트에게 응답

쿠키와 세션의 비교

  • 쿠키는 서버의 자원을 전혀 사용하지 않으며, 세션은 서버의 자원을 사용
  • 보안 면에서 세션이 더 우수
    • 쿠키도 만료시간이 있지만 파일로 저장되기 때문에 브라우저를 종료해도 게속해서 정보가 남아 있을 수 있지만, 세션은 브라우저가 종료되면 만료시간에 상관없이 삭제됨
    • 쿠키는 클라이언트 로컬에 저장되기 때문에 변질되거나 request에서 스니핑 당할 우려가 있어서 보안에 취약하지만 세션은 쿠키를 이용해서 sessionId만 저장하고 그것으로 구분해서 서버에서 처리
  • 요청 속도는 쿠키가 세션보다 더 빠름
    • 세션은 서버의 처리가 필요하기 때문에

암호화

  • 단방향 암호화

    • 암호화는 가능하지만 복호화는 불가능
      • 암호화 : 평문을 암호로 만듦
      • 복호화 : 암호를 평문으로 만듦
    • 복호화할 필요가 없는, 복호화해서 안되는 정보에 활용 : 사용자 비밀번호
    • 주로 Hash 기법을 활용
  • Hash 사용하기(sha512)

    • createHash(알고리즘) : 사용할 해시 알고리즘을 넣어준다
      • 현재는 sha512 정도로 충분하지만, 이후에 sha512도 취약해지면 더 강화된 알고리즘으로 변경
    • update(문자열) : 변환할 문자열을 넣어준다
    • digest(인코딩) : 인코딩할 알고리즘을 넣어준다
      • base64, hex, latin1 이 주로 사용되는데, 그 중 base64가 결과 문자열이 가장 짧아서 자주 활용된다
const crypto = require("crypto")

console.log(
	"bash64: ",
    crypto.createHash("sha512").update("password1234").digest("base64")

코드

  • cookie2.js
const http = require('http');
const fs = require('fs').promises;
const path = require('path');

const parseCookies = (cookie = '') =>
  cookie
    .split(';')
    .map(v => v.split('='))
    .reduce((acc, [k, v]) => {
      acc[k.trim()] = decodeURIComponent(v);
      return acc;
    }, {});

http.createServer(async (req, res) => {
  const cookies = parseCookies(req.headers.cookie); // { mycookie: 'test' }
  // 주소가 /login으로 시작하는 경우
  if (req.url.startsWith('/login')) {  // 로그인으로 들어왔다
    const url = new URL(req.url, 'http://localhost:8084');  // 8084 받아옴
    const name = url.searchParams.get('name');
    const expires = new Date();
    // 쿠키 유효 시간을 현재시간 + 5분으로 설정
    expires.setMinutes(expires.getMinutes() + 5);   // 해당 쿠키의 유효시간을 5분, expires에 가져왔다
    res.writeHead(302, {
      Location: '/',
      'Set-Cookie': `name=${encodeURIComponent(name)}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,   // Set-Cookie에 내가 넣고 싶은 정보를 모두 넣을 수 있다
    });
    res.end();
  // name이라는 쿠키가 있는 경우
  } else if (cookies.name) {
    res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end(`${cookies.name}님 안녕하세요`);
  } else {
    try {
      const data = await fs.readFile(path.join(__dirname, 'cookie2.html'));
      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
      res.end(data);
    } catch (err) {
      res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
      res.end(err.message);
    }
  }
})
  .listen(8084, () => {
    console.log('8084번 포트에서 서버 대기 중입니다!');
  });
  • session.js
const http = require('http');
const fs = require('fs').promises;
const path = require('path');

const parseCookies = (cookie = '') =>
  cookie
    .split(';')
    .map(v => v.split('='))
    .reduce((acc, [k, v]) => {
      acc[k.trim()] = decodeURIComponent(v);
      return acc;
    }, {});

const session = {};

http.createServer(async (req, res) => {
  const cookies = parseCookies(req.headers.cookie);
  if (req.url.startsWith('/login')) {
    const url = new URL(req.url, 'http://localhost:8085');
    const name = url.searchParams.get('name');
    const expires = new Date();
    expires.setMinutes(expires.getMinutes() + 5);
    const uniqueInt = Date.now();  // 현재 시간
    session[uniqueInt] = {  // 현재 시간을 key로 가지고 이 key 안에는 name과 expires가 있다
      name,
      expires,
    };
    res.writeHead(302, {
      Location: '/',
      'Set-Cookie': `session=${uniqueInt}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,   // key를 보내서 client가 cookie에 저장할 수 있게 해준다
    });
    res.end();
  // 세션쿠키가 존재하고, 만료 기간이 지나지 않았다면
  } else if (cookies.session && session[cookies.session].expires > new Date()) {
    res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end(`${session[cookies.session].name}님 안녕하세요`);
  } else {
    try {
      const data = await fs.readFile(path.join(__dirname, 'cookie2.html'));
      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
      res.end(data);
    } catch (err) {
      res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
      res.end(err.message);
    }
  }
})
  .listen(8085, () => {
    console.log('8085번 포트에서 서버 대기 중입니다!');
  });

JWT 설치

  • JWT관련 모듈 설치
npm install jsonwebtoken dotenv
  • 토큰 발급
    • .sign( {넣을 정보}, 비밀키, {만료시간} )
const jwt = require("jsonwebtoken");
const token = jwt.sign({id : "kdkd"}, "shhhh", {  // id : kdkd, 비밀키(탈취되면 안됨) : shhhh
    expiresIn : "1m"  // 1분만 유효
    issuer : "yubin",
});
console.log(token);
profile
대한민국의 미래를 묻는다면 고개를 들어 나를 쳐다보거라

0개의 댓글