60일차 [인증/보안] Cookie/Session

2021. 10. 24. 13:07
반응형

2021. 10. 20 수요일

1. Today's Key Points!🔑

  • HTTP, HTTPS
  • Hashing, Salt
  • Authorization, Authentication
  • Cookie
  • Session

2. 정리해보자!🧹

HTTPS

https는 http 요청을 SSL 혹은 TLS라는 알고리즘을 이용해, HTTP 통신을 하는 과정에서 내용을 암호화하여 데이터를 전송하는 방법이다.

https프로토콜을 사용해야만 하는 이유? 데이터 제공자의 신원을 보장받을 수 있기 때문이다. HTTPS 프로토콜의 특징 중 하나는 암호화된 데이터를 주고받기 때문에, 중간에 인터넷 요청이 탈취되더라도 그 내용을 알아볼 수 없다. 또 다른 특징 중 하나는 브라우저가 응답과 함께 전달된 인증서 정보를 확인할 수 있다는 점이다. 브라우저는 인증서의 도메인과 데이터를 제공한 제공자의 도메인을 비교할 수 있기 때문에 인증서의 도메인 정보와 데이터 제공자의 도메인 정보가 다른 '중간자 공격'을 감지해서 보안 위협으로부터 사용자 및 사용자의 데이터를 보호할 수 있다.

기밀성 (privacy) 

  • 메시지를 가로챌 수 없음.
  • 메시지를 읽을 수 없음.
  • 메시지는 암호화 되어 있다.

무결성 (integrity)

  • 메시지가 조작되지 않음.
  • 메시지가 목적지로 가는 도중에 조작되지 않음.
  • 원본 그대로 잘 도착한다.

인증서는 무엇을 보장할까?

  • 브라우저에서 접속한 서버가 "의도한" 서버임을 보장
  • 브라우저와 서버가 통신할 때 암호화할 수 있도록 서버의 공개키 제공

비대칭 키 암호화

  • 공개 키 (열쇠구멍) : 메시지를 암호화하는데 사용
  • 비공개 키 (개인키 : 열쇠) : 암호화된 메시지를 해독하는데 사용

SSL / TLS

  • 웹 서버와 사용자의 웹 브라우저간 통신을 암호화하는데 쓰는 프로토콜
  • 공개 키와 개인키를 교환해서 보안 세션을 생성 -> 통신을 암호화하는 방식
  • TLS는 SSL 보다 조금 더 많은 암호화를 하기 때문에 안전하다.

Hashing

클라이언트가 서버로부터 인증절차를 받으려면 로그인을 통해서 권한을 부여받는 절차가 필요할 것이다. 이때 아이디와 패스워드를 서버에 보내면 서버는 데이터베이스에서 사용자의 아이디와 패스워드 정보를 비교해보고 맞다고 하면 인증을 내어준다. 이때 아이디는 공개가 되도 되지만, 패스워드는 공개되면 안될 것이다. 

그렇기 때문에 패스워드를 암호화해서 데이터베이스에 저장할 수 있도록 암호하를 하는 작업이 필요하다. 이것을 Hashing이라고 한다. 어떠한 문자열에 '임의의 연산'을 적용하여 다른 문자열로 변환하는 것이다. 이때 실질적으로 서비스에 적용될 때는 3가지의 철칙이 있다.

  1. 모든 값에 대해 해시 값을 계산하는데 오래걸리지 않아야 한다.
  2. 최대한 동일한 해시 값을 피해야 하며, 모든 값은 고유한 해시 값을 가져야 한다.
  3. 아주 작은 단위의 변경이라도 완전히 다른 해시 값을 가져야 한다.

이렇게 해싱한 패스워드를 데이터 베이스에 저장을 하고 만약 패스워드를 탈취당한다고 해도 다른 곳에서는 사용을 할 수 없을 것이기에 악용될 걱정을 덜 수 있게된다.

