이번 시간에는 Router와 passport에 대해서 정리를 하려고 한다.
Node.js에서 간단하게 인증을 할 수 있게 도와주는 Node.js용 미들웨어입니다.
일반적인 로그인뿐만 아니라 카카오톡, 구글 등 SNS 로그인 인증도 가능합니다.
아래는 passport의 인증 전략에 대한 코드이다.
일단 전략도 중요하지만, 로그인을 시도하면 Router에서 거친 뒤 passport로
와서 인증을 거치고 나서 다시 Router에서 진행되는 흐름이 있다.
이 부분까지 설명하기에는 매우 복잡하기 때문에 생략하도록 하겠다.
const passport = require('passport');
const local = require('./local');
const { User } = require('../models');
module.exports = () => {
// 로그인 성공 시 쿠키와 id만 들고있는다.
passport.serializeUser((user, done) => {
// null - 서버 에러
// user.id - 성공해서 user의 id를 가져온다.
done(null, user.id)
});
// 서버에서 유저에 대한 모든 정보를 갖고 있게되면, 서버 과부화가 생기게된다.
// 그래서 서버는 id만 갖고있다가, 페이지 이동 시 필요한 유저 정보는 DB에서 찾아서 가져온다.
// 그게 deserializeUser 역할이다.
passport.deserializeUser( async (id, done) => { // DB에서 정보를 찾으면 req.user로 넣어준다.
try {
const user = await User.findOne({ where: { id }});
done(null, user); // done 시 callback
} catch(error) {
console.error(error);
done(error);
}
});
local();
};
const passport = require('passport');
const bcrypt = require('bcrypt');
// Strategy -> LocalStrategy로 이름 변경
const { Strategy: LocalStrategy } = require('passport-local');
const { User } = require('../models');
// local 로그인 전략
// done : 첫번째인자 - 서버 에러 / 두번째인자 - 응답 실패,성공 유무 / 세번째인자 - 실패 시 나타낼 문구(reason: XXXX);
module.exports = () => {
passport.use(new LocalStrategy({
usernameField: 'email', // req.body.email 라고 명시적으로 알려줌 (정확한 명을 넣어야한다.)
passwordField: 'password'
}, async (email, password, done) => { // 함수가 추가된다.
try {
const user = await User.findOne({ // 로그인 시도에서 이메일 있는 조건으로 찾아보기.
where: { email }
});
if (!user) {
// passport에서는 res로 응답이 아닌, 우선 done으로 처리를 한다.
return done(null, false, { reason: '이메일이 일치하지 않습니다.'});
}
// 비밀번호 비교 체크
// 첫번째 인자 password : 사용자가 입력한 비밀번호
// 두번째 인자 user.password : 실제 DB에 있는 비밀번호
const result = await bcrypt.compare(password, user.password);
if (result) { // 비밀번호 일치할 경우
return done(null, user); // 두번째 user는 성공의 의미
}
// 비밀번호 일치하지 않을 경우
return done(null, false, { reason: '비밀번호가 일치하지 않습니다.' });
} catch (err) {
console.error(err);
return done(err); // done의 첫번째 인자는 서버 에러시 넣는다.
}
}));
};
클라이언트에서 API요청을 할 경우 해당하는 주소 및 메소드에 맞게 Router를 만들고,
작업 수행에 필요한 코드를 작성하면 된다.
const express = require('express'); // express 가져오기
const { User } = require('../models'); // User 가져오기
const bcrypt = require('bcrypt'); // 비밀번로 해쉬화에 필요한 라이브러리
const passport = require('passport');
const router = express.Router(); // express에서 제공하는 Router 미들웨어
// GET /
router.get('/', (req, res) => {
res.send('hello~ express');
});
위에서 예시로 만든 router.get
은 클라이언트에서 get
을 사용해서 서버로 요청이 오면 실행이 된다. /
는 기본 주소를 뜻한다. 여기서 app.js
를 살펴봐야 한다.
app.js
// app.js에 만든 코드
const pageRouter = require('../routes/page');
app.use('/', pageRouter);
위 처럼 설정은 해놓은 상태였다. 여기서 pageRouter
는 /
기본적인 주소를 갖게 된다.
즉, http://localhost:3000/
을 뜻한다.
그래서 위 코드를 실행해야할 경우에 어떻게 요청을 해야할까?
const 함수명 = createAsyncThunk('변수명', async () => {
const response = await axios.get('http://localhost:3000/');
return response;
});
이런식으로 get
에는 /
하나만 추가 됐기 때문에 응답으로 hello~ express
를 받게된다.
그럼 작성했던 내용을 쭈욱 정리하겠다.
// 회원가입
// POST /signup
router.post('/signup', isNotLoggedIn, async (req, res, next) => { // POST /signup/
try {
const exEmail = await User.findOne({ // 이메일 검사
where: { // where : DB에서 조건을 건다.
email: req.body.email,
}
});
const exNickname = await User.findOne({ // 이메일 검사
where: {
nick: req.body.nickname,
}
});
if (exEmail) { // 이메일 검사 후 이메일이 기존에 있다면?
// return으로 res(응답)을 한번만 보내도록 한다. 응답 후 router 종료된다.
return res.status(403).send('이미 사용중인 이메일입니다.');
}
if (exNickname) { // 이메일 검사 후 닉네임이 기존에 있다면?
return res.status(403).send('이미 사용중인 닉네임입니다.');
}
// bcrypt - 비밀번호 해쉬화하기
const hashedPassword = await bcrypt.hash(req.body.password, 12);
// User 테이블에 신규 유저 생성하기
await User.create({
nick: req.body.nickname,
email : req.body.email,
password: hashedPassword,
});
// 요청에 대한 성공으로 status(201) : 생성이 됐다는 의미 (기재하는게 좋다.)
res.status(201).send('create User!');
} catch(err) {
console.error(err);
next(err); // status(500) - 서버에러
}
});
// 로그인
// 미들웨어 확장법 (req, res, next를 사용하기 위해서)
// passport index.js에서 전달되는 done의 세가지 인자를 받는다.
router.post('/login', isNotLoggedIn, (req, res, next) => {
passport.authenticate('local', (err, user, info) => { // 여기서 local를 실행한다.
if (err) { // 서버 에러
console.error(err);
return next(err);
}
if (info) { // 클라이언트 에러 (비밀번호가 틀렸거나, 계정이 없거나), info.reason에 에러 내용이 있음.
res.status(403).send(info.reason);
}
// 아래는 마지막으로 에러를 검사하는 코드다.
// 성공하면 passport의 serialize가 실행된다.
return req.login(user, async (loginErr) => {
if (loginErr) {
console.error(loginErr);
return next(loginErr);
}
// 비밀번호를 제외한 모든 정보 가져오기
const fullUserWithoutPassword = await User.findOne({
where: { id: user.id },
attributes: {
exclude: ['password'], // exclude: 제외한 나머지 정보 가져오기
},
});
// 비밀번호를 제외한 유저 정보를 json으로 응답
return res.status(200).json(fullUserWithoutPassword);
});
})(req, res, next); // 미들웨어 확장에서는 끝에 항상 넣어줘야한다.
});
// 로그아웃
// POST /logout/
router.post('/logout', isLoggedIn, (req, res) => {
req.logOut();
req.session.destroy();
res.send('로그아웃');
});
module.exports = router;
엄청 복잡해 보이지만 사실 좀 복잡하다. 하지만 흐름에 대해서 정리를 한다면,
크케 복잡하진 않다. 이렇게만 하면 기본적인 회원가입과 로그인은 가능하다.
하지만 로그인을 하고 다른 페이지 이동하거나 새로고침을 해도 로그인을 유지하고자 하지 않았나?
이 부분은 이제 프론트에서도 SSR을 설정해줘야하고, 백에서도 지속적인 로그인을 검사해야하는
route를 추가적으로 작성해야한다.
이 부분은 다음 시간에 진행하도록 하겠다.
오늘은 SNS 로그인 작업을 해야하기 때문에!!