회원가입과 로그인 같은 복잡한 작업이 많은 기능을 쉽게 구현할 수 있게 도와주는 검증된 모듈이다.
passport 문법은 이해하면 쉽지만, 이해하기까지의 과정은 복잡하고 난해하기 때문에 이 과정을 기록하려 한다.
npm i passport passort-local passport-kakao bcrypt
-> 비밀번호 암호화를 도와주는 bcrypt도 같이 설치한다.
-> 소셜 네트워크로 로그인도 가능하기 때문에, kakao도 함께 설치한다.
-> 루트에 passport라는 폴더 생성 후, 전략 파일들을 넣어줄 것이다.
-> 전략 파일이란, 로그인 인증 전략을 말하는데 우리는 local과 kakao를 설치했으니 localStrategy.js 파일과 kakaoStrategy.js 파일을 생성한다.
-> index.js파일도 생성
// app.js
const passport = require('passport');
const passportConfig = require('./passport');
const app = express();
passportConfig(); //🔑 passport 설정
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
resave : false,
saveUninitialized : false,
secret : process.env.COOKIE_SECRET,
cookie : {
httpOnly : true,
secure : false,
},
}));
app.use(passport.initialize()); // 🔑 요청 객체에 passport 설정을 심음
app.use(passport.session()); //🔑 req.session객체에 passport 정보 저장
-> passport.session()이 실행되면, 세션 쿠키 정보를 바탕으로 해서 passport/index.js의 deserializeUser()가 실행 , deserailizeUser는 추후 만들 예정.
// passport/index.js
const passport = require('passport');
const local = require('./lcoalStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');
module.exports = () => {
passport.serializeUser((user, done) => {
done(null, user.id);
});
-> 🔑 로그인이 성공했을 때 실행될 함수
-> req.session 객체에 어떤 데이터를 저장할 지 선택, 사용자의 아이디만 저장 (메모리 차지때문)
passport.deserializeUser((id, done) => {
User.findOne({ where : { id })
.then(user => done(ull, use))
.catch(err => done(err));
});
-> req.session에 저장된 사용자 아이디를 바탕으로 db 조회로 사용자 정보를 얻어낸 후
-> req.user에 저장
-> 사용자가 사이트에 접속할 때마다 호출되는 함수
local();
kakao();
}
본격적으로 로그인,회원가입 라우트를 작성하기 전, passport 처리과정을 정리하고자 한다.
✨ 로그인 과정 ✨
1. 로그인 요청이 들어옴 (routes/auth)
2. passport.authenticate 메서드 호출 (전략 파일 실행)
3. 로그인 전략 수행 (2번에서 생성한 localStrategy, kakaoStrategy 작성)
4. 로그인 성공 시 사용자 정보 객체와 함께, req.login 호출
5. req.login 메서드가 passport.serializeUser 호출 (passport/index.js의 함수 serailizeUser)
6. req.session에 사용자 아이디만 저장 ( { 세션쿠키 : 유저id } 형태로 메모리에 저장)
7. 로그인 완료
🐾 로그인 이후 과정 🐾
1. 모든 요청에 passport.session() 미들웨어가 passport.deserializeUser메서드 호출 (passport/index.js의 함수 deserializeUser)
2. req.session에 저장된 아이디로 데이터베이스에서 사용자 조회
3. 조회된 사용자 정보를 req.user에 저장
4. 라우터에서 req.user 객체 사용 가능
const express = require('express');
const passport = require('passport');
const { isNotLoggedIn } = require('../middlewares');
const { join } = require('../controllers/auth');
const router = express.Router();
router.post('/join', isNotLoggedIn, join);
module.exports = router;
⭐ route.post('/join', isNotLoggedIn, join); 코드 설명
-> /join : 라우트 이동 path
-> isNotLoggedIn : 로그인 여부 확인 middleware
-> join : 실질적으로 회원가입하는 로직을 보여주는 controller
middlewares 폴더 생성 -> index.js 파일 생성
// 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}`);
res.status(403).send('로그인 필요');
}
};
controllers 폴더 생성 -> auth.js 파일 생성
// controllers/auth.js
const passport = require('passport');
const User = require('../models/user');
const bcrypt = require('bcrypt');
exports.join = async (req, res, next) => {
const { nick, email, password } = req.body;
try {
const exUser = await User.findOne({ where : { email }});
if(exUser) {
return res.redirect('/join?error=exist');
} // 이미 존재하는 이메일이면 리다이렉팅
const hash = await bcrypt.hash(password,12); // 🔑암호화
await User.create({
email,
nick,
password : hash
})
return res.redirect('/');
} catch (error) {
console.log(error);
next(error);
}
}
const authRouter = require('../routes/auth');
app.use('/auth', authRouter);
-> 위 두 코드 추가해주면, '/auth'로 회원가입 라우터에 접근 가능
-> 기존의 auth.js의 코드에 로그인 관련 코드만 추가해줄 것임.
const express = require('express');
const passport = require('passport');
const { isNotLoggedIn } = require('../middlewares');
const { join , login} = require('../controllers/auth'); // 로그인 컨트롤러도 추가로 가져오기
const router = express.Router();
router.post('/join', isNotLoggedIn, join);
router.post('/login', isNotLoggedIn, login); // 로그인 라우터 추가, 미들웨어는 동일
module.exports = router;
-> isNotLoggedIn 미들웨어 작성은 이미했으니, 생략
// controllers/auth.js
exports.login = async (req, res, next) => {
passport.authenticate('local', (authError, user, info) => {
if(authError) {
console.error(authError);
return next(authError);
} // 서버 실패시
if(!user) {
return res.redirect(`/?loginError=${info.message}`);
} // 유저 없을 때 (로직 실패)
return req.login(user, (loginError) => {
if(loginError) {
console.log(loginError);
return next(loginError);
}
return res.redirect('/');
}) // 인증이 성공하면, req.login으로 세션에 유저 정보 저장
})(req, res, next) // 미들웨어 확장 패턴
};
2번 코드 해석
🔺 passport.authenticat('local', ...)
-> 이 부분이 전략을 말함. passport의 특성인데 이 코드에서 먼저 localStrategy 파일로 이동해 전략 과정을 거친다. localStrategy는 곧 작성할 예정.
🔺 (authError, user, info)
-> localStrategy에 작성될 done메소드에 들어가는 파라미터와 동일한 역할을 한다고 볼 수 있음. -> done(null, false, {message : '비밀번호 불일치' } ) 이렇게 결과를 내려주면 이를 각각 authError, user, info로 해석할 수 있음
⭐ req.login(user, ..)
-> 전략(localStrategy) 코드에서 성공해서 done(null, exUser)이런 식으로 결과를 내려주면 실행되는 로그인 코드. req.login으로 세션에 유저 정보를 저장한다. 그리고, req.login 메서드가 passport.serializeUser 호출 (passport/index.js의 함수) -> serailizeUser가 req.session에 사용자 아이디만 저장해줌 -> 로그인 완료 ⭐
const passport = require('passport');
const User = require('../models/user');
const { Strategy : LocalStrategy } = require('passport-local').Strategy;
// -> LocalStrategy라는 이름으로 Strategy 생성을 할 수 있음.
const bcrypt = require('bcrypt');
module.exports = () => {
passport.use(new LocalStrategy({
usernameField : 'email', // req.body.email
passwordField : 'password', // req.body.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);
console.log(exUser, 'user존재')
// 일치한다면 exUser 보내주기
} else {
done(null, false, { message : '비밀번호가 일치하지 않습니다.'})
}
} else {
done(null, false, { message : '가입되지 않은 회원입니다.'});
}
} catch (error) {
console.error(error);
done(error);
}
}))
};
-> app.js에서 이미 auth로 로그인/회원가입 라우터 등록했으니, 그 과정은 생략
// pasport.kakaoStrategy.js
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', // 🎈 카카오 로그인 후 카카오가 결과를 전송해줄 url
}, async (accessToken, refreshTokeny, 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?.kaka_account?.email,
nick : profile.displayName,
snsId : profile.id,
provider : 'kakao',
});
done(null, newUser);
}
} catch (error) {
console.error(error);
done(error);
}
}));
};
🎉 코드 추가 해석
async (accessToken, refreshToken, profile, done)
-> accessToken, refreshToken은 로그인 성공 후 카카오가 보내준 토큰이다. 현재는 사용하지 않는다
-> profile은 카카오가 보내준 유저 정보이다.
-> profile의 정보를 바탕으로 카카오 회원가입도 가능한 것.
// routes/auth.js
// GET /auth/kakao
router.get('/kakao', passport.authenticate('kakao'));
// GET /auth/kakao/callback
router.get('/kakao/callback, passport.authenticate('kakao', {
failureRedirect: '/?loginError=카카오로그인 실패',
}), (req, res) => {
res.redirect('/');
});
module.exports = router;
-> 접속해서 회원가입 후 앱 키 저장.
🎉 위의 과정들이 모두 끝나면, passport 모듈을 이용해
회원가입/로그인/카카오로그인 모두 가능할 것이다.