salt란 암호화해야 하는 값에 어떤 '별도의 값'을 추가하여 결과를 변형하는 것. 이렇게 별도의 값을 추가하는 이유

  1. 암호화만 해놓는 다면 해시된 결과는 늘 동일하다. 그래서 해시된 값과 원래 값을 테이블로 만들어서 decoding 해버리는 경우도 있다.
  2. 원본값에 임의로 약속된 '별도의 문자열'을 추가해서 해시를 진행한다면 기존 해시값과 전혀 다른 해시값이 반환되어 알고리즘이 노출되더라도 원본값을 보호할 수 있다.
  3. (암호화 하려는 값) + (Salt 용 값) => (hash 값)

salt사용 시 주의점

  1. Salt는 유저와 패스워드 별로 유일한 값을 가져야 한다.
  2. 사용자 계정을 생성할 때와 비밀번호를 변경할 때 마다 새로운 읨의의 Salt를 사용해서 해싱해야 한다.
  3. Salt는 절대 재사용하지 말아야 한다.
  4. Salt는 DB의 유저 테이블에 같이 저장되어야 한다.

Authorization vs Authentication

Authentication(인증) : 로그인이라고 생각하면 된다. 내가 이 사이트에 가입된 회원임을, 즉 특정 서비스에 일정 권한이 주어진 사용자임을 아이디랑 패스워드 등을 통해서 말 그대로 인증을 받는 것이다.

Authorization(인가) : 인증을 받은 사용자가 이후 여러 서비스의 기능들을 사용할 때 즉, 페이스북에 로그인으로 인증을 하고 나서 내 친구들의 목록을 보거나 내 담벼락에 글을 작성하거나 친구의 게시물의 내 명의로 좋아요나 댓글을 다는 등 내 계정으로만 할 수 있는 활동을 시도할 때 페이스북이 내가 로그인 되어있음을 알아보고 허가를 해주는 것이다. 로그인이 유지되는 상태에서 일어나느 일이라고 보면 된다.

Cookie

HTTP의 stateless(무상태성)을 보완해주는 것. 서버에서 클라이언트에 데이터를 저장하는 방법의 하나이다. 단순히 서버에서 클라이언트에 쿠키를 전송하는 것만 의미하지 않고 클라이언트에서 서버로 쿠키를 전송하는 것도 포함된다. 이것을 이용하면 사용자의 로그인 상태를 유지할 수 있다.

