[nodejs]Passport 모듈로 로컬, 소셜(카카오)로그인 구현
Passport 모듈 사용이유: 회원가입과 로그인을 직접 구현 할 수도 있지만, 세션과 쿠키 처리 등 복잡한 작업이 많으므로 검증된 모듈을 사용.
로그인 전체과정
1. /auth/login 라우터를 통해 로그인 요청이 들어옵니다.
2. 라우터에서 passport.authenticate 메서드를 호출합니다.
3. 로그인 전략(LocalStrategy)를 수행합니다.
4. 로그인 성공 시 사용자 정보 객체와 함께 req.login을 호출합니다.
5. req.login 메서드가 passport.serializeUser 호출합니다.
6. req.session에 사용자 아이디만 저장해서 세션을 생성합니다.
7. express-session에 설정한 대로 브라우저에 connect.sid 세선 쿠키를 전송합니다.
8. 로그인 완료
로그인 이후 과정
1. 요청이 들어옵니다.(어떠한 요청이든)
2. 라우터에 요청이 도달하기 전에 passport.session 미들웨어가 passport.deserializeUser 메서드를 호출합니다.
3. connect.sid 세션 쿠키를 읽고 세션 객체를 찾아서 req.session으로 만듭니다.
4. req.session에 저장된 아이디로 데이터베이스에서 사용자를 조회합니다.
5. 조회된 사용자 정보를 req.user에 저장합니다.
6. 라우터에서 req.user 객체를 사용해서 요청처리를 합니다.
// passport/index.js
const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');
module.exports = () => {
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
User.findOne({ where: { id } })
.then(user => done(null, user))
.catch(err => done(err));
});
local();
kakao();
};
passport.serializeUser, passport.deserializeUser - Passport를 이해하는 핵심.
serializeUser - 로그인 시 실행되며, req.session(세션) 객체에 어떤 데이터를 저장할지 정하는 메서드, 매개변수로 user를 받고 나서 done 함수에 두 번째 인수로 user.id를 넘긴다.
done(err,user,...) - done함수의 첫 번쨰 인수는 에러가 발생할 때 사용, 두 번째 인수는 저장하고 싶은 데이터를 넣는다.
로그인의 경우에는 세션에 사용자 정보를 모두 저장하면 세션의 용량이 커지고 데이터 일관성에 문제가 발생하므로 사요자의 아이디만 저장.
deserializeUser - 각 요청마다 실행, passport.session 미들웨어가 이 메서드를 호출. serializeUser 의 done의 두 번째 인수로 넣었던 데이터가 deserializeUser의 배개변수가 됩니다. 이 경우에는 사용자의 아이디 입니다.
로컬 로그인 구현
회원가입, 로그인, 로그아웃 라우터를 만들어 봅니다. 이러한 라우터에는 접근 조건이 있습니다.
로그인한 사용자 - 회원가입 로그인 라우터 접근불가.
로그인 하지 않은 사용자 - 로글아웃 라우터 접근불가.
라우터에 접근 권한을 제어하는 미들웨어가 필요합니다.
// middlewares/index.js
// 로그인 여부 확인
exports.isLoggedIn = (req, res, next) => {
if (req.isAuthenticated()) {
next();
} else {
res.status(403).send('로그인 필요');
}
};
// 로그아웃 여부 확인
exports.isNotLoggedIn = (req, res, next) => {
if(!req.isAuthenticated()) {
next();
} else{
const message = encodeURIComponent('로그인한 상태입니다');
res.redirect(`/?error=${message}`);
}
};
auth controllers를 구현하겠습니다
// controllers/auth.js
const bcrypt = require('bcrypt');
const passport = require('passport');
const User = require('../models/user');
// 회원가입
exports.join = async (req, res, next) => {
const { email, nick, password } = req.body;
try {
const exUser = await User.findOne({ where : { email } });
// email이 존재할 경우
if (exUser) {
return res.redirect('/join?error=exist');
}
// password 암호화
const hash = await bcrypt.hash(password, 12);
await User.create({
email,
nick,
password: hash,
});
return res.redirect('/');
} catch (error) {
console.error(error);
return next(error);
}
}
회원가입 컨트롤러 입니다.
암호화에 bcrypt 모듈을 사용하였습니다.
// ... 위의 코드계속
exports.login = (req, res, next) => {
passport.authenticate('local', (authError, user, info) => {
if (authError) {
console.error(authError);
return next(authError);
}
if(!user) {
return res.redirect(`/?error=${info.message}`);
}
return req.login(user, (loginError) => {
if (loginError) {
console.error(loginError);
return next(loginError);
}
return res.redirect('/);
});
})(req, res, next); // 미들웨어 내의 미들웨어 에는 (req, res, next)를 붙입니다.
로그인 컨트롤러입니다.
로그인 요청이 들어오면 passport.authenticate('local') 미들웨어가 로컬 로그인 전략을 수행합니다.
미들웨어인데 라우터 미들웨어 안에 들어 있으며, 미들웨어에 사용자 정의 기능을 추가하고 싶을 때 이렇게 할 수 있습니다.
전략이 성공하거나 실패하면 authenticate 메서드의 콜백 함수가 실행됩니다.
authErr 값이 있으면 실패한겁니다.
두번째 매개변수는 사용자 정보입니다. 이 자이레 값이 있다면 성공한 것이고, 이 값으로 req.login 메서드를 호출합니다.
Passport는 req객체에 login과 logout 메서드를 추가합니다.
req.login은 passport.serializeUser를 호출하고, req.login에 제공하는 user객체가 serializeUser로 넘어가게 됩니다.
이 때 connect.sid 세션 쿠키가 브라우저에 전송됩니다.
//...상위 코드에서 이어집니다
exports.logout = (req, res) => {
req.logout(() => {
res.redirect('/');
});
};
로그아웃 컨트롤러입니다.
req.logout 메서드는 req.user 객체와 req.session 객체를 제거합니다.
req.logout 메서드는 콜백 함수를 인수로 받고, 세션 정보를 지운 후 콜백 함수가 실행됩니다.
다음은 localStrategy입니다.
// passport/localStrategy.js
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const User = require('../models/user');
module.exports = () => {
passport.use(new LocalStrategy({
usernameField : 'email',
passwordField : 'password',
passReqToCallback : false,
}, async (email, password, done) => {
try {
const exUser = await User.findOne({ where : { email} });
if (exUser) {
const result = await bcrypt.compare(password, exUser.password);
if (result) {
done(null, exUser);
} else {
done(null, false, {message : '비밀번호가 일치하지 않습니다.' });
}
}else {
done(null, false, { message : '가입되지 않은 회원입니다.' });
}
} catch (error) {
console.error(error);
done(error);
}
}));
};
LocalStrategy 생성자의 첫 번째 인수로 주어진 객체는 전략에 관한 설정을 하는 곳입니다.
usernameField와 passwordField에는 일치하는 로그인 라우터의 req.body속성명을 적습니다.
done함수 - (authError, user, info) 첫번째인수는 서버에러를 표시하고 , 두번째 인수를 사용하지 않는 경우는 로그인에 실패했을 때뿐입니다. 세 번째 인수를 사용하는 경우는 로그인 처리 과정에서 비밀번호가 일치하지 않거나 존재하지 않는 회원일 경우와 같은 사용자 정의 에러가 발생했을 때 입니다.
소셜로그인(카카오)
로그인 인증과정을 소셜에 맡기는 것을 뜻합니다.
따라서 회원 가입 절차가 따로 없습니다. 처음 로그인할 때는 회원 가입 처이를 해야 하고, 두 번째 로그인부터는 로그인 처리를 해야합니다.
소셜로그인 전략
const passport = require('passport');
const KakaoStrategy = require('passport-kakao').Strategy;
const User = require('../models/user');
module.exports = () => {
passport.use( new KakaoStrategy({
clientID: process.env.KAKAO_ID,
callbackURL : '/auth/kakao/callback',
}, async (acessToken, refreshToken, profile, done) => {
console.log('kakao profile', profile);
try {
const exUser = await User.findOne({
where : {snsId : profile.id, provider: 'kakao' },
});
if (exUser) {
done(null, exUser);
} else {
const newUser = await User.create({
email : profile._json?.kakao_account?.email,
nick : profile.displayName,
snsId : profile.id,
provider : 'kakao',
});
done(null, newUser);
}
}catch (error) {
console.error(error);
done(error);
}
}));
};
clientID는 카카오에서 발급해주는 아이디입니다.
노출되지 않아야 하므로 process.env.KAKAO_ID로 설정하였습니다.
callbackURL은 카카오로부터 인증 결과를 받을 라우터 주소입니다.
기존에 카카오를 통해 회원가입을 하였는지 조회합니다. 이미 회원가입 되어 있는 경우이면 사용자 정보와 함께 done함수를 호출하고 전략을 종료합니다.
없다면 회원가입을 진행합니다.
카카오에서는 인증 후 callbackURL에 적힌 주소로 accessToken, refreshToken과 profile을 보냅니다.
profile에는 사용자 정보들이 들어 있습니다. 사용자를 생성한 뒤 done 함수를 호출합니다.