Express Session

김동현·2023년 6월 2일
0

nodeJS

목록 보기
12/14

Session

사용자 정보와 같은 민감한 정보는 쿠키로 주고 받으면 중간에 정보가 탈취당했을 때 타격이 크다.

따라서 민감한 정보는 서버에서 사용자별로 저장하여 관리하고 쿠키로는 사용자를 식별할 수 있는 id만 주고받는다.

물론 id마저 탈취당해서 악의적인 공격자가 다른 사용자로 위장할 순 있지만, 민감한 정보자체를 탈취당한것이 아니기 때문에 세션을 이용하는 것이 더 낫다.

id탈취를 방지하는 여러 기술도 있다.
쿠키 자체의 수명관리를 설정할 수 있고(Max-Age), 자바스크립트에서 쿠키를 볼 수 없도록 할 수 있고(HttpOnly) 서버와 주고받는 데이터 자체를 암호화하는 프로토콜(HTTPS) 등 여러가지 보안기술들이 있다.

Session Flow

const app = http.createServer((request, response) => {
  // 쿠키 출력
  console.log(request.cookie);
  const { pathname, searchParams } = new url.URL(
    request.url,
    `http://localhost:${PORT}`
  );
  if (pathname === "/") {
    // 메인 페이지
    const htmlFile = fs.readFileSync(path.join(__dirname, "index.html"), {
      encoding: "utf8",
    });
    return response.end(htmlFile);
  } else if (pathname === "/login") {
    // 로그인 페이지
    const htmlFile = fs.readFileSync(path.join(__dirname, "login.html"), {
      encoding: "utf8",
    });
    return response.end(htmlFile);
  } else if (pathname === "/login_process") {
   	// 로그인 실패시 리다이렉트 : /
    // 로그인 성공시 리다이렉트 : /user?id=유저아이디
  } else if (pathname === "/user") {
    return response.end(`<h1>Hello</h1>`);
  } else {
    return response.end("Hello");
  }
});

기본적인 골격은 위와 같다.
로그인 페이지에서 폼 제출을 하면 /login_process 로 action 한다.
사용자 정보는 보통 db에 저장되어있지만 여기서는 단순 Object로 구성했다.

module.exports = [
  {
    username: "username1",
    password: "password1",
  },
  {
    username: "username2",
    password: "password2",
  },
  {
    username: "username3",
    password: "password3",
  },
  {
    username: "username4",
    password: "password4",
  },
  {
    username: "username5",
    password: "password5",
  },
];

/login_process 로 action시 진행되는 로직을 짜보자.
먼저 form 으로 제출한 데이터를 받아와야 한다.

else if (pathname === "/login_process") {
  let body = "";
  request.on("data", (chunk) => {
    body += chunk;
  });
  request.on("end", () => {
    const { username, password } = qs.parse(body);
    // 로그인 로직
  });
}

이제 usernamepassword 변수에는 form에서 제출한 데이터가 담겨 있다.
위에서 사용자 정보 모듈을 불러와서 이 데이터들을 토대로 사용자를 검색해야한다.

const userdata = require("./userdata.js");
// find user
const user = userdata.find((user) => {
  return user.username === username;
});
if (user === undefined) {
  // invalid username
  console.error("invalid username");
  response.writeHead(301, { Location: "/" });
  return response.end();
} else if (user.password !== password) {
  // invalid password
  console.error("invalid password");
  response.writeHead(301, { Location: "/" });
  return response.end();
} else {
  // logged in
}

로그인 실패시 전부 / url로 리다이렉트 한다.
로그인 성공시 로직을 보자.

const { randomUUID } = require("node:crypto");
const data = {
  cookie: { sessionId: randomUUID() },
};
fs.writeFile(
  path.join(__dirname, `/usersession/${data.cookie.sessionId}.json`),
  JSON.stringify(data),
  (err) => {
    if (err) return response.end(err);
    response.writeHead(301, {
      "Set-Cookie": `sessionId=${JSON.stringify(
        data.cookie.sessionId
      )}`,
      Location: `/user?id=${username}`,
    });
    return response.end();
  }
);

randomUUID 는 랜덤한 문자열을 생성해주는 crypto모듈의 함수이다.
data 라는 객체를 만들고 cookie 라는 객체를 포함시킨다.
cookie 객체는 쿠키값을 갖도록 설정하는데, 지금은 세션을 다루고 있기 때문에 세션과 관련된 sessionId 키만 임의로 만들었다.
그리고 sessionId 와 이름이 같은 세션 json파일을 만들었다.

현재 로그인이 성공한 상태이기 때문에 /user?id=${username} 로 리다이렉트 해야한다.
하지만 그냥 리다이렉트만 하면 재요청시 로그인이 된 상태인지 모른다.
로그인이 된 상태라는 의미를 쿠키로 보내야 한다.
따라서 브라우저에게 쿠키값으로 sessionId 를 보낸다.

이제 브라우저는 다음 요청시 sessionId 가 담긴 쿠키를 서버에게 보내고 서버는 sessionId 쿠키를 기반으로 세션 파일을 검색한다.
이를 통해 로그인이 된 상태인지 아닌지 판단할 수 있다.


이렇게 세션 Id를 브라우저와 서버가 주고받는 쿠키를 세션 쿠키라고 한다.

express-session

직접 세션을 구현해보는 것도 좋지만 보안공부를 한 평생 할 것이 아니라면 남이 만든 라이브러리를 사용하고 꾸준히 업데이트하는게 효율적이다.

