My Boundary As Much As I Experienced

express-session과 passport.js로 세션만들기 심화. (개념/동작 흐름 매우 구체적으로 정리) 본문

BackEnd/Node.js

express-session과 passport.js로 세션만들기 심화. (개념/동작 흐름 매우 구체적으로 정리)

Bumang 2024. 8. 28. 00:12

공부하게 된 계기:

현재 듣는 express 강의에서 지금까지 App객체로 모든 라우트를 관리하다가

페이지 별로 분리하여 router로 분리하는 법을 알려주었다.

그리고 학생한테 강의에서 분리하지 않은 로직들도 모두 모듈화해보라는 숙제를 내주어서

이를 수행하고 있었는데... passport와 express-session 코드 부분을 분리할 때 조금 애를 먹었다.

 

왜냐하면 강의에서 이들 라이브러리에 대해 대강 추상적인 설명만 듣고 넘어갔던터라

각각의 import/export, 호출 시점, 동작 흐름에 대해 엄밀히 알지 못했기 때문에

파일로 분리하기가 힘들었던 것이다.

 

그래서 이제 엄밀하게 공부해보면서 세션에 대한 이해도를 올렸는데, 이를 설명해보겠다.

 

1. express-session과 passport의 차이는 무엇이고 어떤 관계인가?

express-session의 역할

express-session은 Express.js 애플리케이션에서 세션(session)을 만들기 위한 미들웨어이다.

복잡할거 없이 정말 말그대로 '세션'을 생성한다. 그러므로 세션인증방식을 채택하려면

거의 필수적으로 깔게 되는 라이브러리다.

 

아래 방식처럼 앱의 미들웨어에 세션을 생성하여 제공한다.

그러면 세션이 일단 생성된 것이다.

app.use(session({
  secret: 'your_secret_key', // 세션 암호화를 위한 비밀 키
  resave: false,             // 세션 데이터가 바뀌지 않더라도 세션을 다시 저장할지 여부
  saveUninitialized: false,  // 초기화되지 않은 세션을 저장할지 여부
  cookie: {
    maxAge: 1000 * 60 * 60 * 24 * 7 // 쿠키의 만료 시간 (밀리초로 설정)
  }
}));

 

 

passport.js의 역할

그렇다면 passport는 무엇인가? passport는 인증에 관련된 여러가지 관리 기능을 가진 미들웨어이다.

당연히 세션도 세세하게 컨트롤할 수 있다. 즉, express-session으로 만든 세션을 passport로 조작한다고 보면 된다.

물론 passport없이 세션을 관리할 수 있긴 하다. 그러나 passport가 제공하는 편의기능, 추상화된 기능들을 일반적으로 많이 사용한다.

 

passport의 다양한 전략

Passport는 전략(Strategy)이라는 이름으로 여러가지 모듈로 나뉘어져있다.

각각의 전략은 각각의 인증 방법을 정의한 모듈이다.

예를 들어, passport-local 전략은 아이디/비밀번호(사용자 이름과 비밀번호)를 확인하는 로직을 담고 있다.

이밖에도 Google OAuth, Facebook OAuth 등을 다룬 전략들도 존재한다.

  • 각 전략은 passport.use() 메서드를 통해 설정된다.
  • 예시: LocalStrategy, GoogleStrategy, FacebookStrategy

 

express-session과 passport.js로 세션을 설정하는 방법 (실행 순서 중요!)

express-session으로 먼저 session을 만든 다음에

passport의 initialize를 진행하고 passport 세션을 실행해야된다.

말했듯 express-session으로 세션을 먼저 만들어주고, passport로 이것을 관리하는 개념이기 때문.

// 세션 미들웨어가 passport 초기화보다 먼저 설정되어야 합니다.
app.use(
  session({
    secret: process.env.SECRET,
    resave: false, // 유저가 서버로 요청할 때마다 세션을 갱신할건지?
    saveUninitialized: false, // 로그인 안 해도 세션을 만들건지?
    cookie: {
      maxAge: 60 * 60 * 1000 * 24 * 7,
    },
    store: MongoStore.create({
      mongoUrl: process.env.DB_URL,
      dbName: "forum",
    }),
  })
);

