[TS express] 세션 인증

🪐 C:on·2022년 1월 6일
0

TS-Express

목록 보기
4/4

JWT의 장단점

암호화방식이나 타입과 함께 유저아이디, 유효기간 등을 jwt서버로 전달하면 그 정보들을 jwt가 해독할 수 있는 토큰으로 발행받는다. 따라서 토큰에는 모든 정보들이 jwt만 읽을 수 있는 형태로 저장되어 있다.

  • 서버에 데이터를 저장하지 않아(stateless)서 스케일링, 유지보수에 유연하다.
    • 근데 페이스북 정도가 아니면 스케일에 대응하기 어려운 것은 아닌것 같다.
  • jwt정보를 다른 사람이 훔치게 되면 훔친 정보로 로그인이 가능해진다.
    • 토큰 유효기간을 짧게 하고 대신 refresh토큰을 사용하면 된다. ⇒ 어디차박에 적용해보자!
  • 전달된 토큰은 돌이킬 수 없다.
    • 악의적인 사용자가 유효기간전 정보 탈취가 가능해질 수 있다.
    • 물론 서버만 알고있는 시크릿키가 있어야 토큰을 디코딩할 수 있지만 토큰자체를 해석하는 방법을 찾는다면 시크릿키 없이도 디코딩이 될 가능성은 분명히 있다.

세션쿠키의 장단점

요청받은 로그인 데이터가 데이터베이스의 데이터와 일치하면 세션을 세션 저장소에 저장한 뒤, 다시 이와 연결되는 세션 ID를 발행해서 쿠키에 실어 보낸다.

  • 쿠키 자체에는 계정정보가 들어있지 않기 때문에 http요청 중 노출되더라도 중요한 정보가 노출될 위험 부담이 적어진다.
  • 세션 하이재킹 공격(http요청을 가로채 쿠키를 탈취)시에 그 쿠키를 이용해서 요청을 보낼 수 있다.
    • ssl을 적용하고 세션에 유효기간을 설정한다.
  • 추가 저장공간이 필요하게 되고 확장, 유지보수에 추가 작업이 필요해진다.

세션 방식을 선택한 이유

아무래도 토큰에 정보들이 들어가있다는 것이 부정적으로 다가온다. 또, 부득이 한 경우에 서버에서 사용자의 로그인 상태를 관리해야 하거나 모든 사용자를 로그아웃시켜야만 하는 경우가 있을 때 jwt인증방식은 할 수 있는 것이 없다. 이러면에서 장점과 단점을 비교했을 때 jwt만 가지는 장점보다 세션방식만 가지는 장점이 더 많아서 세션을 사용한 인증을 구현하기로 했다.

🔨 사용하기

express-session flow

  • 요청이 들어오면 세션 미들웨어를 통과하여 req에 session이라는 속성을 생성해서 미들웨어에서 설정한 옵션들을 쿠키 데이터로 할당한다.
    [1] Session {
    [1]   cookie: {
    [1]     path: '/',
    [1]     _expires: 2022-01-08T15:27:54.237Z,
    [1]     originalMaxAge: 10000,
    [1]     httpOnly: true,
    [1]     secure: false
    [1]   }
    [1] }
  • 속성만 추가되었을 뿐, 세션 저장소에는 이 정보들을 저장하지 않는다.
  • req.session에 새로운 속성을 추가해주면 그때 세션이 선언되었다고 간주하여 세션저장소에 저장을 진행한다.
  • connect.id를 키로 가지며 선언한 것들을 value로 가지는 쿠키를 응답에 동봉하여 보낸다.
    //세션저장소 data필드에 저장되는 것
    {"cookie":{"originalMaxAge":10000,"expires":"2022-01-08T15:53:35.293Z","secure":false,"httpOnly":true,"path":"/"},"is_logined":true,"userId":1,"dispayName":"james"}
  • 클라이언트가 받은 세션id역시 같은 시간의 expire옵션이 설정되어있다. 브라우저에서 expire 값을 변경하여 브라우저에 계속보관하더라도 서버에서는 세션id와 매핑되는 세션저장소의 데이터가 data필드의 expires가 만료된 상태면 사용하지 않는다.
    • 따라서 session옵션을 설정할 때 resave를 false로 둬서 처음 저장되었을 때의 expires를 유지해줘야 한다.
  • 로그아웃을 할 경우 destroy를 사용하면 세션저장소의 세션데이터를 삭제하고 req.session도 모두 삭제한다. 브라우저에 저장되어 있는 쿠키는 따로 삭제해주지 않아도 활용가치가 없는 id가 된다

MySQL에 세션데이터 저장

express-session만 사용하게 되면 서버의 메모리에 데이터를 저장하므로 서버가 재구동될 때 데이터가 모두 휘발된다. 따라서 다른 저장소에 데이터를 저장해줘야 하는데 mysql과 함께 사용할 수 있는 패키지로 express-mysql-session 이 있다.

import * as session from "express-session"
import expressMySqlSession from "express-mysql-session";
import mysql2 from "mysql2/promise";

const MySQLStore = expressMySqlSession(Session);
const connection = mysql2.createPool(config.db);
const sessionStore = new MySQLStore({}, connection);
app.use(
  session({
    secret: "asdfasffdas",
    resave: false,
    saveUninitialized: false,
    store: sessionStore,
    cookie: {
      httpOnly: true,
      secure: false,
    },
  })
);
  • resave
    • false로 해주어야 세션 저장소의 데이터가 바뀌지 않으면 세션을 저장하지 않도록 할 수 있다.
    • true로 지정하게 되면 세션저장소의 data필드의 expires 값이 새로 들어오는 요청마다 갱신이 되므로 원래 기대했던 시간에 삭제되지 않는다.
  • saveUninitialized
    • d.ts에 작성된 설명 : 선언되지 않은 세션이 저장소에 저장되도록 강요하는 옵션이다. 로그인 세션을 구현하는 경우에는 false로 설정해야 서버의 부하를 줄여준다.
    • 따라서 false로 선언하면 회원가입이 성공되면 그때 세션을 선언해줌으로써 세션이 저장소에 저장되도록 할 수 있다.