하지만 데이터를 저장한 이후 아무 때나 데이터를 가져올 수 없다. 데이터를 저장한 이후 특정 조건들이 만족하는 경우에만 다시 가져올 수 있다. 이런 조건들을 쿠키 옵션으로 표현할 수 있다.

  1. Domain : 서버에 접속할 수 있는 이름을 뜻하는데, 쿠키 옵션에서 도메인은 포트 및 서브 도메인 정보, 세부 경로를 포함하지 않는다. 예를 들어 요청해야할 URL이 http://www.localhost.com:3000/users/login 이라 하면, 여기서 Domain은 localhost.com이 된다. 만약 쿠키 옵션에서 도메인 정보가 존재한다면 클라이언트에서는 쿠키의 도메인 옵션과 서버의 도메인이 일치해야만 쿠키를 전송할 수 있다. 쿠키가 적용되어야 하는 호스트 주소를 나타낸다.
  2. Path : 세부경로는 서버가 라우팅할 때 사용하는 경로이다. 예를 들어 요청해야할 URL이http://www.localhost.com:3000/users/login 이라 하면 Path는 /users/login이 된다. 명시하지 않으면 기본으로 / 으로 설정되어 있다. Path 옵션의 특징은 설정된 path를 전부 만족하는 경우 요청하는 path가 추가로 더 존재하더라도 쿠키를 서버에 전송할 수 있다. 즉 path가 /users로 설정되어 있고, 요청하는 세부 경로가 /users/login인 경우라면 쿠키 전송이 가능하다. 하위 경로를 모두 포함하여 설정이 적용된다.
  3. MaxAge or Expires : 쿠키가 유효한 기간을 정하는 옵션이다. MaxAge는 앞으로 몇 초 동안 쿠키가 유효한지 설정하는 옵션이다. Expires는 MaxAge와 비슷한데, 언제까지 유효한지 Date를 지정한다. 이후 지정된 시간, 날짜를 초과하게 되면 쿠키는 자동으로 파괴된다. 하지만 두 옵션이 모두 지정되지 않는 경우에는 브라우저의 탭을 닫아야만 쿠키가 제거될 수 있다.
  4. Secure : 쿠키를 전송해야할 때 사용하는 프로토콜에 따른 쿠키 전송 여부를 결정한다. 만약 옵션이 true일 경우 HTTPS 프로토콜을 이용해서 통신하는 경우에만 쿠키를 전송할 수 있다.
  5. HttpOnly : 자바스크립트에서 브라우저의 쿠키에 접근 여부를 경정한다. 만약 해당 옵션이 true로 설정된 경우, 자바스크립트에서는 쿠키에 접근이 불가능하다. 기본값은 false이다. 옵션이 false인 경우 자바스크립트에서 쿠키에 접근이 가능해지므로 'XSS' 공격에 취약하다.
  6. SameSite : Cross-Origin 요청을 받은 경우 요청에서 사용한 메소드와 해당 옵션의 조합으로 서버의 쿠키 전송 여부를 결정하게 된다. 아래는 SameSite의 옵션.
    • Lax : Cross-Origin 요청이면(사이트가 서로 다르면) 'GET' 메소드에 대해서만 쿠키를 전송할 수 있다. SameSite 옵션의 기본값이다.
    • Strict : Cross-Origin이 아닌 same-site인 경우에만(사이트가 같을 경우에만) 쿠키를 전송할 수 있다.
    • None : (사이트가 달라도)항상 쿠키를 보내줄 수 있다. 하지만 여기서 주의해주어야할 점은 쿠키 옵션 중에 Secure 옵션이 필요하다.

Cookie를 이용한 상태 유지

이러한 쿠키의 특성을 이용해서 서버는 클라이언트에 인증정보를 담은 쿠키를 전송하고, 클라이언트는 전달받은 쿠키를 요청과 같이 전송해서 Stateless 한 인터넷 연결을 Stateful하게 유지할 수 있다.

하지만 기본적으로는 쿠키는 오랜 시간 동안 유지될 수 있고, 자바스크립트를 이용해서 쿠키에 접근할 수 있기 때문에 쿠키에 민감한 정보를 담는 것은 위험하다.

이런 인증정보를 탈취해서 서버에 요청을 보내면 서버는 누가 요청을 보낸 건지 상관하지 않고 인증된 유저의 요청으로 취급하기 때문에, 개인 유저정보 같은 민감한 정보에 접근이 가능하다.

쓰임새

  • 인증 상태 저장
  • 장바구니
  • 팝업 7일간 보지않기
  • 맞춤 광고

Session

서버와 클라이언트간의 연결이 활성화된 상태를 말한다. 즉, 사용자가 인증에 성공한 상태를 말한다. 쿠키는 클라이언트에 데이터를 저장하는 방식이었다면, 세션은 데이터를 서버에 저장한다. 쿠키에는 그 데이터에 대한 아이디만 암호화된 상태로 부여를 한다.

Session 전달 방법

로그인을 한 뒤 장바구니에 담는 요청을 보내면, DB에 정보를 저장하고, 신분증 역할을 하는 session_id를 쿠키에 담아서 보내준다. 그 다음 또 다른 요청을 보내면 다시 로그인을 할 필요없이 쿠키에 담겨있는 session_id를 확인해서 적절한 응답을 보내준다.

Cookie & Sesson 설명 접속 상태 저장 결로 장점 단점
Cookie 쿠키는 그저 http의 stateless한 것을 보완해주는 도구이다. 클라이언트 서버에 부담을 덜어준다. 쿠키 그 자체는 인증이 아니다.
Session 접속 상태를 서버가 가진다. 접속 상태와 권한 부여를 위해 세션아이디를 쿠키로 전송한다. 서버 신뢰할 수 있는 유저인지 서버에서 추가로 확인이 가능하다. 하나의 서버에서만 접속 상태를 가지므로 분산에 불리하다.