// Passport 초기화
app.use(passport.initialize());
app.use(passport.session());

 

2. passport.use()로 전략 설정

그리고 다음으로 passport.use(...)에 구사하고자 하는 전략을 설정하게 된다.

아래 전략은 아이디/비밀번호를 인증방법으로 사용하는 LocalStrategy 전략이다.

 

아래 코드처럼 설정해놓으면 언제 동작하는가? 바로 로그인 시도가 발생할 때이다.

로그인 시도가 발생할 때 아래 개발자가 짜놓은 로직을 실행시켜준다. (로직은 각자 짜기 나름이다.)

 

그러나 아래 로직은 범용적이고 직관적이니 참고할만하다.

몽고db에서 user를 찾고 유저아이디가 없으면 실패, 유효한 비밀번호가 아니면 실패시킨다.

그리고 유저아이디가 있으며 비밀번호도 유효하면 성공을 시킨다.

//그리고 passport.use(...)를 통해 구사하고자 하는 전략을 설정한다.
passport.use(new LocalStrategy(async (username, password, done) => {
  const user = await db.collection('users').findOne({ username });
  if (!user) {
    return done(null, false, { message: 'Invalid username' });
  }

  const isValidPassword = await bcrypt.compare(password, user.password);
  if (!isValidPassword) {
    return done(null, false, { message: 'Invalid password' });
  }

  return done(null, user);
}));

 

3. new LocalStrategy에 제공하는 비동기 함수 설명

LocalStrategy를 생성자 함수로 만들면서 콜백 하나를 제공해주는데 이는 아래와 같다.

username, password, done이라는 패러미터를 받아 활용하는데, 이는 각각 유저아이디, 패스워드, 성공여부 콜백이다.. 당연하게도.

어디서 이것들을 받아오는가? 바로 유저가 로그인 시도할 때 html의 form데이터에서 username, password 컬럼을 참조해와서

첫 번째, 두 번째 패러미터를 받아오는 것이다.

 

유저가 보낸 값에 username과 password가 있으면 passport.use 미들웨어가 냉큼 캐치해오는 것이다. 

그리고 로직 상으로 이리저리 검사한 다음에 done함수로 인증을 판정해주는 것이다.

 

done함수의 첫 번째 인자는 error여부, 두 번째는 user정보, 세 번째는 기타 정보 (메시지 등) 설정이다.

error는 boolean값이고, user는 db같은데서 조회해서 가져온 raw user정보를 제공해주는 편이다.

async (username, password, done) => done(error, user, info);

 

 

4. done으로 성공/실패 판정을 내리면 passport는 이후 어떻게 작동하나? (직렬화)

Passport.js는 done이 호출되면, 이를 바탕으로 다음 작업을 수행한다

  • 인증 성공 시: req.login()을 호출하여 사용자를 세션에 저장하고, req.user에 사용자 객체를 할당한다.
  • 인증 실패 시: 적절한 에러 메시지나 리다이렉트를 처리한다.

이렇게 로그인에 성공하면 바로 직렬화 과정을 거쳐 유저 데이터를 단순화하고 이를 세션db에 저장한다.

직렬화(serialization)의 사전적 의미는 복잡한 객체구조나 배열을 원시구조로 단순화하는 과정을 얘기한다.

(사실 꼭 객체 -> 원시값일 필요는 없고, 단순화해서 데이터를 아낀다는 것에 의의를 두면 된다.)

// done(error, selializedValue)
passport.serializeUser((user, done) => {
  done(null, user._id); // 객체에서 user._id만 빼서 세션에 저장
});

여기까지가 로그인 요청을 passport가 어떻게 가공하는지를 설명한 것이다.

그리고 아래에는 추가적으로 이제 인증이 필요한 요청이 왔을 때

어떻게 passport가 세션 정보를 꺼내 쓰는지를 설명하겠다.

 


 

