*위 내용을 정리하였음
1. Passport
- 회원 가입, 로그인시 세션, 쿠키 처리의 작업을 해주는 Passport모듈을 사용해서 구현할 것이다.
- 뿐만 아니라 SNS 로그인까지 구현해보자.
npm i passport passport-local passport-kakao bcrypt
- 관련 모듈들을 설치하고 app.js와 Passport 모듈을 연결한다.
- require('./passport')는 require('./passport/index.js')와 같다.
const passport = require('passport');
const passportConfig = require('./passport');
passportConfig(); // 패스포트 설정
app.use(passport.initialize());
app.use(passport.session());
- passport.initialize()는 요청(req) 객체에 passport 설정을 심는 미들웨어 이다.
- passport.session()은 req.session 객체에 passport 정보를 저장한다. 이전에 반드시 세션 객체를 생성하는 express-session 미들웨어를 먼저 연결해두어야 한다.
- 아래는 전체 app.js 코드이다.
const express = require('express');
const cookieParser = require('cookie-parser');
const morgan = require('morgan');
const path = require('path');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
const passport = require('passport');
dotenv.config();
const pageRouter = require('./routes/page');
const authRouter = require('./routes/auth');
const { sequelize } = require('./models');
const passportConfig = require('./passport');
const app = express();
passportConfig(); // 패스포트 설정
app.set('port', process.env.PORT || 8001);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
sequelize.sync({ force: false })
.then(() => {
console.log('데이터베이스 연결 성공');
})
.catch((err) => {
console.error(err);
});
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
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());
app.use(passport.session());
app.use('/', pageRouter);
app.use('/auth', authRouter);
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
});
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');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기중');
});
- 이제 passport 모듈을 살펴보자 (passport/index.js)
// passport/index.js
const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');
module.exports = () => {
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
User.findOne({ where: { id } })
.then(user => done(null, user))
.catch(err => done(err));
});
local();
kakao();
};
- serializeUser는 로그인 요청시 실행된다.
- req.session 객체에 어떤 데이터를 저장할지 정하는 메서드이다.
- done함수로 req.session에 매개변수로 받은 user객체의 user.id만을를 저장한다. 첫번째 인수는 에러 발생 여부이다.
- deserializeUser는 매 요청 시 실행된다.
- passport.session 미들웨어가 이 메서드를 요청한다.
- session에 저장된 id가 User테이블에 존재하는지 확인하고 확인이 되면 사용자 정보를 user객체로 가져온다. 그 user객체를 req.user에 저장한다.
- 정리하면 serializeUser는 사용자 정보 객체를 세션에 아이디만 저장하는 것이고
- deserializeUser는 세션에 저장된 아이디를 가지고 디비를 조회하여 사용자 정보 객체를 불러오는 것이다.
- 이렇게 하면 세션에 불필요하게 데이터를 담아두지 않아도 된다.
2. 로컬 로그인 구현
- 로그인시 동작을 전략이라고 표현한다.
- local()은 로컬 로그인 전략이다.
- 로그인을 했으면 회원가입 라우터에 접근할 수 있으면 안되고 로그인을 안했으면 로그아웃 라우터에 접근할 수 있으면 안된다.
- 또한 로그인을 안했으면 게시물 업로드 라우터에 접근하게 해서는 안된다.
- 따라서 로그인, 로그아웃 등의 라우터에 접근 권한을 제어할 수 있도록 하는 미들웨어가 필요하다.
- 이 미들웨어를 가지고 로그인 여부를 검사하여 사용자를 걸러내야 한다.
- passport가 req 객체에 알아서 추가해주는 req.isAuthenticated 메서드를 사용해보자.
- passport는 로그인 중이면 req.isAuthenticated에 true값을 붙인다.
// routes/middlewares.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}`);
}
};
- 이렇게 두 미들웨어를 만들었다. page라우터에서 어떻게 적용하는지 살펴보자.
// routes/page.js
const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const router = express.Router();
router.use((req, res, next) => {
res.locals.user = req.user;
res.locals.followerCount = 0;
res.locals.followingCount = 0;
res.locals.followerIdList = [];
next();
});
router.get('/profile', isLoggedIn, (req, res) => {
res.render('profile', { title: '내 정보 - NodeBird' });
});
router.get('/join', isNotLoggedIn, (req, res) => {
res.render('join', { title: '회원가입 - NodeBird' });
});
router.get('/', (req, res, next) => {
const twits = [];
res.render('main', {
title: 'NodeBird',
twits,
});
});
module.exports = router;
- 위 처럼 join을 원하는 경우는 isNotLoggedIn 미들웨어에서 next를 호출해주는 경우에만 진행이된다.
- profile을 가져오는 경우는 isLoggedIn이 next를 호출해주는 경우에만 다음 미들웨어가 수행된다.
- 회원가입, 로그인, 로그아웃 라우터를 만들어보자.
// routes/auth.js
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();
router.post('/join', isNotLoggedIn, async (req, res, next) => {
const { email, nick, 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.error(error);
return next(error);
}
});
router.post('/login', isNotLoggedIn, (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.error(loginError);
return next(loginError);
}
return res.redirect('/');
});
})(req, res, next); // 미들웨어 내의 미들웨어에는 (req, res, next)를 붙입니다.
});
router.get('/logout', isLoggedIn, (req, res) => {
req.logout();
req.session.destroy();
res.redirect('/');
});
router.get('/kakao', passport.authenticate('kakao'));
router.get('/kakao/callback', passport.authenticate('kakao', {
failureRedirect: '/',
}), (req, res) => {
res.redirect('/');
});
module.exports = router;
- join은 회원가입 라우터이다. 로그인이 안되어 있을때만 다음 미들웨어가 수행된다. 이메일, 닉네임, 비밀번호를 요청객체의 body에서 가져와서 이미 가입되어있는지 확인한다. 이미 가입되어있다면 에러를 쿼리스트링으로 붙여서 회원가입 화면으로 돌려보낸다. 그게 아니라면 새로운 사용자 정보를 db에 저장하고 홈 화면으로 안내한다.
- login은 로그인 라우터이다. 로그인이 안되어 있을때만 접근가능하다. 미들웨어 안에 또 미들웨어가 들어가 있다. 맨뒤에 (req, res, next)를 사용해서 매개변수를 전달한다. 'local.js'이라는 전략을 수행하는데 authError에 값이 반환되면 로그인은 실패한 것이다. 만약 그렇지 않고 user가 존재하면 req.login을 수행한다. req에 login, logout은 Passport객체가 추가한다. req.login 메서드가passport.serializeUser 호출한다. 여기서 req.login이 전달받은 user 객체를 serializeUser에게 보내준다.
- logout라우터는 req.logout으로 req.user객체를 제거한다. req.session.destroy()로 req.session 내용을 제거한다. 그리고 메인 페이지로 돌려보낸다.
// passport/localStrategy.js
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const User = require('../models/user');
module.exports = () => {
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
}, 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);
} else {
done(null, false, { message: '비밀번호가 일치하지 않습니다.' });
}
} else {
done(null, false, { message: '가입되지 않은 회원입니다.' });
}
} catch (error) {
console.error(error);
done(error);
}
}));
};
- passport-local 모듈에서 Strategy 생성자를 불러와서 그 안에 전략을 구현한다.
- 첫번째 인수는 전략에 관한 설정을 하는 곳이다. 사용자 이름은 req.body.email에 있고 비밀번호는 req.body.password에 있기 때문에 위처럼 작성한다.
- 두번째 인수는 실제 전략을 수행하는 곳이다. done의 첫번째 인수는 서버쪽에 문제가 생겼을때 에러이다. 따라서 비밀번호가 없거나 가입하지 않은 경우는 서버쪽의 문제가 아니기 때문에 첫번째 인수는 null이다.
- 두번째는 로그인 성공시 사용자 정보를 건네준다. 로그인 실패시 false이다.
- 세번째는 사용자 정의 에러가 발생했을때 사용한다. (비밀번호 오류, 가입한적 없음 등)
- 이렇게 전략이 done()을 호출하면서 수행이 완료되면 passport.authenticate의 콜백 함수에서 나머지 로직이 수행된다.
3. 카카오 로그인 구현
// passport/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',
}, async (accessToken, refreshToken, 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 && profile._json.kakao_account_email,
nick: profile.displayName,
snsId: profile.id,
provider: 'kakao',
});
done(null, newUser);
}
} catch (error) {
console.error(error);
done(error);
}
}));
};
- passport-kakao 모듈로부터 strategy생성자를 불러와서 전략을 구현한다.
- 카카오에서 발급해주는 아이디와 카카오로부터 받은 인증 결과를 받을 라우터 주소를 설정한다.
- 카카오로 로그인한 사용자가 있는지 찾고나서 없다면 회원가입을 진행한다. 이때 사용할 정보는 카카오가 보내준 profile객체를 사용하면 된다.
- 이제 카카오 로그인 라우터를 만들어보자
router.get('/kakao', passport.authenticate('kakao'));
router.get('/kakao/callback', passport.authenticate('kakao', {
failureRedirect: '/',
}), (req, res) => {
res.redirect('/');
});
- GET /auth/kakao로 접근하면 카카오 로그인이 시작된다. /auth/kakao 링크는 layout.html 카카오 버튼에 붙어있다.
- 로그인 여부 최종 결과를 GET /kakao/callback으로 받는다.
- 카카오가 내부적으로 req.login을 호출하기 때문에 그냥 바로 어디로 이동할지 안내만 해주면 된다.
- 이렇게 완성된 auth라우터를 app.js에 연결해준다.
- 카카오 개발자 계정을 생성하고 애플리케이션을 추가한다.
- REST API 키를 .env에 저장해서 사용하면 된다.
- 플랫폼 등록 http://localhost:8001 등
- 제품설정 > 카카오 로그인 > 활성화 설정 > Redirect URI 에 http://localhost:8001/auth/kakao/callback이라고 고쳐준다.
- 그 다음 제품설정 > 카카오 로그인> 동의항목 > 카카오 계정으로 정보수집 후 제공 체크
- 위의 과정을 다 거치면 GET auth/kakao 라우터로 요청을 보낼 수 있다.
4. 최종 정리
- 로그인 시도
1. 라우터를 통해 로그인 요청이 들어온다.
2. 라우터에서 passport.authenticate 메서드 호출
3. 로그인 전략 수행
4. 로그인 성공시 사용자 정보 객체와 함께 req.login 호출
5. req.login 메서드가 passport.serializeUser 호출
6. req.session에 사용자 아이디만 저장
7. 로그인 완료
- 로그인 이후
1. 요청이 들어옴
2. 라우터에 요청이 도달하기 전에 passport.session 미들웨어가 passport.deserializeUser 메서드 호출
3. req.session에 저장된 아이디를 가지고 데이터베이스에서 사용자 조회
4. 조회한 사용자 정보 req.user에 저장
5. 이후 라우터에서 req.user를 사용할 수 있게 된다.
'ComputerScience > NodeJs' 카테고리의 다른 글
node - 10.1 웹 API 서버 만들기 (프로젝트 구조, jwt토큰) (0) | 2022.02.08 |
---|---|
node - 9.4 익스프레스로 SNS서비스 만들기 (이미지 업로드, 팔로잉, 해시태그 구현) (0) | 2022.02.07 |
node - 9.2 익스프레스로 SNS 서비스 만들기(SQL DB) (0) | 2022.02.05 |
node - 9.1 익스프레스로 SNS 서비스 만들기(프로젝트 구조) (0) | 2022.02.05 |
node - 8 몽고디비 (0) | 2022.02.03 |