Session 단점

기본적으로 서버의 메모리에 세션정보를 저장한다. 만약 서버의 이용자가 매우 많은 경우라면 메모리 저장공간의 일정부분을 항상 차지를 하게있게 되므로 가용메모리의 양이 줄어들어서 서버의 성능이 안좋아지는 상황이 발생한다.

세션은 기존의 쿠키를 완전하게 대체한것이 아니기 때문에 여전히 쿠키를 사용하고 있다. httpOnly 설정이 없는 경우에 쿠키는 악의적인 스크립트 공격 즉, XSS공격에 취약하다. 세션도 쿠키를 이용하는 것이기 때문에 쿠키의 한계를 그대로 가지고 있다. 만약 세션아이디 정보가 담긴 정보가 탈취된다면 여전히 개인정보가 유출될 위험이 존재한다. 공공장소에서 로그인을 하고 사용이 끝난뒤 로그아웃을 해주어야 하는 이유가 바로 이런 부분 때문이다.

3. Sprint 복기!🧐

환경변수를 설정해주고, 마이그레이션을 해주면 연결해놓은 데이터베이스에 모델로 만들어준 부분이 테이블로 저장이 될 것이다. 

마이그레이션 해주기 전 상태

npx sequelize-cli db:migrate

마이그레이션 해준 결과

테이블은 생성이 됐고, 근데 테이블안에 내용을 보면 아무것도 없는 것을 볼 수 있다. 그래서 시드 파일을 하나 생성해서 시드파일을 넣어보자. 더미 데이터를 하나 넣어놓고 테스트할 때 사용할 데이터를 넣어준다고 생각하면 될 것이다.

npx sequelize-cli db:seed:all

시드파일 넣어준 결과

Server 구현

그럼이제 서버를 구현해보자. 우선 쿠키설정, CORS 및 세션 설정을 해주자.

// 세션 및 쿠키 설정
const FileStore = require("session-file-store")(session) //'npm install session-file-store'로 모듈을 설치해주어야 한다.

app.use(
  session({
    secret: '@codestates',
    resave: false,
    saveUninitialized: false, // 이 부분을 true로 설정하면 login이 안되도 세션이 생기게된다.
    store: new FileStore() // 옵션으로 이렇게 부여하고 저장하면 session 폴더가 생기게 된다. 그리고 로그인을 하면 세션 객체가 파일안에 생성이 된다.
    cookie: {
      domain: 'localhost', // 클라이언트 url이 https://localhost:3000 이기 때문에 포트 및 서브 도메인 정보, 세부 경로를 포함하지 않은 도메인만 작성해준다.
      path: '/', // path에 해당하고 뒤에 붙는 세부경로에 대해서 쿠키를 전송해줄 수 있다.
      maxAge: 24 * 6 * 60 * 10000,
      sameSite: 'none', // none으로 설정해주면 모든 상황에 쿠키를 줄 수 있는데, secure 설정이 있기 때문에 none으로 해줄 수 있다.
      httpOnly: true, // true로 설정해서 자바스크립트로 쿠키에 접근해서 수정을 못하도록 해준다.
      secure: true, // https 프로토콜을 사용하기 때문에 true로 설정해준다.
    },
  })
);

세션이 생성이 되면 쿠키에 랜덤으로 세션아이디가 만들어지고, 서버에는 세션객체가 저장되어서 나중에 클라이언트에서 요청이 오면 쿠키에있는 세션아이디(식별자)를 바탕으로 유저를 식별한다. 클라이언트를 구현하고 나서 어떤식으로 흘러가는지 좀 더 확인해보자.

// CORS 설정

app.use(cors({
  origin : 'https://localhost:3000', //해당 도메인의 요청만 받는다.
  method : ["GET", "POST", "OPTION"], //요청 가능한 메소드
  credentials : true // 인증 정보에 대한 요청이 있는데 credentials가 false이거나 설정을 해주지 않으면 응답을 무시하고 웹 콘텐츠로 반환하지 않는다.
}));