5. 클라이언트의 후속 요청을 역직렬화(deserialize)

  • 사용자가 로그인한 후, 브라우저는 클라이언트의 상태를 유지하기 위해 쿠키를 통해 세션 ID를 서버에 자동으로 전송한다.
  • 예를 들어, 사용자가 특정 페이지에 접근하거나 데이터를 조회하는 요청을 보낼 때, 클라이언트의 브라우저는 이전에 저장된 세션 ID를 포함한 요청을 서버로 보낸다.

로그인 성공 시 서버에서 클라이언트로 쿠키에 세션 키와 인증 만료 시간이 적힌 정보를 전달한다.

그러면 기본적으로 클라이언트에서 보내는 모든 요청엔 쿠키가 포함된다.

그러면 클라이언트에서 받아온 세션에 적힌 _id값으로 다시 세션db를 뒤져서 온전한 user정보를 받아온 다음에

콜백함수에 다시 제공한다. (동일하게 첫 번째 패러미터는 에러여부, 두 번째는 유저객체)

passport.deserializeUser(async (id, done) => {
  try {
    const user = await db.collection('users').findOne({ _id: new ObjectId(id) });
    done(null, user);
  } catch (err) {
    done(err, null);
  }
});

 

6. req.user에 사용자 정보 할당

  • deserializeUser 메서드에서 조회된 사용자 정보는 done(null, user)을 통해 전달되며, Passport.js는 이 정보를 요청 객체의 req.user에 할당한다.
  • 이로써 이후 요청에서 로그인된 사용자의 정보에 접근할 수 있게 된다.

즉, 로그인한 이후 요청은 모두 deserializeUser를 거쳐서 req.user를 가공하여 실제 route에 전달하게 되는 것이다.

모든 요청이 필수적으로 거치는 미들웨어인 것이다.

 

6. 흐름 정리

  • done(null, user) 호출 → req.login 실행 → serializeUser 호출 및 세션 저장 → 요청 응답
  • 후속 요청: 세션 ID 확인deserializeUser 호출 → req.user에 사용자 정보 할당

 

자 여기까지 passport를 통해서 어떻게 세션이 관리되는지를 자세히 알아보았다.

jwt방식이 메이저가 되었어도.. 역시 클래식한 방법도 알아둬야하는 법.

추후 개인블로그를 express와 세션 방식으로 백엔드 구성을 해보면서 실제 프로젝트로 실습해봐야겠다.

 

 

 

PS. mongo-connect를 안 쓰면 서버 메모리에 저장되는건가?

그렇다. connect-mongo와 같은 외부 스토어를 사용하지 않으면,

기본적으로 express-session은 세션 데이터를 서버의 메모리(즉, 서버 프로세스의 메모리) 내에 저장함.

 

서버 메모리에 저장하는 방식의 단점은

서버 메모리를 너무 많이 잡아먹는다는 점과 서버가 죽으면 모든 세션이 만료되게 된다는 것이다.

앞서 말했듯이 passport랑 같이 쓰면 세션 만료 시 삭제도 알아서 해주니 같이 사용하는 것이 좋다.

 

설치:

npm install connect-mongo

 

활용:

app.use(session({
  secret: 'your_secret_key',
  resave: false,
  saveUninitialized: false,
  cookie: { maxAge: 1000 * 60 * 60 * 24 * 7 }, // 7일
  store: MongoStore.create({ mongoUrl: 'mongodb://localhost:27017/sessions' }) // express-session으로 만든 세션에 store 컬럼을 추가해주면 된다.
}));

 

  • 세션 만료 시 MongoDB에서 자동 삭제: connect-mongo는 세션 데이터에 TTL 인덱스를 적용하여, 세션이 만료되면 MongoDB에서 해당 데이터를 자동으로 삭제된다.
  • 세션 만료 설정: express-session에서 설정한 cookie.maxAge 값이 MongoDB의 TTL 인덱스와 연동되어 세션 만료 처리가 이루어진다.
  • Passport와의 통합: Passport를 사용하여 세션 기반 인증을 할 때도, 이 TTL 인덱스가 적용되어 세션 관리가 자동으로 처리된다.