elice/WIL
[week_07] Express.js์ MongoDB๋ก ์น์๋น์ค ๋ง๋ค๊ธฐ (3) | JWT, OAuth, Nginx
๊ฑด๋ง๋
2022. 2. 25. 18:11
1. JWT
- JSON Web Token
- ์ธ์ฆ์ ์ํ ์ ๋ณด๋ฅผ ์ ์ ์๋ช ์ ์ด์ฉํด์ ํ์ธํ๋ ๋ฐฉ๋ฒ
- Web Token โก ๋ฐ์ดํฐ๋ฅผ ์น์์ ์ฌ์ฉํ๊ธฐ ์ํ ์คํ โก base64 ์ธ์ฝ๋ฉ ์ฌ์ฉ
1. header
- ํ ํฐ์ ํ์ (jwt), ๋ฐ์ดํฐ ์๋ช ๋ฐฉ์
2. payload
- ์ ๋ฌ๋๋ ๋ฐ์ดํฐ
3. signature
- ํค๋์ ํ์ด๋ก๋์ ์ ์์๋ช
- payload๋ decode ์ ์ ๋ณด๊ฐ ๋ ธ์ถ๋๊ธฐ ๋๋ฌธ์, ๋ฏผ๊ฐํ ์ ๋ณด๋ฅผ ์ ์ธํ๊ณ ํ ํฐ์ ์์ฑํด์ผ ํ๋ค.
- ์๋ฒ๊ฐ JWT๋ฅผ ์์ฑํ ๋, ๋น๊ณต๊ฐํค๋ฅผ ์ด์ฉํด์ ์๋ช โก payload ์กฐ์ํ๋ฉด ์๋ช ์ด ๋ถ์ผ์น โก ์ธ์ฆ ์คํจ
โจ JWT ์๋ ๋ฐฉ์
1. ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ
2. ์๋ฒ๊ฐ ๋ก๊ทธ์ธ๋ ์ ์ ์ ๋ณด๋ฅผ JWT๋ก ์์ฑํ์ฌ ํด๋ผ์ด์ธํธ์ ์ ๋ฌ
3. ํด๋ผ์ด์ธํธ๋ ์ ๋ฌ๋ฐ์ JWT๋ฅผ ์ด์ฉํ์ฌ ์ธ์ฆ์ด ํ์ํ ์์ฒญ์ ์ฌ์ฉ
- session์ ๊ฒฝ์ฐ, ์น ๋ธ๋ผ์ฐ์ ๊ฐ ์๋ ์ดํ๋ฆฌ์ผ์ด์ ์ ๊ฒฝ์ฐ ํ์ฉํ๊ธฐ ๋ถ์ ํฉํ๋ค. JWT๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋ ํด๋ผ์ด์ธํธ์์๋ ๋์ผํ ๋ฐฉ์์ ์ฌ์ฉ์ ์ธ์ฆ์ ๊ตฌํํ ์ ์๊ธฐ ๋๋ฌธ์, JWT๋ฅผ ์ฌ์ฉํ๋ค.
1-1. JWT +Cookie ์ฌ์ฉํ๊ธฐ
๐น Session์ ์ฌ์ฉํ ์ ์ ๋ก๊ทธ์ธ
Cookie์ Session ID ์ ์ฅ โก Session Store์์ ์ ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
๐น JWT๋ฅผ ์ฟ ํค์ ์ ์ฅํ๋ ๊ฒฝ์ฐ
JWT๋ก ์์ฒญ โก ์๋ฒ๊ฐ ์๋ช ํ์ธ ํ ์ ์ ์ ๋ณด ์ฌ์ฉ
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ๊ทผ์ด ์ค์ด์ ํจ์จ์ ์ธ ์ธ์ฆ ๊ตฌํ ๊ฐ๋ฅ!
โจ JWT ๋ก๊ทธ์ธ ๊ตฌํํ๊ธฐ
1. ๊ธฐ์กด ์ธ์ ์ผ๋ก ๊ตฌํ๋ ๋ก๊ทธ์ธ ๋นํ์ฑํ
2. ๋ก๊ทธ์ธ ๋ก์ง์์ JWT ์์ฑ ํ ์ฟ ํค๋ก ์ ๋ฌ
3. passport-jwt ํจํค์ง๋ก JWT ๋ก๊ทธ์ธ ๋ฏธ๋ค์จ์ด ์์ฑ ๋ฐ ์ฌ์ฉ
1) ๋ก๊ทธ์ธ ๋ก์ง์ JWT ํ ํฐ ์์ฑ ๋ฐ ์ฟ ํค ์ ๋ฌ
setUserToken = (res, user) => {
const token = jwt.sign(user, secret); // ์ ์ jwt ํ ํฐ ์์ฑ
res.cookie('token', token);
} // res.cookie ํจ์ ์ฌ์ฉ => ํ ํฐ์ ํด๋ผ์ด์ธํธ์ ์ฟ ํค๋ก ์ ๋ฌ
// --------------------
router.post('/', passport.authenticate('local'), (req, res, next) => {
// ์ ์ ํ ํฐ ์์ฑ ๋ฐ ์ฟ ํค์ ์ ๋ฌํ๊ธฐ
setUserToken(res, req.user);
res.redirect('/');
});
2) passport-jwt๋ก ์์ฒญ๋ JWT ํ ํฐ์ ์๋ช ํ์ธํ๊ณ ์ธ์ฆํ๊ธฐ
const JwtStrategy = require('passport-jwt').Strategy;
const cookieExtractor = (req) => {
const { token } = req.cookies;
return token;
};
const opts = {
secretOrKey: secret,
jwtFromRequest: cookieExtractor,
}
module.exports = new JwtStrategy(opts, (user, done) = {
done(null, user);
});
// -----
passport.use(jwt);
3) JWT ๋ฏธ๋ค์จ์ด ์ถ๊ฐ
- JWT ํ ํฐ์ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ชจ๋ ์์ฒญ์ ํฌํจ
- ์์ฒญ์ ํ ํฐ์ด ์๋ ๊ฒฝ์ฐ ๋ก๊ทธ์ธ๋ ์ํ๋ก ์ฒ๋ฆฌํ๊ธฐ ์ํด
- ๋ชจ๋ ์์ฒญ์ ๊ณตํต์ ์ผ๋ก ์ ์ฉํ ์ ์๋ ๋ฏธ๋ค์จ์ด๋ก JWT ๋ก๊ทธ์ธ์ ์ถ๊ฐ
/* jwt middleware */
app.use((req, res, next) => {
if(!req.cookies.token) { // ํ ํฐ ์๋ ๊ฒฝ์ฐ
next();
return;
}
return passport.authenticate('jwt')(req, res, next);
// authenticate ํจ์์ req, res, next ์ ๋ฌ
// ์ด ๋ jwt Strategy๋ฅผ ์ฌ์ฉํ๋ค.
});
// app.js
// passport.initialize์ ๋ค๋ฅธ ๋ฏธ๋ค์จ์ด๋ค ์ฌ์ด์ ์์ฑํด์ผ ํ๋ค.
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(passport.initialize());
// jwt ๋ก๊ทธ์ธ ๋ฏธ๋ค์จ์ด ์ถ๊ฐ
app.use(getUserFromJWT);
app.use('/', indexRouter);
app.use('/posts', loginRequired, postsRouter);
app.use('/users', loginRequired, usersRouter);
app.use('/api', loginRequired, apiRouter);
app.use('/auth', authRouter);
4) ๋ก๊ทธ์์
- ํด๋ผ์ด์ธํธ ์ฟ ํค๋ฅผ ์ญ์ ํ์ฌ ์ฒ๋ฆฌ
- token ๊ฐ์ null๋ก ์ ๋ฌํ๋ ๊ฒ๊ณผ ํจ๊ป, cookie์ ๋ง๋ฃ ์๊ฐ์ 0์ผ๋ก ์ค์ ํ์ฌ ํด๋ผ์ด์ธํธ๊ฐ ์ฟ ํค๋ฅผ ๋ฐ๋ก ๋ง๋ฃ์ํค๋๋ก ์ ๋ฌ
res.cookie('token', null, {
maxAge: 0,
});
2. ํ์ ๋น๋ฐ๋ฒํธ ์ฐพ๊ธฐ ๊ตฌํ
1. ์์์ ๋ฌธ์์ด๋ก ๋น๋ฐ๋ฒํธ ์ด๊ธฐํ
2. ์ด๊ธฐํ๋ ๋ฌธ์์ด์ ๋ฉ์ผ๋ก ์ ๋ฌ โก ๋ฉ์ผ ๋ฐ์ก๊ธฐ๋ฅ ๊ฐ๋ฐ ํ์
3. ์ด๊ธฐํ ํ ์ฒซ ๋ก๊ทธ์ธ ์ ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ ์์ฒญ
2-1. ๋ฉ์ผ ๋ฐ์ก๊ธฐ๋ฅ ๊ตฌํ ๋ฐฉ๋ฒ
- SMTP ์๋ฒ ์ด์ฉ : ๋ค์ด๋ฒ, ๊ตฌ๊ธ ๋ฑ์ ๋ฉ์ผ์๋ฒ ์ด์ฉํ์ฌ ๋ฉ์ผ ๋ฐ์ก ๋ฐ ๊ด๋ฆฌ ๊ธฐ๋ฅ ์ง์ ๊ฐ๋ฐ
- ๋ฉ์ผ ๋ฐ์ก ์๋น์ค ์ด์ฉ(Mailgun, Sendgrid, ...) : ๋ฉ์ผ ๋ฐ์ก api ๋ฐ ๊ด๋ฆฌ์ฉ ์นํ์ด์ง ์ ๊ณต
1) Nodemailer + Gmail ์ฌ์ฉํ๊ธฐ
/* Nodemailer + Gmail ์ฌ์ฉํ๊ธฐ */
const nodemailer = require('nodemailer');
const transport = nodemailer.createTransport({
service: 'Gmail',
auth: {
user: "google account",
pass: "app password",
},
});
const message = {
from: "login account",
to: "mail address",
subjet: "title",
text: "message"
};
transport.sendMail(message, (err, info) => {
if (err) {
console.error('err', err);
return;
}
console.log('ok', info);
});
2) ๋น๋ฐ๋ฒํธ ์ด๊ธฐํ ๊ธฐ๋ฅ ๊ฐ๋ฐ
function generateRandomPassword() { // ์์์ ๋ฌธ์์ด ์์ฑ
return Math.floor(
Math.random() * (10 ** 8)
).toString().padStart('0', 8);
}
//---
router.post('/reset-password', asyncHandler(... => {
const { email } = req.body; // ์ด๋ฉ์ผ์ ๋ฐ์์
const password = generateRandomPassword(); // ์์ฑ๋ ์์์ ๋ฌธ์์ด๋ก ์ฌ์ฉ์์ ๋น๋ฐ๋ฒํธ ์ด๊ธฐํ
await User.findOneAndUpdate({ email }, { // ์ ๋ฌ๋ฐ์ ์ด๋ฉ์ผ๋ก ์ ์ ํจ์ค์๋ ๋ณ๊ฒฝ
password: getHash(password),
});
await sendEmail(email, '...', password); // ์ด๊ธฐํํ ๋น๋ฐ๋ฒํธ๋ฅผ ๋ฉ์ผ๋ก ๋ฐ์ก
res.redirect('/');
}));
3) ์ด๊ธฐํ ํ ๋ก๊ทธ์ธ ์ ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ ์์ฒญ
/* ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ */
const UserSchema = {
...
passwordReset: {
type: Boolean,
default: false,
}
...
}
// ---
router.post('/reset-password', ...
await User.findOneAndUpdate({
...
passwordReset: true, // ํจ์ค์๋ ๋ฆฌ์
๋ ๊ฒฝ์ฐ true๋ก ๋ณ๊ฒฝ
});
/* ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ ์์ฒญ */
function checkPasswordReset(req, res, next) {
if(req.user && req.user.passwordReset) {
res.redirect('/update-password');
return;
}
next();
}
// ---
router.post('/update-password', ...
await User.findOneAndUpdate({
...
passwordReset: false, // ๋น๋ฐ๋ฒํธ ์
๋ฐ์ดํธ ๋๋ฉด ๋ค์ false๋ก ๋ณ๊ฒฝ
});
3. OAuth
- Open Authorization
- ์๋น์ค ์ ๊ณต์๊ฐ ๋ค๋ฅธ ์๋น์ค์๊ฒ ๋ฐ์ดํฐ๋ฅผ ์ ๊ณตํ๊ธฐ ์ํด ์๋น์ค ์ฌ์ฉ์์๊ฒ ์ ๊ณตํ๋ ์ฌ์ฉ์ ์ธ์ฆ๋ฐฉ์์ ํ์ค
- ์ฌ์ฉ์์ ์ธ์ฆ์ ์ ๊ณตํ๋ ํ์ค
- ์ด๋ฅผ ํ์ฉํด์ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ๊ฐํธํ๊ฒ ๊ตฌ์ฑํ ์ ์๋ค.
- ์น ์๋น์ค ์ ๊ณต์๋ ์์ด๋, ๋น๋ฐ๋ฒํธ ๋ก๊ทธ์ธ์ ๊ตฌํํ ํ์๊ฐ ์๋ค.
- ์น ์๋น์ค ์ฌ์ฉ์๋ ๋ก๊ทธ์ธ ์ ์์ด๋, ๋น๋ฐ๋ฒํธ๋ฅผ ์ ๋ ฅํ ํ์๊ฐ ์๋ค.
1. ์๋น์ค ์ ๊ณต์์๊ฒ ์ธ์ฆ ์์ฒญ
2. ์ธ์ฆ ์๋ฃ ํ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์์ฒญํ ์๋น์ค๋ก ์ ๋ฌ
3. ์ธ์ฆ ์ ๋ณด๋ฅผ ์ด์ฉํด ์๋น์ค ์ ๊ณต์์ ๋ฐ์ดํฐ ์ฌ์ฉ
ex) ๊ตฌ๊ธ ์บ๋ฆฐ๋ ์ฐ๋ ์๋น์ค๋ฅผ ๋ง๋ ๋ค๊ณ ๊ฐ์
1. ๊ตฌ๊ธ OAuth ์ธ์ฆ ์์ฒญ
2. ์ธ์ฆ๋ OAuth Token์ ๊ธฐ๋ก
3. OAuth Token์ ์ฌ์ฉํ์ฌ ๊ตฌ๊ธ ์บ๋ฆฐ๋ API ์ฌ์ฉ
4. ๊ตฌ๊ธ ๋ก๊ทธ์ธ ๊ตฌํ
๐ ๊ตฌ๊ธ ๋ก๊ทธ์ธ ๊ตฌํ ์์
- ๊ตฌ๊ธ ํด๋ผ์ฐ๋ ํ๋ซํผ ํ๋ก์ ํธ ์์ฑ
- API ๋ฐ ์๋น์ค โก OAuth ๋์ํ๋ฉด ์ค์
- ์ฌ์ฉ์ ์ธ์ฆ์ ๋ณด โก OAuth ํด๋ผ์ด์ธํธ ID ๋ง๋ค๊ธฐ
- passport-google-oauth20 ์ฐ๋
4-1. passport-google-oauth20
- passport-strategy ์ธํฐํ์ด์ค์ ๊ตฌ๊ธ ๋ก๊ทธ์ธ ๊ตฌํ์ฒด
- ์์ฝ๊ฒ ๊ตฌ๊ธ OAuth 2.0์ ๊ตฌํํด์ฃผ๋ ํจํค์ง
1) passport-google-oauth20 ์์ฑ
/* google strategy */
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const config = { // OAuth ํด๋ผ์ด์ธํธ ์ค์ ๊ฐ ๋ฐ ์๋ฃ ๊ฒฐ๊ณผ๋ฅผ ์ ๋ฌ๋ฐ์ callbackURL
clientID: 'clientID',
clientSecret: 'clientSecret',
callbackURL: 'callbackUrl',
};
...
// accessToken, RefreshToken์ ๋ค๋ฅธ ๊ตฌ๊ธ API๋ค์ ์ฌ์ฉํ๊ธฐ ์ํ ํ ํฐ
new GoogleStrategy(config, (accessToken, refreshToken, profile, done) => {
const { email, name } = profile._json; // profile: ์ ๋ฌ๋ฐ์ ์ ์ ์ ๋ณด, ์ ์ ์์ฑ ๋๋ OAuth ์ ๋ณด ์
๋ฐ์ดํธ์ ์ฌ์ฉ
..
//create or update user
})
- ๊ตฌ๊ธ ๋ก๊ทธ์ธ์ด ์๋ฃ๋ ํ ๊ฒฐ๊ณผ๋ฅผ ์ ๋ฌ๋ฐ๋ ๋ถ๋ถ
2) passport-google-oauth20 ์ฌ์ฉ
passport.use(google);
// ---
// /auth/google ์ ๊ทผ ์ ์๋์ผ๋ก ๊ตฌ๊ธ ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋์ด๊ฐ
router.get('/google', passport.authenticate('google', {
scope: ['profile', 'email']
}));
// ๋ก๊ทธ์ธ ์๋ฃ ํ ๋ก๊ทธ์ธ ์ ๋ณด๋ฅผ config์ ์ค์ ๋ ์ฃผ์์ธ /auth/google/callback์ผ๋ก ์ ๋ฌ
// ์ ๋ฌ๋ฐ์ ๋ฐ์ดํฐ๋ strategy์์ ์ฒ๋ฆฌ, ์ฒ๋ฆฌ๊ฐ ์๋ฃ๋๋ฉด request handler ์คํ
router.get('/google/callback', passport.authenticate('google', {
failureRedirect: '/login'
}), (req, res, next) => {
res.redirect('/');
});
5. Nginx
- ์น ์๋ฒ ์ํํธ์จ์ด (HTTP ์์ฒญ์ ๋ฐ์์ ํ์ผ์ด๋ ์คํ ๊ฒฐ๊ณผ๋ฅผ HTTP ์๋ต์ผ๋ก ๋ณด๋ด์ฃผ๋ ์ํํธ์จ์ด)
- Node.js๋ ๊ธฐ๋ณธ์ ์ผ๋ก HTTP ์์ฒญ์ ์์ ํ๊ณ ์๋ตํ๋ ๊ธฐ๋ฅ์ด ์ด๋ฏธ ์กด์ฌ
- ํ์ง๋ง, Node.js ๋จ๋ ์ผ๋ก production-level ์๋น์ค๋ฅผ ๊ตฌ์ถํ ์๋ ์๋ค.
- Nginx์ reverse-proxy ๊ธฐ๋ฅ์ ์ฌ์ฉํด์ Node.js์ Nginx๋ฅผ ์ฐ๊ฒฐํ ์ ์์
// http://www.example.com์ผ๋ก ์ ์ํ ๋ชจ๋ ์์ฒญ์ localhost:3000์ผ๋ก ์ ๋ฌํ๋ ์ค์ ํ์ผ
server {
listen 80;
server_name www.example.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
}
}