express로 세션(session) 구현하기 (feat. passport, express-session)
세션은 회원기능이 필요할 때 가장 기본은 할 수 있는 보장된 인증 방식이다.
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');
}
});