이때 config로 부터 받아온 object의 타입이 정의되어 있지 않기 때문에 오류가 난다. 따라서 db환경변수 데이터는 다음과 같이 타입을 강제해준다.

db: {
    host: required("DB_HOST") as string,
    port: parseInt(required("DB_PORT")) as number,
    user: required("DB_USER") as string,
    password: required("DB_PASSWORD") as string,
    database: required("DB_DATABASE") as string,
  },

여기까지 설정했으면 마지막으로 이제부터 발생하는 오류를 하나하나 해결해줘야 정상적으로 동작시킬 수 있다.


오류 1. session parameter 속성 부재

express의 session형식에는 설정한적이 없는 속성이기 때문에 TS에서는 입력할 수 가 없다.

다음의 코드를 app.ts의 상단에 작성한다.

declare module "express-session" {
  export interface SessionData {
    is_logined?: boolean;
    dispayName?: string;
    userId?: number;
  }
}

세션에 추가할 속성들에 대해서 위 코드에 선언해주면 할당이 가능해진다.


오류 2. mysql연동 오류

오류메시지는 다음과 같다.

Client does not support authentication protocol requested by server; consider upgrading MySQL client

플러그인이 caching_sha2_password 를 사용하지 못하는 오류이다.

MySQL5,7까지는 mysql_native_password 가 default였다. 하지만 이 경우 hash코드를 탈취하면 비밀번호를 알아낼 수 있는 문제가 생긴다. 이를 막고자 RSA key를 이용한 salt 추가 방법으로 보안을 강화시키기로 했는데 여러번의 연산이 필요한 단점이 생겼다. 다시 이를 보완하기 위해 key와 정보를 메모리에 저장하는 caching_sha2_password plugin 이 기본 플러그인이 되었다.

해결방법1. 모듈 수정

mysql2로 커넥션 풀을 생성해서 연결해주었기 때문에 세션 모듈에서 문제가 있는 것 같다는 의심을 하게 되었고, 세션 관련 모듈의 파일들을 살펴보았다.

그 과정에서 나는 설치한 적 없는 mysql모듈을 발견했다. 이전 typeORM연동에서도 오류를 발생시킨 모듈이었기 때문에 분명 삭제하고 mysql2를 새로 설치했었다.

mysql과 관련있는 express-mysql-session모듈을 확인했더니 index파일의 첫줄에서 mysql모듈을 require하고있었고 이 모듈의 package.json에도 dependencies옵션에 mysql이 있었다.

먼저 mysql모듈을 지우고 세션모듈의 mysql대신 mysql2require해주는 것으로 바꿔주었다. 이 방식에 오류가 없을 것이라고 생각한 이유는 typeorm모듈에 mysql에서 mysql2로 교체가 가능하다고 공식문서에서 참고했었기 때문이다.

재실행 후 정상적으로 쿼리가 생성되는 것을 확인할 수 있었다.

해결방법2. MySQL 수정

select host, user, plugin, authentication_string from mysql.user;

mysql에서 위 쿼리를 날려주면 userroot인 필드의 plugincaching_sha2_password인 것을 확인할 수 있다.

이때 다음 명령어를 입력해줘서 플러그인을 mysql_native_password 로 바꿔준다.

ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '1234';

여기까지 완료되면 정상적으로 2번오류는 더 이상 발생하지 않고 정상 작동한다.


세션 저장하기

auth.controller.ts에서 로그인이 성공했을 시에 세션의 속성에 값을 할당한다.

req.session.is_logined = true;
req.session.userId = user.id;
req.session.dispayName = user.username;

처음 설정할 때 saveUninitialized를 false로 선언했기 때문에 이렇게 속성값을 할당하면 세션 데이터가 req 속성에 추가된다. 추가된 값은 다음과 같다.

[1] Session {
[1]   cookie: {
[1]     path: '/',
[1]     _expires: 2022-01-08T15:49:22.327Z,
[1]     originalMaxAge: 3600000,
[1]     httpOnly: true,
[1]     secure: false
[1]   },
[1]   is_logined: true,
[1]   userId: 1,
[1]   dispayName: 'james'
[1] }

그 다음에는 save를 통해 저장을 해준다. save를 하지않고 응답을 보내도 저장이 되지만 이때 정확하게 저장이 된 이후에 응답을 보내기 위해선 save의 콜백함수로 응답을 반환해주는 것이 좋다.

req.session.save(() => {
        return res.sendStatus(202);
});

세션 삭제하기

로그아웃 요청을 보내면 클라이언트의 브라우저 쿠키에서 세션데이터를 갱신해줘야 한다. session의 destroy 메서드를 사용하면 세션저장소에 있는 데이터와 req.session을 모두 삭제해준다. 따라서 console에 req.session을 찍어보면 undefined가 출력될 것이다. 브라우저의 쿠키는 따로 삭제해주지 않아도 활용할 수 없는 id가 된다.

주의할 점은 destroy의 콜백함수로 응답을 반환해야한다. 그렇지 않으면 삭제가 되지 않은 상태를 보내게 될 수도 있기 때문이다.

0개의 댓글