credentials 를 왜 true로 설정해주어야 하는가? 인증정보는 크게 3가지가 있다. cookie, authorization headers, TLS client certificates(인증서) 이러한 인증 정보를 가진 요청이 왔을 때, credentials가 true로 설정이 되어있지 않다면 응답을 무시하고 웹 콘텐츠로 반환을 하지 않기 때문에 true로 설정을 해주어야 한다.

login

// login
const { Users } = require("../../models");

module.exports = {
  post: async (req, res) => {
    // userInfo는 유저정보가 데이터베이스에 존재하고, 완벽히 일치하는 경우에만 데이터가 존재한다.
    const userInfo = await Users.findOne({
      where: { userId: req.body.userId, password: req.body.password },
    });

    // userInfo 결과 존재 여부에 따라 응답을 구현해야 한다.
    // 결과가 존재하는 경우 세션 객체에 userId가 저장되어야 한다.
    if (!userInfo) {
      return res.json({ message: "not authorized" });
    } else {
      req.session.save(() => { // 세션 객체에 저장할 수 있는 express-session 메소드. 
        req.session.userId = userInfo.userId; // 실제로는 암호화된 아이디가 들어가야 할 것이다.
        res.json({ message: "ok" });
      });
    }
  },
};

req.session.save()를 사용하면 세션객체에 값을 저장할 수 있다. 그래서 userId를 저장해서 추후에 그 사용자에 대한 정보를 응답해주거나 할 수 있게 된다. 그리고 sessionID가 랜덤하게 생성되서 쿠키에 담아주고 그것이 식별자 역할을 하게된다.

logout

// logout
module.exports = {
  post: (req, res) => {
    // 세션 아이디를 통해 고유한 세션 객체에 접근할 수 있다.
    // 앞서 로그인시 세션 객체에 저장했던 값이 존재할 경우, 이미 로그인한 상태로 판단할 수 있다.
    // 세션 객체에 담긴 값의 존재 여부에 따라 응답을 구현해야한다.

    if (!req.session.userId) {
      // 로그인이 되어있지 않은 상태에서 로그아웃 요청을 하는 경우
      return res.status(400).json({ message: "you need login" });
    } else {
      req.session.destroy(() => { // 세션을 파괴하는 express-session 메소드
        res.status(200).json({ message: "see you again" });
      });
    }
  },
};

 

로그인이 되어있는 상태라면 세션객체에 userId값이 존재할 것이고, 존재하는 상태에서 로그아웃 요청을 하면 잘못된 요청이라는 결과를 보여주어야 할 것이다. 기본적으로 서버에서는 이렇게 구축을 해놓을 것이지만 프론트엔드 영역에서 이러한 요청이 애초에 오지않게 할 수 도 있을 것이다.

로그인이 되어있지 않는 상태라면, 로그아웃이라는 기능을 사용할 수 없게 해줘서 서버에서 처리를 하지 않도록 해주는 것이다. 즉, 프론트엔드 영역에서 로그인이 되어있지 않다면 로그아웃 버튼을 볼 수 없게 만들어 놓고 이런 요청이 애초에 오지 않게 해주는 방법도 있지않을까 한다.

userInfo

// userInfo
const { Users } = require("../../models");

module.exports = {
  get: async (req, res) => {
    if (!req.session.userId) {
      return res.status(400).json({ message: "not authorized" });
    } else {
      // 데이터베이스에서 로그인한 사용자의 정보를 조회한 후 응답한다.
      const userInfo = await Users.findOne({
        where: { userId: req.session.userId },
      });
      if (!userInfo) {
        return res.status(400).json({ message: "not found your information" });
      }
      delete userInfo.password;
      return res.status(200).json({ data: userInfo, message: "ok" });
    }
  },
};

로그인이 되었다면 세션이 만들어지고, 세션 객체에 userId를 만들어 주기때문에 userId가 존재하면 로그인이 된 상태이고, 존재하지 않으면 로그인이 되지 않은 상태라는 것을 알 수 있다.

