데이터베이스 세팅
로그인 기능을 구현하기 위해 사용자 테이블이 필요하고, 게시글을 저장할 게시글 테이블도 필요하다.
해시태그를 사용하기 때문에 해시태그 테이블도 필요하다.
models 폴더 안에 정의해보자.
const Sequelize = require('sequelize');
module.exports = class User extends Sequelize.Model{
static init(sequelize){
return super.init({
email: {
type: Sequelize.STRING(40),
allowNull: true,
unique: true,
},
nick: {
type: Sequelize.STRING(15),
allowNull: false,
},
password: {
type: Sequelize.STRING(100),
allowNull: true,
},
provider: {
type: Sequelize.STRING(10),
allowNull: false,
defaultValue: 'local',
},
snsId: {
type: Sequelize.STRING(30),
allowNull: true,
},
},{
sequelize,
timestamps: true,
underscored: false,
modelName: 'User',
tableName: 'users',
paranoid: true,
charset: 'utf8',
collate: 'utf8_general_ci',
});
}
static assciate(db){
db.User.hasMany(db.Post);
db.User.belongsToMany(db.User, {
foreignKey: 'followingId',
as: 'Followers',
through: 'Follow',
});
db.User.belongsToMany(db.User, {
foreignKey: 'followerId',
as: 'Followings',
through: 'Follow',
});
}
};
models/user.js
const Sequelize = require('sequelize');
module.exports = class Post extends Sequelize.Model{
static init(sequelize){
return super.init({
content: {
type: Sequelize.STRING(140),
allowNull: false,
},
img: {
type: Sequelize.STRING(200),
allowNull: true,
},
},{
sequelize,
timestamps: true,
underscored: false,
modelName: 'Post',
tableName: 'posts',
paranoid: false,
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
});
}
static assciate(db){
db.Post.belongsTo(db.User);
db.Post.belongsToMany(db.Hashtag, {through: 'PostHashtag'});
}
};
models/post.js
const Sequelize = require('sequelize');
module.exports = class Hashtag extends Sequelize.Model{
static init(sequelize){
return super.init({
title: {
type: Sequelize.STRING(15),
allowNull: false,
unique: true,
},
},{
sequelize,
timestamps: true,
underscored: false,
modelName: 'Hashtag',
tableName: 'hashtags',
paranoid: false,
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
});
}
static assciate(db){
db.Hashtag.belongsToMany(db.Post, {through: 'PostHashtag'})
}
};
models/hashtag.js
'use strict';
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const User = require('./user');
const Post = require('./post');
const Hashtag = require('./hashtag');
const db = {};
const sequelize = new Sequelize(
config.database, config.username, config.password, config,
);
db.sequelize = sequelize;
db.User = User;
db.Post = Post;
db.Hashtag = Hashtag;
User.init(sequelize);
Post.init(sequelize);
Hashtag.init(sequelize);
User.assciate(db);
Post.assciate(db);
Hashtag.assciate(db);
module.exports = db;
models/index.js
User 모델과 Post 모델은 1:N 관계에 있으므로 hasMany로 연결된다.
팔로잉은 N:M 관계이다. 따라서 User 모델과 User모델 간에 N:M 관계가 있는 것이다.
through 옵션을 사용해 생성할 모델이름을 바꿔 줄 수 있다.
Post 모델과 Hashtag 모델 또한 N:M 관계이다. 마찬가지로 through 옵션을 사용하자.
이제 생성한 모들을 데이터베이스에 연결하자.
npx sequelize db:create
app.js에 다음과 같이 추가하자.
const {sequelize} = require('./models');
const app = express();
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);
});
Passport 모듈 - 로그인 구현
로그인을 구현하기 위해서는 세션과 쿠키 처리에 대해 복잡한 작업이 많이 요구된다. 따라서 검증된 모듈을 사용하는 것이 좋다. Passport를 사용해서 구현해보자.
우선 패키지를 설치하자.
npm i passport passport-local passport-kakao bcrypt
설치 후 Passport 모듈을 미리 app.js와 연결하자.
const passportConfig = require('./passport');
const app = express();
passportConfig(); //passport 설정
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.COOKE_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());
passport.initialize 미들웨어는 요청에 passport 설정을 심고, passport.session 미들웨어는 req.session 객체에 passport 정보를 저장한다.
passport를 만들어보자.
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();
}
passport/index.js
serializeUser는 로그인 시 실행된다. req.session 객체에 어떤 데이터를 저장할지 정하는 메서드이다.
매개변수로 user를 받고 나서, done 함수에 두 번째 인수로 user.id를 넘긴다. 이는 저장하고 싶은 데이터이다.
세션에 사용자 데이터를 저장하는데 세션에 사용자 정보를 모두 저장하면 세션의 용량이 커지고 일관성에 문제가 발생하여 id만 저장한다.
deserializeUser는 매 요청 시 실행된다.
serializeUser에서 세션에 저장했던 id를 받아 데이터 베이스에서 사용자를 조회한다.
로컬 로그인
다른 sns 서비스를 통해 로그인하지 않고 자체적으로 회원가입 후 로그인을 하는 로컬 로그인을 구현해보자.
우선 회원가입 요청을 받을 수 있게 라우터 먼저 구현하자.
exports.isLoggedIn = (req, res, next) => {
if(req.isAuthenicated()){
next();
}else{
res.status(403).send('로그인 필요');
}
};
exports.isNotLoggedIn = (req, res, next) => {
if(!req.isAuthenicated()){
next();
}else{
const message = encodeURIComponent('로그인한 상태입니다.');
res.redirect(`/?error=${message}`);
}
};
routes/middlewares.js
로그인 여부를 확인하는 메서드이다. 이를 다른 라우터에서 적용시켜 보자.
const express = require('express');
const {isLoggeIn,, 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', (req, res) => {
res.render('profile', {title: '내 정보 - NodeBird'});
})
router.get('/join', (req, res) => {
res.render('join', {title: '회원가입 - NodeBird'});
})
router.get('/', (req, res) => {
const twits = [];
res.render('main', {
title: 'NodeBird',
twits,
});
});
module.exports = router;
routes/page.js
middleware req.isAuthenticated()가 ture여야만 next가 호출되기 때문에 로그인 여부에 따라 실행하는 명령어가 달라진다.
회원가입, 로그인, 로그아웃 라우터를 작성해보자.
const express = require('express');
const passport = require('passport');
const bcrypt = require('bcrypt');
const {isLoggeIn, 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(err){
console.error(err);
return next(err);
}
});
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);
});
router.get('/logout', isLoggeIn, (req, res) => {
req.logout();
req.session.destroy();
res.redirect('/');
});
module.exports = router;
routes/auth.js
이제 passport에 로그인 전략을 구현하자.
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(err){
console.error(err);
done(err);
}
}));
};
passport/localStrategy.js
위와 같은 구조로 동작하는 것이다.
카카오 로그인
sns로그인의 특징은 회원가입 절차가 따로 없다는 것이다.
처음 로그인할 때 회원가입 처리를 하고, 두 번째 로그인할 때는 로그인 처리를 해야 한다.
우선 전략부터 짜보자.
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.displatName,
snsId: profile.id,
provider: 'kakao',
});
done(null, newUser);
}
}catch(err){
console.error(err);
done(err);
}
}));
};
passport/kakaoStrategy.js
kakao에서 발급하는 id는 노출되지 않아야 하므로. env파일에 넣을 것이다. callbackURL은 카카오로부터 인증 결과를 받을 라우터 주소이다. kakao를 통해 회원 가입한 사용자가 있는지 조회한 후, 있다면 로그인, 없다면 회원가입을 시킨다.
이제 라우터를 만들어보자.
router.get('/kakao', passport.authenticate('kakao'));
router.get('/kakao/callback', passport.authenticate('kakao',{
failureRedirect: '/',
}), (req, res) => {
res.redirect('/');
});
추가한 라우터들을 app.js에 연결하고 설정을 해보자.
kakao.Strategy.js에서 사용하는 clientID를 발급받기 위해 다음 링크로 이동하자.
REST API 키를 복사하여. env에 넣자.
그리고 web플랫폼을 등록하면 된다.
그리고 카카오 로그인을 활성화시킨다.
RedirectURI를 다음과 같이 수정하자.
마지막으로 다음과 같이 동의 항목을 설정한다.
이제 테스트해보자.
성공적으로 완료했다.