이번에 다룰 내용은 로그인(로컬, 카카오)에 관한 내용인데, 개인적으로 sns를 만들면서 가장 코드를 이해하기 힘들었다. 따라서 이번 포스팅에서는 로그인만을 다루며 코드를 이해해보겠다!
사실 이 부분은 이전 포스팅에서도 한 번 다뤘었는데, 어떤 흐름으로 흘러가는지 정확히 하기 위해 이번 포스팅을 작성했다.
Github: https://github.com/delay-100/study-node/tree/main/ch9/sns5
- 기본 module 세팅
- 전체 app.js 세팅
- 메인 페이지 이해하기 +layout.html 설명
- 회원가입 기능 이해하기
- 로컬 로그인 기능 이해하기
- kakao 로그인 기능 이해하기
Model은 SNS 만들기 -2에서 다뤘으므로 흐름 정리 포스팅에서는 다루지 않겠다. (model git 주소)
메인페이지 기본 실행 화면 (로컬 로그인 예시 작성 상태)
메인페이지 로컬 로그인 후 실행 화면
해당 위치
- url 주소: http://localhost:8001/, (http://127.0.0.1:8001/auth/login, http://127.0.0.1:8001/auth/logout)
- api 주소: sns5/routes/
auth.js
, passport/localStrategy.js
, passport/index.js
- html: sns5/views/
main.html
layout.html 설명 中 로컬 로그인 버튼 내용
2. 로컬 로그인 폼
->로그인
버튼 클릭 시type="submit"
에 의해form
태그의action="/auth/login"
실행 +method="post"
-> routes/auth.js
의 post("/login") 실행
-> http://127.0.0.1:8001/auth/login
Git [routes/auth.js
] 中 /login
const express = require('express');
const passport = require('passport');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const router = express.Router();
// 로컬 로그인 라우터, /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)를 붙힘
});
=5-1> 1. isNotLoggedIn을 routes/middlewares
에서 불러옴, 2. passport.authenticate('local',
미들웨어가 로컬 로그인 전략인 passport/localStrategy.js
실행, 3. 5-2 실행 결과
를 (authError, user, info)
의 인자로 대입 후 실행
5-1.1. isNotLoggedIn을 routes/middlewares
에서 불러옴, 설명 생략 => 4-1.1.
참고
5-1.2. passport.authenticate('local',
미들웨어가 로컬 로그인 전략인 passport/localStrategy.js
실행
Git [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', // body-parser에 의해 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);
}
}));
};
=5-2> 5-2.1. db에 일치하는 User 존재, 비밀번호 일치(로그인 가능), 5-2.2. db에 일치하는 User가 존재, 비밀번호 불일치(로그인 불가/에러), 5-2.3. db에 일치하는 User가 미존재(로그인 불가/에러), 5-2.4. 에러가 난 경우
5-2.1. db에 일치하는 User 존재, 비밀번호 일치(로그인 가능)
->done(null, exUser);
5-2.2. db에 일치하는 User가 존재, 비밀번호 불일치(로그인 불가/에러)
->done(null, false, { message: '비밀번호가 일치하지 않습니다.'});
5-2.3. db에 일치하는 User가 미존재(로그인 불가/에러)
->done(null, false, { message: '가입되지 않은 회원입니다.'});
5-2.4. 에러가 난 경우
->done(error);
5-1.3. 5-2 실행 결과
를 (authError, user, info)
의 인자로 대입 후 실행
5-1.3.1.
5-2.4.
가 인자로 온 경우
-> authError = error
->console.error(authError);
(+console.error(error) 설명)
->return next(authError);
-> app.js의 아래쪽에 선언되어 있는 err 매개변수가 있는 곳으로 이동Git [
app.js
] 中 에러 관련 함수// 에러 관련 함수 app.use((err, req, res, next) => { res.locals.message = err.message; res.locals.error = process.env.NODE_ENV !== 'production' ? err : {}; // 개발용 res.status(err.status || 500); res.render('error'); });
5-1.3.2.
5-2.2.
,5-2.3.
가 인자로 온 경우
-> authError = null, user = false, info = { message: ~~ }
-> db에 User가 없음
->return res.redirect(`/?loginError=${info.message}`);
-> redirect 내의 주소가/
이므로 routes/page.js
의 get('/') 부분이 실행된 후, views/main.html
이 실행됨 (3-1 참고)
->/?loginError=${info.message}
도 주소에 들어감
Git [views/main.html
] 中 script 부분
<script>
window.onload = () =>{
if (new URL(location.href).searchParams.get('loginError')){
alert(new URL(location.href).searchParams.get('loginError'));
}
};
</script>
=5-3> URL에서 loginError
이 있는지 확인 후 있으면 경고창(alert) 표시
5-1.3.3.
5-2.1.
가 인자로 온 경우
-> authError = null, user = exUser
->req.login(user, (loginError) => {~~}
부분 실행
+req.login(user,
은 passport가 req에 login을 추가해서 실행 가능한 것!
->req.login
은 passport/index.js
의serializeUser
실행시킴
->5-4.1.
부분이 실행되고 return res.redirect('/');부분이 실행됨
-> res.redirect('/')에 의해 routes/page.js의 get /로 이동
-> http://127.0.0.1:8001 로 이동됨
Git [passport/index.js
]
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 미들웨어가 이 메소드를 호출
// 라우터가 실행되기 전 먼저 실행됨! -> 모든 요청이 들어올 때 매번 사용자의 정보를 조회함(db에 큰 부담 -> 메모리에 캐싱 또는 레디스 같은 db 사용)
passport.deserializeUser((id, done) => { // deserializeUser: 매 요청 시 실행, id: serializerUser의 done으로 id 인자를 받음
User.findOne({
where:{id}, // db에 해당 id가 있는지 확인
include: [{
model: User,
attributes: ['id', 'nick'], // 속성을 id와 nick으로 지정함으로서, 실수로 비밀번호를 조회하는 것을 방지
as: 'Followers',
}, {
model: User,
attributes: ['id', 'nick'],
as: 'Followings',
}],
})
.then(user => done(null, user)) // req(요청).user에 저장 -> 앞으로 req.user을 통해 로그인한 사용자의 정보를 가져올 수 있음
.catch(err => done(err));
});
local();
kakao();
};
=5-4> 1. done(null, user.id); 실행 후 반환 2. deserializeUser 실행, 3. passport/index.js
의 local();
로 인해 5-1.2.
실행해 전략 확인
app.js
const passport = require('passport');
const passportConfig = require('./passport'); // require('./passport/index.js')와 같음
passportConfig(); // 패스포트 설정, 한 번 실행해두면 ()에 있는 deserializeUser 계속 실행 - passport/index.js
// passport 사용 - req.session 객체는 express-session에서 생성하므로 express-session 뒤에 작성해야함
app.use(passport.initialize()); // 요청(req 객체)에 passport 설정을 심음
app.use(passport.session()); // req.session 객체에 passport 정보를 저장(요청으로 들어온 세션 값을 서버에 저장한 후, passport 모듈과 연결)
카카오 로그인 화면 (메인 페이지에서 카카오톡
클릭 시)
메인페이지 카카오 로그인 후 실행 화면
해당 위치
- url 주소: http://localhost:8001/, (http://127.0.0.1:8001/auth/logout)
- api 주소: sns5/routes/
auth.js
, passport/kakaoStrategy.js
, passport/index.js
- html: sns5/views/
main.html
layout.html 설명 中 카카오 로그인 버튼 내용
3. 카카오톡 버튼
->카카오톡
버튼 클릭 시href="/auth/kakao"
에 의해 http://127.0.0.1:8001/auth/kakao 에 get 요청
-> routes/auth.js
의 get("/kakao") 실행
Git [routes/auth.js
] 中 /auth/kakao
// 카카오 로그인 라우터, /auth/kakao
router.get('/kakao', passport.authenticate('kakao')); // 카카오 api가 get으로 되어있어서 무조건 get으로 받아옴
// passport가 알아서 kakao 로그인 창으로 redirect 함
=6-1> passport가 kakao에 로그인 인증을 요청함 (카카오 로그인 화면으로 이동)
Git [routes/auth.js
] 中 /auth/kakao/callback
// 카카오 로그인 후 성공 여부 결과를 받음
router.get('/kakao/callback', passport.authenticate('kakao', { // 카카오 로그인 전략을 다시 수행함
// 로컬 로그인과 다른 점: passport.authenticate 메서드에 콜백 함수를 제공하지 않음
// 로그인 성공 시 내부적으로 req.login을 호출함 (내가 직접 호출할 필요X)
failureRedirect: '/', // failureRedirect 속성: 콜백 함수 대신 로그인에 실패했을 때 어디로 이동할지를 적음
}), (req, res) => { // 성공 시 어디로 이동할지 적는 미들웨어
res.redirect('/');
});
=6-2> kakao에서 받은 인증이 유효하거나 유효하지 않아도 get /
(routes/page.js
) - http://127.0.0.1:8001/ 로 이동
+로그인 성공 시 내부적으로 req.login
(5-1.3.3, 5-4 참고)을 실행시킨다.
여기서는 5-4.3.에서 local();이 아닌 kakao(); 에 영향을 받는다. 따라서 passport/kakaoStrategy.js
가 실행된다.
Git [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);
}
}));
};
=6-3> 1. 회원가입이 이미 되어있는 경우, 2. 새로운 User로 생성 가능한 경우, 3. 에러가 난 경우
6-3.1. 회원가입이 이미 되어있는 경우
->done(null, exUser);
-> 해당 유저를 반환
6-3.2. 새로운 User로 생성 가능한 경우
->done(null, newUser);
-> db에 새로운 User를 생성 후 새로운 유저와 함께 반환
6-3.3. 에러가 난 경우
->done(error);
-> console.error(error) 실행
+console.error(error) 설명
+ clientID: process.env.KAKAO_ID
kakao에 client ID 발급 받는 방법은 이 포스팅에 설명되어 있다.
다음 포스팅에서는 7. 글쓰기/이미지 업로드 이해하기, 8. 팔로우-팔로잉 기능 이해하기, 9. 해시태그 검색 기능 이해하기를 다뤄보겠다.
잘못된 정보 수정 및 피드백 환영합니다!!