본격적으로 백엔드 부분을 구현해보자! 이번 포스팅은 이 전 게시글과 이어진다. 이전에 작성한 코드(sns2)와 일부 내용이 달라지므로 git에 올라가있는 sns 폴더를 sns3 폴더로 복사해 작업했다. 구현하는 부분이 생길수록 계속 복사할 것이다.( sns4, sns5 ...)
책 Node.js 교과서(개정 2판) 책의 9장의 내용을 참고했다.
+모든 코드는 github주소에 있다.
지금까지 구현한 것
- 프로젝트 기본 뼈대 잡기
- 프론트엔드 화면 구현하기
- DB 세팅하기
이번 포스팅에서 구현할 것
- 로그인 구현하기(with Passport 모듈)
- Passport 기본 세팅
- 로컬 로그인 구현
- sns 로그인 구현(with kakao)
Github: https://github.com/delay-100/study-node/tree/main/ch9/sns3
과거 포스팅에서 로그인을 모듈없이 구현해봤었는데, 검증된 모듈(Passport)을 사용해보자!
[설치할 모듈]
npm i passport passport-local passport-kakao bcrypt
실행화면(console)
Git [sns3/app.js
] - 설치한 passport 모듈을 app.js와 연결
...
const passport = require('passport');
...
const passportConfig = require('./passport'); // require('./passport/index.js')와 같음
...
passportConfig(); // 패스포트 설정, 한 번 실행해두면 ()에 있는 deserializeUser 계속 실행
...
// passport 사용 - req.session 객체는 express-session에서 생성하므로 express-session 뒤에 작성해야함
app.use(passport.initialize()); // 요청(req 객체)에 passport 설정을 심음
app.use(passport.session()); // req.session 객체에 passport 정보를 저장(요청으로 들어온 세션 값을 서버에 저장한 후, passport 모듈과 연결)
...
Git [sns3/passport/index.js
] - Passport 관련 기본 코드 작성(serializeUser
, deserializeUser
)
const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');
module.exports = () => {
// 세션에 불필요한 데이터를 담아두지 않기 위한 과정들(serializeUser, deserializeUser)
// serializeUser: 사용자 정보 객체를 세션에 아이디로 저장
passport.serializeUser((user,done) => { // serializeUser: 로그인 시 실행됨, req.session(세션) 객체에 어떤 데이터를 저장할지 정하는 메서드
done(null, user.id); // done 첫 번째 인수: 에러 발생 시 사용, done 두 번째 인수: 저장하고 싶은 데이터를 넣음
// user.id만 저장한 이유: 세션에 user의 모든 정보를 저장하면 서버의 용량이 낭비되기 때문
});
// deserializeUser: 세션에 저장한 아이디를 통해 사용자 정보 객체를 불러옴
// passport.session 미들웨어가 이 메소드를 호출
passport.deserializeUser((id, done) => { // deserializeUser: 매 요청 시 실행, id: serializerUser의 done으로 id 인자를 받음
User.findOne({where:{id}}) // db에 해당 id가 있는지 확인
.then(user => done(null, user)) // req(요청).user에 저장 -> 앞으로 req.user을 통해 로그인한 사용자의 정보를 가져올 수 있음
.catch(err => done(err));
});
local();
kakao();
};
로컬 로그인?: 타 sns 서비스가 아닌, 서비스 자체적으로 회원가입 후 로그인을 하는 것
+여기서는 로그인 전략(동작) 위주로 다루고 회원가입은 따로 만들어보겠다.
+잘 이해가 되지 않는 부분은 이 블로그를 참고했다.
Passport에서 로컬 로그인을 구현하려면 passport-local 모듈이 필요하다.
Git [sns3/routes/middlewares.js
] - 로그인 확인관련 미들웨어 구현(isLoggedIn, isNotLoggedIn)
// 로그인 확인 관련 미들웨어 생성
// 로그인이 된 상태를 확인하는 미들웨어
exports.isLoggedIn = (req, res, next) => {
// 로그인이면 허용
if(req.isAuthenticated()){ // req.isAuthenticated(): 로그인 중이면 true, 아니면 false
next(); // 다음 미들웨어로 넘겨줌
} else { // 로그인이 아니면 비허용
res.status(403).send('로그인 필요');
}
};
// 로그인이 되지 않은 상태를 확인하는 미들웨어
exports.isNotLoggedIn = (req, res, next) => {
// 로그인이 아니면 허용
if(!req.isAuthenticated()){
next(); // 다음 미들웨어로 넘겨줌
} else{ // 로그인이면 허용
const message = encodeURIComponent('로그인한 컴포넌트입니다.');
res.redirect(`/?error=${message}`); // 에러 페이지로 바로 이동시킴
}
};
Git [sns3/routes/page.js
] - middlewares.js
에서 만든 미들웨어를 page 라우터에 적용
// app.js에서 기본 router로 설정한 page.js
const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares'); // 구조분해할당으로 middlewares의 두 미들웨어를 가져옴
const router = express.Router();
// 모든 요청마다 실행
router.use((req,res,next)=>{
// res.locals.user = null; // res.locals는 변수를 모든 템플릿 엔진에서 공통으로 사용, 즉 user는 전역 변수로 이해하면 됨(아래도 동일)
res.locals.user = req.user; // 요청으로 온 유저를 넌적스에 연결
res.locals.followerCount = 0;
res.locals.followingCount = 0;
res.locals.followerIdList = [];
next();
});
// http://127.0.0.1:8001/profile 에 get요청이 왔을 때
router.get('/profile', isLoggedIn, (req, res) => {
res.render('profile', { title: '내 정보 - sns'});
});
// http://127.0.0.1:8001/join 에 get요청이 왔을 때
router.get('/join', isNotLoggedIn, (req, res)=>{
res.render('join', {title: '회원가입 - sns'});
});
// http://127.0.0.1:8001/ 에 get요청이 왔을 때
router.get('/', (req, res, next) => {
const twits = [];
res.render('main', {
title: 'sns',
twits,
});
});
module.exports = router;
Git [sns3/routes/auth.js
] - 회원가입, 로컬 로그인, 로그아웃 라우터 작성(/auth/join, /auth/login, /auth/logout)
// 회원가입, 로컬 로그인, 로그아웃 라우터
const express = require('express');
const passport = require('passport');
const bcrypt = require('bcrypt');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const User = require('../models/user');
const router = express.Router();
// 회원가입 라우터, /auth/join
router.post('/join', isNotLoggedIn, async (req, res, next) => {
const { email, nick, password } = req.body;
try {
const exUser = await User.findOne({ where: {email}}); // User db에 같은 email이 있는지 확인
if(exUser){ // 이미 User가 존재하면
return res.redirect('/join?error=exist'); // 주소 뒤에 에러를 쿼리스트링으로 표시
}
// User이 존재하지 않으면(회원가입 가능)
const hash = await bcrypt.hash(password, 12); // bcrypt 모듈을 이용해 비밀번호 암호화 - crypto 모듈의 pbkdf2 메서드를 이용해 암호화도 가능
// 두번째 인수(추천- 12~31): pbkdf2의 반복횟수와 유사, 숫자가 커질수록 비밀번호를 알아내기 어렵지만 암호화 시간도 오래걸림
await User.create({
email,
nick,
password: hash,
});
return res.redirect('/');
} catch (error) {
console.error(error);
return next(error);
}
});
// 로그인 라우터, /auth/login
router.post('/login', isNotLoggedIn, (req, res, next) =>{
passport.authenticate('local', (authError, user, info) => { // passport.authenticate('local') 미들웨어가 로컬로그인 전략(passport/localStrategy.js) 수행
// 미들웨어인데 라우터 미들웨어 안에 들어있음 - 미들웨어에 사용자 정의 기능을 추가하고 싶은 경우
if(authError){ // 로그인 전략(동작)이 실패한 경우 - authError 에 값이 존재
console.error(authError);
return next(authError);
}
if(!user){ // 2번째 매개변수 값(user)이 존재하지 않는 경우 - db에 계정이 X
return res.redirect(`/?loginError=${info.message}`);
}
// 2번째 매개변수 값(user)이 존재하는 경우 - passport가 req 객체에 login, logout 메서드를 추가함
return req.login(user, (loginError) => { // req.login은 passport.serializeUser를 호출 - req.login에 제공하는 user 객체가 serializeUser로 넘어가게 됨
if(loginError) {
console.error(loginError);
return next(loginError);
}
return res.redirect('/');
});
})(req, res, next); // 미들웨어 내의 미들웨어에는 (req, res, next)를 붙힘
});
// 로그아웃 라우터, , /auth/logout
router.get('/logout', isLoggedIn, (req, res) => {
req.logout(); // req.user 객체를 제거함
req.session.destroy(); // req.session 객체의 내용을 제거함 - 세션 정보를 지움
res.redirect('/'); // 메인 페이지로 돌아감
});
module.exports = router;
Git [sns3/passport/localStrategy.js
] - 로컬 로그인 전략 설정
// 로그인 전략
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy; // passport-local 모듈의 전략 생성자를 가져옴
const bcrypt = require('bcrypt');
const User = require('../models/user');
module.exports = () => {
passport.use(new LocalStrategy({ // LocalStrategy 생성자의 첫 번째 인수(객체): 전략에 관한 설정
usernameField: 'email', // req.body 내의 속성명(req.body.email)
passwordField: 'password',
}, async (email, password, done) => { // LocalStrategy 생성자의 두 번째 인수(함수): 실제 전략 수행
// 첫 번째 인수에서 넣어주었던 email, password가 여기의 매개변수가 됨
// done은 routes/auth.js의 passport.authenticate의 콜백 함수
try {
const exUser = await User.findOne({ where: { email }}); // db에 일치하는 Email이 있는지 확인
if(exUser){ // db에 일치하는 User가 있는 경우+
const result = await bcrypt.compare(password, exUser.password); // bcrypt: 암호화 모듈, password: req(입력)의 매개변수, exUser.password: db에 저장되어 있는 password
if(result){ // 비밀번호까지 일치하는 경우
done(null, exUser); // routes/auth.js의 passport.authenticate('local', ~~) 에서 ~~부분에 들어가는 값이 done으로 반환됨
} else { // 비밀번호 불일치
done(null, false, { message: '비밀번호가 일치하지 않습니다.'});
}
} else{ //
done(null, false, { message: '가입되지 않은 회원입니다.'});
}
} catch(error){
console.error(error);
done(error);
}
}));
};
카카오 로그인?: 로그인 인증 과정을 카카오에게 맡기는 것
+장점
+회원가입 절차가 따로 없어, 처음 로그인 시 회원가입 처리를 해야 함
Git [sns3/passport/kakaoStrategy.js
] - 카카오 로그인 전략 설정
// 카카오 로그인 전략
const passport = require('passport');
const KakaoStrategy = require('passport-kakao').Strategy; // passport-kakao 모듈로부터 Strategy 생성자를 불러와 전략 구현
const User = require('../models/user');
module.exports = () => {
passport.use(new KakaoStrategy({
clientID: process.env.KAKAO_ID, // clientID: 전략 구현을 위해 카카오에서 발급해주는 id, 노출 방지로 .env 파일에서 관리함
callbackURL: '/auth/kakao/callback', // callbackURL: 카카오로부터 인증 결과를 받을 라우터 주소
}, async (accessToken, refreshToken, profile, done) => {
console.log('kakao profile', profile);
try {
const exUser = await User.findOne({
where: {snsId: profile.id, provider: 'kakao'}, // snsId: 카카오 아이디와 같은 지?, provider: 카카오에서 로그인했는지?
}); // 기존에 카카오를 통해 회원가입한 사용자가 있는지 조회
if(exUser){ // 이미 User로 존재하는 경우(회원가입이 이미 되어있는 경우)
done(null, exUser); // 사용자 정보와 함께 done함수 호출
} else { // user가 존재하지 않는 경우 - 회원가입 진행
const newUser = await User.create({
email: profile._json && profile._json.kakao_account_email,
nick: profile.displayName,
snsId: profile.id,
provider: 'kakao',
});
done(null, newUser); // 새로운 유저와 함께 done 실행
}
} catch (error) {
console.error(error);
done(error);
}
}));
};
Git [sns3/routes/auth.js
] - 카카오 로그인 라우터 설정
...
// 카카오 로그인 라우터, /auth/kakao
router.get('/kakao', passport.authenticate('kakao')); // 카카오 api가 get으로 되어있어서 무조건 get으로 받아옴
// passport가 알아서 kakao 로그인 창으로 redirect 함
// 카카오 로그인 후 성공 여부 결과를 받음
router.get('/kakao/callback', passport.authenticate('kakao', { // 카카오 로그인 전략을 다시 수행함
// 로컬 로그인과 다른 점: passport.authenticate 메서드에 콜백 함수를 제공하지 않음
// 로그인 성공 시 내부적으로 req.login을 호출함 (내가 직접 호출할 필요X)
failureRedirect: '/', // failureRedirect 속성: 콜백 함수 대신 로그인에 실패했을 때 어디로 이동할지를 적음
}), (req, res) => { // 성공 시 어디로 이동할지 적는 미들웨어
res.redirect('/');
});
module.exports = router;
Git [sns3/routes/auth.js
] - 카카오 로그인 라우터 연결
...
const authRouter = require('./routes/auth');
...
app.use('/auth', authRouter);
...
위와같이 카카오 로그인 관련 코드를 추가해준 후, clientID: process.env.KAKAO_ID
를 위해 clientID
를 발급 받아야한다.
카카오 개발자(https://developers.kakao.com/) 회원가입
내 애플리케이션
메뉴에 들어가서 애플리케이션 추가하기
버튼 클릭 후 정보 입력
REST API 키를 복사해 .env 파일에 넣음
Git [sns3/.env
]
COOKIE_SECRET=serect
KAKAO_ID=여기
플랫폼
에서 Web의 Web 플랫폼 등록
클릭 후 http://localhost:포트번호
입력
제품설정
의 카카오 로그인
에서 활성화 설정
상태를 ON
으로 변경
활성화 설정 아래의 Redirect URI에 http://localhost:8001/auth/kakao/callback
입력
+/auth/kakao/callback은 kakaoStrategy.js의 callbackURL
과 일치해야 함
제품설정
의 동의항목
메뉴에서 로그인 동의항목을 작성
여기까지 모두 완료했으면 서버를 켜서 실행해보자!
http://127.0.0.1:8001/join - 회원가입하기
http://127.0.0.1:8001 - 로컬 계정으로 로그인한 상태
카카오 계정으로 로그인 시
+사진은 동의항목에서 닉네임, 카카오계정(이메일)을 선택한 경우임
http://127.0.0.1:8001 - 카카오 계정으로 로그인한 상태
Kakao 미연동이라고 떠서 로그를 확인해보았다.
로그 안에 미연동이라고 써있는데 카카오측에서 받은 값이 미연동이라는 것 같고,, 결과 자체는 잘 들어온 것 같다! -> 추후에 회원가입 때 이름을 넣어주면 된다
잘못된 정보 수정 및 피드백 환영합니다!!