node.js에서 세션을 다루는 대표적인 라이브러리는 express-session이다.
express팀에서도 이 라이브러리를 사용하라고 공식 홈페이지에서 추천하고 있다.

express-session 공식 깃헙↗
기본적인 사용방법은 다음과 같다.

app.use(
  session({
    secret: "mySecret",
    resave: false,
    saveUninitialized: true,
  })
);
app.get("/", (req, res, next) => {
  res.send(`${JSON.stringify(req.session)}`);
});

app.use() 로 express-session 미들웨어를 호출했다.
request 객체에 session 키가 생겼다.
만약 express-session 미들웨어를 호출하지 않은 상태로 req.session 하면 undefined가 출력될 것이다.
req.session 의 출력결과는 다음과 같다.

{
  "cookie": {
    "originalMaxAge": null,
    "expires": null,
    "httpOnly": true,
    "path": "/"
  }
}

위의 cookie객체의 속성들은 HTTP 쿠키 속성들과 매칭된다.

express-session의 동작을 알아보자.
1. 브라우저가 최초의 요청을 서버에게 보낸다.
2. express-session 옵션에 따른 쿠키를 생성해서 브라우저에게 전달한다.

HTTP 프로토콜을 보면 request때는 쿠키가 없고 response때는 쿠키를 받아온것을 확인할 수 있다.


response에서의 쿠키를 보면 connect.sid 가 보인다.
사용자를 식별하는 id라고 보면 된다.

위에서 express-session 미들웨어를 정의하는데 사용한 옵션에 대해 알아보자.

secret

필수 옵션이다.
세션 ID 쿠키에 서명하는 데 사용되는 비밀코드이다.
하나의 문자열일 수도 있고, 여러 비밀코드들의 배열일 수도 있다.
배열일 경우 첫 번째 요소만 비밀코드로 사용되며 나머지 코드들은 고려만 된다.
고려만 된다는것이 무슨뜻인지는 뒤에 나온다.

비밀코드는 당연히 위의 코드처럼 하드코딩으로 사용해선 안된다.
보통은 환경 변수를 사용하여 로컬 환경에 숨겨 사용한다.

주기적으로 변경하는 것이 좋다.
하지만, 비밀코드가 변경되면 이전의 모든 세션이 무효처리가 된다.
어떻게 해야할까?
위에서 비밀코드들의 배열에서 나머지 코드들은 고려만 된다고 했다.
즉, 첫 번째 요소에 새로운 비밀코드를 삽입하고 기존의 비밀코드는 나머지 요소들 중 하나로 세팅하면 자동으로 비밀코드가 업데이트 되며 이전의 세션들도 무효처리가 되지 않는다.

saveUninitialized

saveUninitialized 가 무엇인지 알기위해선 세션 초기화가 무엇인지 간략하게 알고있어야 한다.
세션 초기화는 세션 식별자를 생성하고, 서버 측에서 세션 데이터를 저장하기 위한 저장소를 설정하는 등의 작업을 포함하는 작업이다.
즉, 상대적으로 오래걸리고 가볍지만은 않은 작업이다.

saveUninitialized 가 true일때의 동작을 보자.
1. 클라이언트가 서버에 최초의 요청을 보낸다.
2. 서버는 세션 식별자를 생성하여 클라이언트에게 세션 쿠키를 전송한다.
3. 클라이언트는 세션 쿠키를 저장하고, 이후의 요청에서 세션 식별자를 함께 전송한다.
4. 서버는 세션 식별자를 기반으로 세션 데이터를 초기화하고 저장소에 저장한다.

saveUninitialized 가 false일때의 동작을 보자.
1. 클라이언트가 서버에 최초의 요청을 보낸다.
2. 서버는 세션을 초기화한다.
세션을 초기화하기 전에는 세션쿠키를 포함하지 않은채 클라이언트에게 응답을 보낸다.
3. 초기화 작업이 끝난 후 요청이 들어오면 세션 쿠키(세션 식별자를 포함한)를 클라이언트에게 전송된다.
4. 클라이언트는 세션 쿠키를 저장하고, 이후의 요청에서 세션 식별자를 함께 전송한다.
5. 서버는 세션 식별자를 기반으로 클라이언트의 세션을 식별하고 세션 데이터를 저장소에서 가져온다.
6. 요청과 응답 사이에서 세션 데이터를 사용하여 작업을 수행하거나 상태를 유지한다.

resave

세션정보가 변경될 수도 있다.
resave 옵션이 true라면 세션정보가 변경되든 말든 무조건 세션정보를 다시 저장한다는 의미이다.
resave 옵션이 false라면 세션정보가 변경되었을 경우만 저장한다.

session-file-store

express-session 공식 홈페이지에 보면 여러 db들을 사용할 수 있는 방법들이 나열되어 있는데 여기선 파일로 저장하는 방법을 보자.
session-file-store 를 설치하자.
기본 사용법은 아래와 같다.

var FileStore = require('session-file-store')(session);
var fileStoreOptions = {};
 
app.use(session({
    store: new FileStore(fileStoreOptions),
    secret: 'keyboard cat'
}));

브라우저에 저장된 쿠키를 보자.

서버에 저장된 쿠키를 보자.

브라우저의 세션 쿠키에 s%3A 라는 prefix를 제외하면 서버에 저장된 세션 파일의 이름과 같다.
이런식으로 세션 ID를 이용해서 사용자별로 데이터를 저장한다.

해당 세션 파일을 열어보면 사용자별 세션정보를 볼 수 있다.

profile
프론트에_가까운_풀스택_개발자

0개의 댓글