Client 구현

Mypage Component

// Mypage
//생략
const handleLogout = () => {
    // TODO: 서버에 로그아웃 요청을 보낸다음 요청이 성공하면 props.logoutHandler를 호출하여 로그인 상태를 업데이트 해야 합니다.
    axios
      .post("https://localhost:4000/users/logout", null, {
        withCredentials: true,
      })
      .then(() => {
        props.logoutHandler();
      });
  };
//생략

 

axios 첫번째 인자는 요청할 url, 두번째 인자는 payload, 세번째 인자는 헤더 옵션을 설정해줄 수 있다. 그런데 3번째 인자에 헤더 옵션을 넣어준다는 것은 알겠는데, withCredentials: true로 해준 이유는 무엇인가?

먼저 CORS는 다른 origin에 대한 요청을 허용하는 정책이다. 같은 origin에서 http 통신을 하는 경우 알아서 cookie가 request header에 들어가게 된다. 하지만 origin이 다른 http 통신에서는 request header에 쿠키가 자동으로 들어가지 않는다. 그래서 우리가 설정을 해주어야 한다. 쿠키를 요청에 포함하고 싶으면 withCredentials: true로 옵션을 설정하고 요청을 보내면 된다. 인증정보가 같이보내지니까 응답해달라고 요청을 보낸다고 생각하면 될 것 같다.

Login Component

// Login
// 생략
class Login extends Component {
  constructor(props) {
    super(props);
    this.state = {
      username: "",
      password: "",
    };
    this.inputHandler = this.inputHandler.bind(this);
    this.loginRequestHandler = this.loginRequestHandler.bind(this);
  }

  inputHandler(e) {
    this.setState({ [e.target.name]: e.target.value });
  }

  loginRequestHandler() {
    // 로그인에 성공하면
    // - props로 전달받은 함수를 호출해, 로그인 상태를 변경
    // - GET /users/userinfo 를 통해 사용자 정보를 요청
    // 사용자 정보를 받아온 후
    // - props로 전달받은 함수를 호출해, 사용자 정보를 변경.
    axios
      .post(
        "https://localhost:4000/users/login",
        {
          userId: this.state.username,
          password: this.state.password,
        },
        { withCredentials: true }
      )
      .then(() => {
        this.props.loginHandler();
        axios
          .get("https://localhost:4000/users/userinfo", {
            withCredentials: true,
          })
          .then((res) => {
            this.props.setUserInfo(res.data.data);
          });
      });
  }
  // 생략

구현 결과

로그인 됐을 때 세션파일에 보면 객체파일이 생성되는 것을 볼 수 있을 것이다. 객체 파일 안을 보면

{
  "cookie": {
    "originalMaxAge": 86400000,
    "expires": "2021-10-25T14:46:38.421Z",
    "secure": true,
    "httpOnly": true,
    "domain": "localhost",
    "path": "/",
    "sameSite": "none"
  },
  "__lastAccess": 1635086798422,
  "userId": "kimcoding"
}

위 처럼 생성이 되어있고, 파일이름이 세션ID가 되는데, 로그인을 하면 세션이 생성되면서 세션ID가 쿠키에 담기게 되고, 쿠키에 세션ID가 담긴채로 요청을 보내면 서버에 있는 세션파일을 알아서 식별해서 세션 객체 안에있는 userID를 가지고 유저정보를 보내주거나 다른 응답들을 할 수 있게 되는 것이다. 그리고 로그아웃을 하면 세션이 사라지는데, 그 부분은 logout 컨트롤러에서 destory를 이용해서 세션을 파괴헀기 때문에 사라지게 되는 것이다. 그래서 공공장소에서 로그인을 하면 반드시 로그아웃을 하라는 이유가 이것 때문이다. 로그아웃을 하지않으면 세션은 서버에 그대로 남아있기 때문에 식별정보만 탈취해서 개인정보를 요청하는 등 악의적으로 이용될 수도 있다. 

반응형
LIST

BELATED ARTICLES

more