My Boundary As Much As I Experienced

express로 세션(session) 구현하기 (feat. passport, express-session) 본문

BackEnd/Node.js

express로 세션(session) 구현하기 (feat. passport, express-session)

Bumang 2024. 8. 5. 23:36

세션은 원기능이 필요할 때 가장 기본은 할 수 있는 보장된 인증 방식이다.

node.js에서 이를 쉽게 구현하려면 passport와 express-session 등을 많이 사용한다.

 

설치

npm install express-session passport passport-local

 

Express-session

express-session은 Express 애플리케이션에서 세션 관리를 쉽게 할 수 있도록 도와주는 미들웨어이다.

서버에서 각 사용자별로 고유한 세션을 생성하고, 이 세션에 데이터를 저장할 수 있게 한다.

세션ID를 클라이언트에 쿠키로 저장하여 서버와 클라이언트 간의 세션을 연결한다.

const session = require("express-session");

app.use(
  session({
    secret: "암호화에 쓸 비번",
    resave: false, // 유저가 서버로 요청할 때마다 세션을 갱신할건지?
    saveUninitialized: false, // 로그인 안 해도 세션을 만들건지?
    cookie: {
      maxAge: 60 * 60 * 1000 * 24 * 7,
    },
  })
);

 

passport 초기 설정

passport는 회원인증 도와주는 메인라이브러리이다.

그중 passport-local은 passport 중에서도 '아이디/비번 방식 회원인증'을 구현하는 라이브러리이다.

두 개 다 설치해줘야 세션을 구현할 수 있다.

 

일단 초기화는 LocalStrategy라는 객체를 생성하여 콜백 안에 로그인 로직을 전달해주면 된다.

const passport = require("passport");
const LocalStrategy = require("passport-local");

app.use(passport.initialize()); // 초기화
app.use(passport.session()); // 세션용 사용자 정보를 저장하는 미들웨어

// passport 내에 passport-local로 불러온 LocalStrategy를 생성하고
// 사용자 인증 로직을 구현한다.
passport.use(
  new LocalStrategy(async (userId, password, cb) => {
    // db조회해서 유저를 찾아온다.
    let result = await db.collection("user").findOne({ username: userId });
    
    // 유저 조회 결과가 없다면
    if (!result) {
      // 콜백을 리턴. (꼭 이름이 'done'이 아니어도 된다.)
      return done(null, false, { message: "아이디 DB에 없음" });
    }
    // 유저는 존재하는데 password가 같다면
    if (result.password == password) {
      return done(null, result);
    // 유저는 존재하는데 password가 다르다면
    } else {
      return done(null, false, { message: "비번불일치" });
    }
  })
);
// done (
//   에러여부: {...} | null,
//   인증여부: boolean, 에러 시 false
//   추가메시지: { message: ... }
// )

 

직렬화와 역직렬화

직렬화:

직렬화는 예전 포스팅에서 한 번 다뤘다. 객체나 복잡한 구조의 데이터를 비트의 연속으로 된 원시값으로 치환하는 것이다.

JSON.stringify()로 객체를 string화 하는 것을 떠올리면 된다.

 

여기 passport라이브러리에서 직렬화란 DB에서 불러온 무거운 유저 정보 객체를 세션에 다 넣지 않고

정말 필요한 것만 추려 '세션 객체'에 저장하는 과정이다.

 

사용자가 로그인 폼을 제출하고 인증에 성공하면, serializeUser 함수가 호출되어 사용자 정보를 세션에 저장한다.

즉, 직렬화는 로그인 성공 시 실행된다.

// 직렬화
passport.serializeUser((user, done) => {
  // 내부 코드를 비동기적으로 실행(async/await과 비슷)
  process.nextTick(() => {
    done(
      null,
      //세션 document에 기록할 내용
      {
        id: user._id,
        username: user.username,
      }
    );
  });
});

 

직렬화가 이루어지면 세션에 아래처럼 유저정보가 등록된다고 생각하면 된다.

// 세션에 저장되는 정보
session: {
  passport: {
    user: 123
  }
}

 

역직렬화:

역직렬화는 각 요청마다 세션에서 사용자 정보를 복원할 때 실행된다.

클라이언트가 서버에 요청을 보낼 때마다, Passport는 세션에서 저장된 사용자 ID를 사용하여 deserializeUser를 호출한다.

 

아래 예시에서 DB조회해서 가져온 유저 객체에서 password부분은 제거한 채로 라우트에 전달해준다.

보통 password같이 민감한 정보는 될 수 있으면(꼭 필요한게 아니라면) 조회 후 바로 지워준다고 하더라. (확실치 않음)

// 역직렬화
passport.deserializeUser(async (user, done) => {
  let result = await db
    .collection("user")
    .findOne({ _id: new ObjectId(user.id) });
  delete result.password;
  process.nextTick(() => {
    // 실제 최신 유저 or null이 담김
    done(null, result);
  });
});

 

로그인 시 passport에 유저정보를 전달하는 코드

 

앞서 말했듯 직렬화가 이루어지기 전에 로그인 시도를 구현하는 부분이다.

passport.authenticate(...)를 실행하는 것 같다.

app.post("/login", async (req, res, next) => {
  // 3개의 인자를 콜백에 담을 수 있음
  // error: 말그대로 에러가 있을 시 오는 객체
  // user: 성공 시 유저 정보
  // info: 실패 시 이유
  passport.authenticate("local", (error, user, info) => {
    if (error) {
      // db조회 실패 및 서버 에러 시
      return res.status(500).json(error);
    } else if (!user) {
      // user 조회는 했으나 정보가 다를 시
      return res.status(401).json(info.message);
    }

    // 성공 시
    req.logIn(user, (err) => {
      if (err) return next(err);

      res.redirect("/");
    });
  })(req, res, next);
  // passport 미들웨어는 요청, 응답, next를 모두 받아서 처리함
});

 

역직렬화된 데이터를 각 라우트에서 활용하는 법

req.user 혹은 req.usAuthenticated()를 통해 역직렬화된 세션 유저데이터를 활용할 수 있다.

인증 여부를 아래처럼 req.usAuthenticated()를 통해 알 수도 있고,

req.user.username 등 user 객체에 있는 정보들을 활용할 수 있다.

// 프로필 라우트
app.get('/profile', (req, res) => {
  if (req.isAuthenticated()) {
    res.send(`Hello ${req.user.username}, your email is ${req.user.email}`);
  } else {
    res.redirect('/login');
  }
});