elice/WIL
[week_07] Express.js์ MongoDB๋ก ์น์๋น์ค ๋ง๋ค๊ธฐ (2) | Hash, Session, ๋ก๊ทธ์ธ/ํ์๊ฐ์
๊ฑด๋ง๋
2022. 2. 23. 19:23
1. Hash
- ๋น๋ฐ๋ฒํธ๋ฅผ ์ ์ฅํ ๋ ์ฌ์ฉ
- ๋ฌธ์์ด์ ๋๋๋ฆด ์ ์๋ ๋ฐฉ์์ผ๋ก ์ํธํํ๋ ๋ฐฉ๋ฒ
- hash ์ถ๋ ฅ๊ฐ์ ์ด์ฉํด์ ์ฌ์ฉ์์ ๋น๋ฐ๋ฒํธ๋ฅผ ์์๋ผ ์ ์๋ค.
๋น๋ฐ๋ฒํธ์ Hash ๊ฐ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ๊ณ , ๋ก๊ทธ์ธ ์ ์ ๋ฌ๋ ๋น๋ฐ๋ฒํธ๋ฅผ Hashํ์ฌ ์ ์ฅ๋ ๊ฐ๊ณผ ๋น๊ตํด์ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ
1-1. Node.js์์์ Hash ์ฌ์ฉ๋ฒ
- crypto ๋ชจ๋์ ์ฌ์ฉํด์ hash ๊ฐ์ ์ป์ ์ ์๋ค.
const hash = crypto.createHash('sha1'); // ํน์ ํด์ฌ ์๊ณ ๋ฆฌ์ฆ ์ค์
hash.update(password); // ์
๋ ฅ๋ฐ์ ํ๋ฌธ put
hash.digest('hex'); // 16์ง์ ๋ฌธ์์ด๋ก ์ถ๋ ฅ
- ๊ฐ๋จํ๊ฒ sha1 ์๊ณ ๋ฆฌ์ฆ์ ์ฌ์ฉํ๊ฑฐ๋, ๋ณด๋ค ๊ฐ๋ ฅํ sha224, sha256 ๋ฑ์ ์๊ณ ๋ฆฌ์ฆ ์ฌ์ฉ ๊ฐ๋ฅ
2. ํ์๊ฐ์ ์์ฒญ ์ฒ๋ฆฌํ๊ธฐ
router.post(... => {
const { email, name, password } = req.body;
const pwHash = getHash(password);
const exist = await User.findOne({ email, });
if (exist) {
throw new Error('์ด๋ฏธ ๊ฐ์
๋ ๋ฉ์ผ์
๋๋ค.');
}
await User.create({
email,
name,
password: pwHash,
});
res.redirect('/');
});
- ๋น๋ฐ๋ฒํธ๋ฅผ hash๊ฐ์ผ๋ก ์ ์ฅ
- ์ด๋ฏธ ์กด์ฌํ๋ ํ์์ธ์ง ์ฒดํฌ
- ๊ฐ์ ์ฑ๊ณต ์ ๋ฉ์ธํ๋ฉด์ผ๋ก redirect
3. Passport.js
- ์ ์ ์ธ์ ๊ด๋ฆฌ ๋ฐ ๋ค์ํ ๋ก๊ทธ์ธ ๋ฐฉ์ ์ถ๊ฐ ๊ฐ๋ฅ
3-1. passport-local
- passport๋ ๋ค์ํ ๋ก๊ทธ์ธ ๋ฐฉ์ ๊ตฌํ์ ์ํด strategy๋ผ๋ ์ธํฐํ์ด์ค ์ ๊ณต
- strategy ์ธํฐํ์ด์ค์ ๋ง๊ฒ ์ค๊ณ๋ ๋ค์ํ ๊ตฌํ์ฒด๋ค์ด ์์ (facebook, google, ...)
- passport-local์ username, password๋ฅผ ์ฌ์ฉํ๋ ๋ก๊ทธ์ธ์ ๊ตฌํ์ฒด
1) ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ: passport-local strategy
/* passport-local */
const config = { usernameField: 'email', passwordField: 'password' };
// ์์ด๋ ํจ์ค์๋ ํ๋ ์ค์ ํ์
const local = new LocalStrategy(config, ..., async (email, password, done) => {
try {
const user = await User.findOne({ email });
if (!user) { // email๋ก ํ์์ด ์ฐพ์์ง์ง ์์ ๊ฒฝ์ฐ
throw new Error('ํ์์ ์ฐพ์ ์ ์์ต๋๋ค.');
}
if (user.password !== password) { // ํจ์ค์๋์ ํด์ฌ๊ฐ์ ๋ง๋ค์ด์ ์ฌ์ฉํ ๊ฒ!
throw new Error('๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.');
}
// ์ธ์
์ ์ ์ฅ๋๋ ์ ์ ์ ๋ณด์ ์ต์ํ
done (null, {
shortId: user.shortId,
email: user.email,
name: user.name,
});
} catch(err) {
done(err, null);
}
});
/* passport use */
const local = require('./strategies/local');
passport.use(local);
- ์์ฑํ strategy๋ฅผ passport.use๋ฅผ ์ด์ฉํด ์ฌ์ฉํ๋๋ก ์ ์ธํด์ผ ํ๋ค.
- passport.use๋ฅผ ์ด์ฉํด strategy ๋ฅผ ์ฌ์ฉํ๋๋ก ์ ์ธํ ํ, passport.authenticate๋ฅผ ์ฌ์ฉํด ํด๋น strategy๋ฅผ ์ด์ฉํด ์์ฒญ์ ์ฒ๋ฆฌํ ์ ์๋ค.
2) ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ: Passport.js๋ก post ์์ฒญ ์ฒ๋ฆฌํ๊ธฐ
/* routes/auth.js */
router.post('/', passport.authenticate('local');
/* app.js */
const session = require('express-session');
app.use(session({
secret: 'secret',
resave: false,
saveUninitialized: true
}));
app.use(passport.initialize());
app.use(passport.session());
app.use('/auth', authRouter);
- passport.authenticate ํจ์๋ฅผ http ๋ผ์ฐํ ์ ์ฐ๊ฒฐํ๋ฉด passport๊ฐ ์๋์ผ๋ก ํด๋นํ๋ strategy๋ฅผ ์ฌ์ฉํ๋ request handler๋ฅผ ์์ฑ
- express-session๊ณผ passport.session์ ์ฌ์ฉํ๋ฉด passport๊ฐ ๋ก๊ทธ์ธ ์ ์ ์ ์ ๋ณด๋ฅผ ์ธ์ ์ ์ ์ฅํ๊ณ ๊ฐ์ ธ์ค๋ ๋์์ ์๋์ผ๋ก ์ํํด์ค๋ค.
3) ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ: session ์ ์ ํ์ฉํ๊ธฐ
passport.serializeUser((user, callback) => {
callback(null, user);
});
passport.deserializeUser((obj, callback) => {
callback(null, obj);
});
- ์ธ์ ์ user ์ ๋ณด๋ฅผ ๋ณํํ์ฌ ์ ์ฅํ๊ณ ๊ฐ์ ธ์ค๋ ๊ธฐ๋ฅ์ ์ ๊ณต
4) ๋ก๊ทธ์์
router.get('/logout', ... {
req.logout();
res.redirect('/');
});
5) ๋ก๊ทธ์ธ ํ์ธ ๋ฏธ๋ค์จ์ด
function loginRequired(req, res, next) {
if(!req.user) {
res.redirect('/');
return;
}
next();
}
app.use('/posts', loginRequired, postsRouter);
- ์ด๋ค ํ์ด์ง ํน์ ๊ธฐ๋ฅ์์ ๋ก๊ทธ์ธ์ ํ์๋ก ์ค์ ํ๊ณ ์ถ์ ๊ฒฝ์ฐ, ๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํ์ฌ ์ฒดํฌ
4. Session
- ์น ์๋ฒ๊ฐ ํด๋ผ์ด์ธํธ ์ ๋ณด๋ฅผ ํด๋ผ์ด์ธํธ๋ณ๋ก ๊ตฌ๋ถํ์ฌ ์๋ฒ์ ์ ์ฅํ๊ณ , ํด๋ผ์ด์ธํธ ์์ฒญ ์ Session ID๋ฅผ ์ฌ์ฉํ์ฌ ํด๋ผ์ด์ธํธ์ ์ ๋ณด๋ฅผ ๋ค์ ํ์ธํ๋ ๊ธฐ์
- ํด๋ผ์ด์ธํธ๊ฐ ์ ๋ณด๋ฅผ ์ ์ฅํ๊ณ , ์์ฒญ ์ ์ ๋ณด๋ฅผ ๋ณด๋ด๋ Cookie์ ๋์กฐ๋จ
์๋ฒ๋ ์ธ์ ์ ์์ฑ โก Session ID๋ฅผ ํด๋ผ์ด์ธํธ์ ์ ๋ฌ โก ํด๋ผ์ด์ธํธ๋ ์์ฒญ ์ Session ID๋ฅผ ํจ๊ป ์์ฒญ์ ๋ด์์ ์ ์ก โก ์๋ฒ๊ฐ ์ ๋ฌ๋ฐ์ Session ID๋ก ํด๋นํ๋ ์ธ์ ์ ์ฐพ์ ํด๋ผ์ด์ธํธ ์ ๋ณด ํ์ธ
- express-session ํจํค์ง๋ฅผ ์ฌ์ฉํ์ฌ ์๋์ผ๋ก session ๋์ ๊ตฌํ ๊ฐ๋ฅ
4-1. Session Store
- express-session ํจํค์ง๋ session์ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅ
- ํ์ฌ ๊ตฌํ๋ ์ดํ๋ฆฌ์ผ์ด์ ์ ์ข ๋ฃ ํ ๋ค์ ์คํํ๋ฉด, ๋ชจ๋ ์ ์ ์ ๋ก๊ทธ์ธ์ด ํด์ ๋จ
- ํน์ ์๋ฒ๊ฐ ์ฌ๋ฌ ๋๊ฐ ์์ ๊ฒฝ์ฐ, ์๋ฒ ๊ฐ ์ธ์ ์ ๋ณด๋ฅผ ๊ณต์ ํ ์ ์์
- ๊ทธ๋์ Session Store๋ฅผ ๊ตฌํํ์ฌ Express Application๋ค์ ์ฐ๊ฒฐํ๋ ๋ฐฉ์์ ํ์ฉํด์ ์์ ๋ฌธ์ ์ ํด๊ฒฐ ๊ฐ๋ฅ
- connect-mongo ํจํค์ง๋ฅผ ์ด์ฉํด MongoDB๋ฅผ session store๋ก ์ฌ์ฉํ ์ ์๋ค.
- ์๋์ผ๋ก session ๊ฐ์ด ๋ณ๊ฒฝ๋ ๋ update๋๊ณ , session์ด ํธ์ถ๋ ๋ find
const MongoStore = require('connect-mongo');
app.use(session({
secret: 'SeCrEt',
resave: false,
saveUninitialized: true,
store: MongoStore.create({
mongoUrl: 'mongoUrl',
}),
}));
- connect-mongo ํจํค์ง๋ฅผ ์ฌ์ฉํด exrpess-session ์ค์ ์ store ์ต์ ์ ์ ๋ฌํ๊ณ , mongoUrl์ ์ค์
- ์ธ์ ๋ฐ์ดํฐ๋ฅผ ๋ชฝ๊ณ ๋๋น์ ์ ์ฅํ๊ณ ๊ด๋ฆฌํ๋ ๊ธฐ๋ฅ์ ์๋์ผ๋ก ์ํ
5. ํ์๊ณผ ๊ฒ์๊ธ์ ์ฐ๋
- ๊ฒ์๊ธ ์์ฑ ์ ๋ก๊ทธ์ธ๋ ํ์ ์ ๋ณด๋ฅผ ์์ฑ์๋ก ์ถ๊ฐ
- ๊ฒ์๊ธ - ์์ฑ์๋ populateํ์ฌ ์ฌ์ฉํ๋๋ก ๊ตฌํ
- ๊ฒ์๊ธ ์์ , ์ญ์ ์ ๋ก๊ทธ์ธ๋ ์ ์ ์ ์์ฑ์๊ฐ ์ผ์นํ๋์ง ํ์ธ
- ์์ฑ์์ ๊ฒ์๊ธ ๋ชจ์๋ณด๊ธฐ ๊ธฐ๋ฅ ๊ตฌํ
/* PostSchema ์์ */
author: {
type: Schema.Types.ObjectId,
ref: 'User'.
required: true
},
/* ๊ฒ์๊ธ์ ์์ฑ์ ์ถ๊ฐ */
const author = await User.find({
shortId: req.user.shortId,
});
if(!author) {
throw new Error('No User');
}
await Post.create({
title,
content,
author,
});
- req.user์๋ strategy์์ ์ต์ํ์ ์ ๋ณด๋ก ์ ์ฅํ shortId, email, usernam๋ง ๊ฐ์ง๊ณ ์๋ค.
- Post ์์ฑ ์ user์ ObjectID๋ฅผ ์ ๋ฌํด์ผ ํ๋๋ฐ, ์ด๋ฅผ ์ํด User์์ shortId๋ก ํ์์ ๊ฒ์ํ์ฌ ํ๋ฒ ๋ ๊ฒ์ฆ
- type: ObjectID๋ก ์ ์ธํ ํ๋์ ๊ฐ์ฒด๊ฐ ์ฃผ์ด์ง๋ฉด ์๋์ผ๋ก ObjectID ์ฌ์ฉ
/* ./routes/posts.js */
router.get('/', ... {
...
const posts = await Post.find({})
...
.populate('author');
res.render('posts/list', {posts});
}
/* ./views/posts/list.pug */
...
td post.author.name
- ๊ฒ์๊ธ find ์ populate๋ฅผ ์ถ๊ฐํ์ฌ ObjectID๋ก ์ ์ฅ๋ author๋ฅผ ๊ฐ ๊ฒ์๊ธ์ ์ฃผ์
- ์ฌ์ฉ ์ post.author.{field}๋ก ์ฌ์ฉ ๊ฐ๋ฅ
/* ์์ , ์ญ์ ์ ์ ์ ํ์ธ */
const post = await Post.find({
shortId,
}).populate('author');
if (post.author.shortId !== req.user.shortId) {
throw new Error('Not Authorized');
}
/* index ์ฌ์ฉ */
author: {
type: Schema.Types.ObjectId,
ref: 'User'.
required: true,
index: true,
},
- index: true => MongoDB์ ์ธ๋ฑ์ค ์์ฑ
- ์ด๋ฏธ ๋ฐ์ดํฐ๊ฐ ๋ง์ ์ํ์์ ์ธ๋ฑ์ค๋ฅผ ์ถ๊ฐํ ์ ์์ ์๊ฐ์ด ๊ธธ์ด์ ธ์ MongoDB๊ฐ ์๋ตํ์ง ์์ ์ ์๊ธฐ ๋๋ฌธ์, ์์๋๋ ์ธ๋ฑ์ค๋ฅผ ๋ฏธ๋ฆฌ ์ถ๊ฐํ๋ ๊ฒ์ด ์ข์
/* ./routes/users.js */
...
router.get('/:shortId/posts', ... => {
...
const { shortId } = req.params;
const user = await User.find({ shortId });
const posts = await Post
.find({ author: user })
.populate('author');
res.render('posts/list', { posts, user });
});
...
/* ๊ฒ์๊ธ ๋ชฉ๋กํ๋ฉด */
h2= user ? `${user.name}์ ๊ฒ์๊ธ`: "์ ์ฒด ๊ฒ์๊ธ"
...
td: a(href=`/users/${post.author.shortId}/posts`)= post.author.name
- ๊ฒ์๊ธ์ ์ฌ์ฉ์ ์ด๋ฆ์ ์ ์ ์ ๊ฒ์๊ธ link ์ถ๊ฐ
6. CSR๋ก ๋๊ธ ๊ตฌํํ๊ธฐ
- ํ์ด์ง ๋ก๋ ์ ํ์ํ ๋ฆฌ์์ค๋ฅผ ํด๋ผ์ด์ธํธ์ ์ ์ธ
- ํด๋ผ์ด์ธํธ์์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ๋น๋๊ธฐ ํธ์ถ
- ํด๋ผ์ด์ธํธ๊ฐ ์ ๋ฌ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ๊ณต, ๋ฆฌ์์ค๋ฅผ ์ฌ์ฉํด์ ํ๋ฉด์ ํ์
1) ๊ฒ์๊ธ์ ๋๊ธ ์ถ๊ฐํ๊ธฐ
/* PostSchema */
const CommenSchema = new Schema({
content: String,
author: {
type: Schema.Types.ObjectId,
ref: 'User',
},
}, {
timestamps: true,
});
const PostSchema = new Schema({
...
comments: [CommentSchema],
});
- mongoose์ sub-schema๋ฅผ ์ด์ฉํ์ฌ Post ์คํค๋ง์ Comment๋ฅผ ๋ฐฐ์ด๋ก ์ถ๊ฐ
- populate๋ฅผ ์ฌ์ฉํ ๋, ObjectID๋ง ์ ์ฅํ๋ ๊ฒ๊ณผ๋ ๋ค๋ฅด๊ฒ Comment์ ๋ด์ฉ์ ๊ฒ์๊ธ์ด ํฌํจํ๊ฒ ๋จ
2) API ์์ฑํ๊ธฐ - ๋๊ธ ์์ฑ
/* routes/api.js */
router.post('/posts/:shortId/comments', ... {
const { shortId } = req.params;
const { content } = req.body;
const author = await User.findOne({ shortId: req.user.shortId });
await Post.updateOne({ shortId }, {
$push: { comments: {
content, author
} },
});
res.json({ result: 'success' });
});
- ๊ฒ์๊ธ ์
๋ฐ์ดํธ ์ $push๋ฅผ ์ฌ์ฉํ์ฌ comments ๋ฐฐ์ด์ ์๋ก ์์ฑ๋ ๋๊ธ ์ถ๊ฐ
- ๋์์ ๋ค์ด์ค๋ ์์ฒญ์ ๋ํด ์ ํํ๊ฒ ์ฒ๋ฆฌ
3) API ์์ฑํ๊ธฐ - ๋๊ธ ๋ชฉ๋ก
/* routes/api.js */
router.get('/posts/:shortId/comments', ... {
const { shortId } = req.params;
const post = await Post.findOne({ shortId });
await User.populate(post.comments, {
path: 'author'
});
res.json(post.comments);
});
4) fetch๋ก API ํธ์ถํ๊ณ ์ฒ๋ฆฌํ๊ธฐ - ๋๊ธ ์์ฑํ๊ธฐ
script
function writeComment() {
const input = document.querySelector('#content')
const content = input.value;
fetch('/api/posts/#{post.shortId}/comments', {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content }),
})
.then(() => {
if(res.ok) {
input.value = '';
loadComments();
} else {
alert('ERROR');
}
});
}
- ๋๊ธ ์์ฑ ๋ฒํผ ํด๋ฆญ ์ writeComment() ์คํ
- input#content์์ ๋ด์ฉ์ ์ฝ์ด fetch๋ก ๋๊ธ ์์ฑ api ํธ์ถ
- ํธ์ถ ๊ฒฐ๊ณผ์ ์ฑ๊ณต ์ฌ๋ถ๋ฅผ ํ์ธํ์ฌ, ๋๊ธ ๋ค์ ๋ถ๋ฌ์ค๊ธฐ ์คํ
7. MongoDB Aggregation
- MongoDB์์ Document๋ค์ ๊ฐ๊ณตํ๊ณ ์ฐ์ฐํ๋ ๊ธฐ๋ฅ
- ๋ค๋ฅธ Collection์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ฑฐ๋, ๊ฒ์๋ ๋ฐ์ดํฐ๋ฅผ ๊ทธ๋ฃนํํ๋ ๋ฑ์ ์์ ์ด ํ์ํ ๊ฒฝ์ฐ ์ฌ์ฉ
- aggregation์ Stage๋ค์ ๋ฐฐ์ด๋ก ์ด๋ฃจ์ด์ง๊ณ ๊ฐ Stage๋ ์์ฐจ์ ์ผ๋ก ์ํ๋จ
db.posts.aggregate([
{ $group: { _id: '$author', count: { $sum: 1 } } },
{ $match: { sum: { $gt: 10 } } },
{ $lookup: { from: 'users', localField: '_id', foreighField: '_id', as 'users' } }
]);
- ์์ฑ์๋ณ ๊ฒ์๊ธ ์๋ฅผ ์ทจํฉํ๊ณ , ๊ฒ์๊ธ ์๊ฐ 10๊ฐ๋ณด๋ค ๋ง์ ์์ฑ์๋ฅผ ์ฐพ์์ ํด๋น ์์ฑ์๋ฅผ ํ์ collection์์ ๊ฒ์ํ๋ค.