암호화방식이나 타입과 함께 유저아이디, 유효기간 등을 jwt서버로 전달하면 그 정보들을 jwt가 해독할 수 있는 토큰으로 발행받는다. 따라서 토큰에는 모든 정보들이 jwt만 읽을 수 있는 형태로 저장되어 있다.
- 서버에 데이터를 저장하지 않아(stateless)서 스케일링, 유지보수에 유연하다.
- 근데 페이스북 정도가 아니면 스케일에 대응하기 어려운 것은 아닌것 같다.
- jwt정보를 다른 사람이 훔치게 되면 훔친 정보로 로그인이 가능해진다.
- 토큰 유효기간을 짧게 하고 대신 refresh토큰을 사용하면 된다. ⇒ 어디차박에 적용해보자!
- 전달된 토큰은 돌이킬 수 없다.
- 악의적인 사용자가 유효기간전 정보 탈취가 가능해질 수 있다.
- 물론 서버만 알고있는 시크릿키가 있어야 토큰을 디코딩할 수 있지만 토큰자체를 해석하는 방법을 찾는다면 시크릿키 없이도 디코딩이 될 가능성은 분명히 있다.
요청받은 로그인 데이터가 데이터베이스의 데이터와 일치하면 세션을 세션 저장소에 저장한 뒤, 다시 이와 연결되는 세션 ID를 발행해서 쿠키에 실어 보낸다.
- 쿠키 자체에는 계정정보가 들어있지 않기 때문에 http요청 중 노출되더라도 중요한 정보가 노출될 위험 부담이 적어진다.
- 세션 하이재킹 공격(http요청을 가로채 쿠키를 탈취)시에 그 쿠키를 이용해서 요청을 보낼 수 있다.
- ssl을 적용하고 세션에 유효기간을 설정한다.
- 추가 저장공간이 필요하게 되고 확장, 유지보수에 추가 작업이 필요해진다.
아무래도 토큰에 정보들이 들어가있다는 것이 부정적으로 다가온다. 또, 부득이 한 경우에 서버에서 사용자의 로그인 상태를 관리해야 하거나 모든 사용자를 로그아웃시켜야만 하는 경우가 있을 때 jwt인증방식은 할 수 있는 것이 없다. 이러면에서 장점과 단점을 비교했을 때 jwt만 가지는 장점보다 세션방식만 가지는 장점이 더 많아서 세션을 사용한 인증을 구현하기로 했다.
[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] }
//세션저장소 data필드에 저장되는 것
{"cookie":{"originalMaxAge":10000,"expires":"2022-01-08T15:53:35.293Z","secure":false,"httpOnly":true,"path":"/"},"is_logined":true,"userId":1,"dispayName":"james"}
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,
},
})
);
이때 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,
},
여기까지 설정했으면 마지막으로 이제부터 발생하는 오류를 하나하나 해결해줘야 정상적으로 동작시킬 수 있다.
express의 session형식에는 설정한적이 없는 속성이기 때문에 TS에서는 입력할 수 가 없다.
다음의 코드를 app.ts의 상단에 작성한다.
declare module "express-session" {
export interface SessionData {
is_logined?: boolean;
dispayName?: string;
userId?: number;
}
}
세션에 추가할 속성들에 대해서 위 코드에 선언해주면 할당이 가능해진다.
오류메시지는 다음과 같다.
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
이 기본 플러그인이 되었다.
mysql2로 커넥션 풀을 생성해서 연결해주었기 때문에 세션 모듈에서 문제가 있는 것 같다는 의심을 하게 되었고, 세션 관련 모듈의 파일들을 살펴보았다.
그 과정에서 나는 설치한 적 없는 mysql모듈을 발견했다. 이전 typeORM연동에서도 오류를 발생시킨 모듈이었기 때문에 분명 삭제하고 mysql2를 새로 설치했었다.
mysql과 관련있는 express-mysql-session모듈을 확인했더니 index파일의 첫줄에서 mysql모듈을
require
하고있었고 이 모듈의 package.json에도 dependencies옵션에 mysql이 있었다.먼저 mysql모듈을 지우고 세션모듈의 mysql대신 mysql2를
require
해주는 것으로 바꿔주었다. 이 방식에 오류가 없을 것이라고 생각한 이유는 typeorm모듈에 mysql에서 mysql2로 교체가 가능하다고 공식문서에서 참고했었기 때문이다.재실행 후 정상적으로 쿼리가 생성되는 것을 확인할 수 있었다.
select host, user, plugin, authentication_string from mysql.user;
mysql에서 위 쿼리를 날려주면
user
가root
인 필드의plugin
이caching_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의 콜백함수로 응답을 반환해야한다. 그렇지 않으면 삭제가 되지 않은 상태를 보내게 될 수도 있기 때문이다.