카카오, 트위터, 구글 등등 소셜 로그인 지원 모듈 (추후 업데이트)
기본적인 이메일, 아이디 로그인 지원 모듈
app.use(passport.initialize());
app.use(passport.session());
일단 passport
모듈 내부의 세션 관련 지원 모듈을 미들웨어로 등록한다.
// passport/local.js
const passport = require('passport');
const { Strategy: LocalStrategy } = require('passport-local');
const bcrypt = require('bcrypt');
const { User } = require('../models');
module.exports = () => {
passport.use(new LocalStrategy(
{
usernameField: 'email',
passwordField: 'password',
},
async (email, password, done) => {
try {
const user = await User.findOne({
where: { email },
});
if (!user) {
return done(null, false, { reason: '존재하지 않는 이메일입니다!' });
}
const result = await bcrypt.compare(password, user.password);
if (result) {
return done(null, user);
}
return done(null, false, { reason: '비밀번호가 일치하지 않습니다.' });
} catch (error) {
console.error(error);
return done(error);
}
},
));
};
passport
모듈의 use
메서드의 인자로
passport-local
모듈 안의 Strategy
인스턴스를 넘긴다.
Strategy
의 생성자는 두 개의 인자를 필요로 하는데
첫 번째 인자는 로그인시 필요한 파라미터의 필드명을 담은 오브젝트
{ usernameField: 'email', passwordField: 'password' }
두 번째 인자는 로그인 로직을 담은 함수
async (email, password, done) => { dosomething... }
첫 번째 인자에서 명시한 필드를 차례로 인자로 받고
마지막 인자로는done
함수를 받는다.
done
함수는 3개의 인자를 받는데
첫 번째 인자는 서버 에러,
두 번째 인자는 성공 객체,
세 번째 인자는 클라이언트 에러를 받는다.
커스텀도 가능하다.
// passport/index.js
const passport = require('passport');
const local = require('./local');
const { User } = require('../models');
module.exports = () => {
// 로그인 성공시 실행될 콜백 등록
passport.serializeUser((user, done) => {
done(null, user.id); // 세션에 userId를 저장한다.
});
// 로그인 이후 자동으로 실행되는 콜백 등록
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findOne({ where: { id }});
done(null, user); // req.user에 넣어준다.
} catch (error) {
console.error(error);
done(error);
}
});
local(); // import한 로컬 로그인 전략 실행, passport의 로그인 전략에 로컬 전략이 추가됐다.
};
그 다음 index.js
에서 각종 로그인 전략 파일들을 import
해서 실행한다.
지금은 local
만 있지만 google, kakao
도 파일을 만들어 추가할 수 있다.
// POST /user/login 로그인
router.post('/login', (req, res, next) => (
passport.authenticate('local', (error, user, info) => {
if (error) {
console.error(error);
next(error);
}
if (info) {
return res.status(401).send(info.reason);
}
return req.login(user, async (loginError) => {
if (loginError) {
console.error(loginError);
return next(loginError);
}
return res.status(200).json(user);
});
})(req, res, next)
));
그 다음 userRouter.js
에서 passport
모듈을 import
해서
로그인 api 에 passport.authenticate
를 등록시킨다.
passport.authenticate
는
첫 번째 인자로 전략 객체명
을 받고
두 번째 인자로 커스텀 done
함수를 받는다.
나중에 유저가 로그인할 때 passport.authenticate
가 리턴한 함수가 실행되고
그 함수는 passport/local.js
에서 Strategy
생성자의 두 번째 인자로 넘겼던
// Strategy 생성자의 두 번째 인자로 넘겼던 함수
async (email, password, done) => {
try {
const user = await User.findOne({
where: { email },
});
if (!user) {
return done(null, false, { reason: '존재하지 않는 이메일입니다!' });
}
const result = await bcrypt.compare(password, user.password);
// 로그인 성공
if (result) {
return done(null, user);
}
return done(null, false, { reason: '비밀번호가 일치하지 않습니다.' });
} catch (error) {
console.error(error);
return done(error);
}
}
로그인 전략 함수를 실행한다.
성공하면 done(null, user)
이 부분이 실행되는데
이 done
함수는 아까 passport.authenticate
의 두 번째 인자로 넘겼던 커스텀 done
함수이다.
// passport.authenticate의 두 번째 인자로 넘겼던 커스텀 done함수
(error, user, info) => {
if (error) {
console.error(error);
next(error);
}
if (info) {
return res.status(401).send(info.reason);
}
req.login(user, async (loginError) => {
if (loginError) {
console.error(loginError);
return next(loginError);
}
return res.status(200).json(user);
});
}
로그인을 성공했다면 마지막에 req.login
을 실행하는데
원래 기본 express
가 지원하는 req
객체에는 login
메서드가 없지만
passport
모듈이 붙여준다.
login
메서드를 실행하면 내부적으로
res.setHeader('Set-Cookie', 'afsdfs');
를 실행시켜서 쿠키를 등록해주고
passport/index.js
에서 등록한 함수를 실행시킨다.
// 로그인 성공시 실행될 함수 등록
passport.serializeUser((user, done) => {
done(null, user.id);
});
done
함수를 통해 user.id
만 세션에 등록한다.
간단한 passport
모듈 내부 구현 상상도
// 로컬 전략 클래스
class LocalStrategy {
constructor(fields, strategyFn) {
this.fields = fields;
this.strategyFn = strategyFn;
}
get getName() {
return 'local';
}
// 전략 함수 리턴
getStrategy() {
const fieldNames = Object.values(this.fields);
// reqBody와 customDone을 받아서
return (reqBody, customDone) => {
// 필드를 모은 다음
const fields = fieldNames.map((fieldName) => reqBody[fieldName]);
// 등록한 전략 함수를 실행
this.strategyFn(...fields, customDone);
};
}
}
// 패스포트 모듈
const passport = (() => {
// 로그인 전략들을 담을 오브젝트
const strategys = {};
// 로그인 성공시 실행할 함수
let serializeUserFn;
// 로그인 전략 추가
const use = (strategyInstance) => {
Strategys[strategyInstance.getName] = strategyInstance.getStrategy();
};
// 로그인 성공시 실행할 함수 등록
const serializeUser = (fn) => {
serializeUserFn = fn;
};
// 인증 함수
const authenticate = (strategyName, customDone) => (req, res, next) => {
// req에 로그인 함수 붙여주기
req.login = (user, callback) => {
// 쿠키를 등록
res.setHeader('Set-Cookie', 'adsfsad');
// 로그인 성공시 실행할 함수 실행 (세션 등록)
serializeUserFn(user, (id) => res.session.email = id);
callback();
};
// 전략 함수 실행
strategys[strategyName](req.body, customDone);
};
return {
use,
serializeUser,
authenticate,
};
})();
대충 이런 흐름으로 구현돼있지 않을까 싶다.
인피니트 스크롤 구현 때
메모리 안 터지도록 구현해주는 모듈.
더미데이터 테스트 도와줌