Ver3.0 ๐ JWT ์ธ์ฆ์ ํ์ฉํ ์ฌ์ฉ์ ๋ก๊ทธ์ธ ์์คํ ์๋ฒฝ ๊ตฌํ ๊ฐ์ด๋

๐ JWT ์ธ์ฆ์ ํ์ฉํ ์ฌ์ฉ์ ๋ก๊ทธ์ธ ์์คํ ์๋ฒฝ ๊ตฌํ ๊ฐ์ด๋
์ค์ ์์ค์ฝ๋์ ํจ๊ป ๋ฐฐ์ฐ๋ ํ๋์ ์ธ ์ธ์ฆ ์์คํ ๊ตฌ์ถ๋ฒ
๐ค JWT๊ฐ ๋ญ๊ธธ๋ ๋ค๋ค ๋๋ฆฌ์ผ?
JWT๋ JSON Web Token์ ์ฝ์์ธ๋ฐ์, ์ฝ๊ฒ ๋งํ๋ฉด ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์์ ํ๊ฒ ๋ด์ ๋์งํธ ์ ๋ถ์ฆ ๊ฐ์ ๊ฑฐ์์. ์ ํต์ ์ธ ์ธ์ ๋ฐฉ์๊ณผ ๋ฌ๋ฆฌ ์๋ฒ์ ์ํ๋ฅผ ์ ์ฅํ์ง ์์๋ ๋๊ธฐ ๋๋ฌธ์ ํ์ฅ์ฑ์ด ๋ฐ์ด๋์ฃ .
โข ํ์ฅ์ฑ: ์ฌ๋ฌ ์๋ฒ๋ก ํ์ฅํ๊ธฐ ์ฌ์์
โข ๋ชจ๋ฐ์ผ ์นํ์ : ์ฑ์์๋ ์ฝ๊ฒ ์ฌ์ฉ ๊ฐ๋ฅํด์
โข ๋ง์ดํฌ๋ก์๋น์ค: ์๋น์ค ๊ฐ ์ธ์ฆ ๊ณต์ ๊ฐ ํธ๋ฆฌํด์
JWT๋ ์ธ ๋ถ๋ถ์ผ๋ก ๋๋์ด์ ธ ์์ด์. Header(ํค๋), Payload(ํ์ด๋ก๋), Signature(์๋ช )๋ก ๊ตฌ์ฑ๋๊ณ , ์ (.)์ผ๋ก ๊ตฌ๋ถ๋ฉ๋๋ค. ์๋ฅผ ๋ค๋ฉด ์ด๋ฐ ์์ด์ฃ :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NSIsIm5hbWUiOiLtmY3quLjrj5kifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
๋ณต์กํด ๋ณด์ด์ง๋ง ๊ฑฑ์ ๋ง์ธ์! ํ๋์ฉ ํํค์ณ๋ณผ๊ฒ์. ๐
๐ ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ ์ค๋นํ๊ธฐ
๋ณธ๊ฒฉ์ ์ผ๋ก ์ฝ๋๋ฅผ ์์ฑํ๊ธฐ ์ ์ ํ์ํ ๊ฒ๋ค์ ์ค๋นํด๋ณผ๊น์? ์ด๋ฒ ์์ ์์๋ Node.js์ Express๋ฅผ ์ฌ์ฉํ ๊ฑฐ์์. ๊ฐ์ฅ ๋์ค์ ์ด๊ณ ๋ฐฐ์ฐ๊ธฐ ์ฌ์ด ์กฐํฉ์ด๊ฑฐ๋ ์! ๐จ
npm init -y
npm install express jsonwebtoken bcryptjs dotenv
npm install --save-dev nodemon
๊ฐ ํจํค์ง๊ฐ ํ๋ ์ผ์ ์์๋ณผ๊ฒ์:
โข express: ์น ์๋ฒ ํ๋ ์์ํฌ
โข jsonwebtoken: JWT ์์ฑ๊ณผ ๊ฒ์ฆ์ ๋ด๋น
โข bcryptjs: ๋น๋ฐ๋ฒํธ ์ํธํ์ฉ
โข dotenv: ํ๊ฒฝ๋ณ์ ๊ด๋ฆฌ
โข nodemon: ๊ฐ๋ฐ ์ ์๋ ์ฌ์์
์ค๋ฌด์์๋ ์ฝ๋๋ฅผ ๊น๋ํ๊ฒ ์ ๋ฆฌํ๋ ๊ฒ ์ ๋ง ์ค์ํด์. ์ด๋ฐ ๊ตฌ์กฐ๋ฅผ ์ถ์ฒ๋๋ ค์:
project/
โโโ config/
โ โโโ db.js
โโโ controllers/
โ โโโ authController.js
โโโ middleware/
โ โโโ authMiddleware.js
โโโ models/
โ โโโ User.js
โโโ routes/
โ โโโ authRoutes.js
โโโ .env
โโโ server.js
โโโ package.json
๐ ํ๊ฒฝ ๋ณ์ ์ค์ ํ๊ธฐ
๋ณด์์ ๋ฏผ๊ฐํ ์ ๋ณด๋ค์ ์ ๋ ์ฝ๋์ ์ง์ ๋ฃ์ผ๋ฉด ์ ๋ผ์! .env ํ์ผ์ ๋ง๋ค์ด์ ๊ด๋ฆฌํด์ผ ํฉ๋๋ค. ์ด๊ฑด ์ ๋ง์ ๋ง ์ค์ํ ๋ถ๋ถ์ด์์. ๐ฑ
# .env ํ์ผ
PORT=5000
JWT_SECRET=your_super_secret_key_change_this_in_production_12345
JWT_EXPIRE=1h
JWT_REFRESH_SECRET=your_refresh_token_secret_key_67890
JWT_REFRESH_EXPIRE=7d
MONGODB_URI=mongodb://localhost:27017/jwt_auth_demo
โข .env ํ์ผ์ ์ ๋ Git์ ์ปค๋ฐํ์ง ๋ง์ธ์ (.gitignore์ ์ถ๊ฐ!)
โข ํ๋ก๋์ ํ๊ฒฝ์์๋ ๋ ๊ฐ๋ ฅํ ์ํฌ๋ฆฟ ํค๋ฅผ ์ฌ์ฉํ์ธ์
โข ํ ํฐ ๋ง๋ฃ ์๊ฐ์ ๋ณด์๊ณผ ์ฌ์ฉ์ฑ์ ๊ท ํ์ ๊ณ ๋ คํด์ ์ค์ ํ์ธ์
๐พ ์ฌ์ฉ์ ๋ชจ๋ธ ๋ง๋ค๊ธฐ
๋จผ์ ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ์ด๋ป๊ฒ ์ ์ฅํ ์ง ์ ์ํด์ผ๊ฒ ์ฃ ? MongoDB์ Mongoose๋ฅผ ์ฌ์ฉํ ์์ ๋ฅผ ๋ณด์ฌ๋๋ฆด๊ฒ์. ์ค์ ๋ก๋ ์ฌ๋ฌ๋ถ์ด ์ ํธํ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฉ๋๋ค! ๐๏ธ
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
email: {
type: String,
required: [true, '์ด๋ฉ์ผ์ ์
๋ ฅํด์ฃผ์ธ์'],
unique: true,
lowercase: true,
trim: true,
match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, '์ ํจํ ์ด๋ฉ์ผ์ ์
๋ ฅํด์ฃผ์ธ์']
},
password: {
type: String,
required: [true, '๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์'],
minlength: [6, '๋น๋ฐ๋ฒํธ๋ ์ต์ 6์ ์ด์์ด์ด์ผ ํฉ๋๋ค'],
select: false // ๊ธฐ๋ณธ ์ฟผ๋ฆฌ์์ ๋น๋ฐ๋ฒํธ ์ ์ธ
},
name: {
type: String,
required: [true, '์ด๋ฆ์ ์
๋ ฅํด์ฃผ์ธ์'],
trim: true
},
createdAt: {
type: Date,
default: Date.now
},
refreshToken: {
type: String,
select: false
}
});
// ๋น๋ฐ๋ฒํธ ์ ์ฅ ์ ์ํธํ
userSchema.pre('save', async function(next) {
// ๋น๋ฐ๋ฒํธ๊ฐ ์์ ๋์ง ์์๋ค๋ฉด ์คํต
if (!this.isModified('password')) {
return next();
}
// ๋น๋ฐ๋ฒํธ ํด์ฑ
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// ๋น๋ฐ๋ฒํธ ๋น๊ต ๋ฉ์๋
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
์ด ์ฝ๋์์ ์ฃผ๋ชฉํ ์ ๋ค์ด ์์ด์:
2. select: false: ์ผ๋ฐ ์ฟผ๋ฆฌ์์ ๋น๋ฐ๋ฒํธ๊ฐ ์๋์ผ๋ก ์ ์ธ๋ผ์. ๋ณด์์ ์ํด ํ์!
3. ์ ํจ์ฑ ๊ฒ์ฆ: ์ด๋ฉ์ผ ํ์, ๋น๋ฐ๋ฒํธ ๊ธธ์ด ๋ฑ์ ์คํค๋ง ๋ ๋ฒจ์์ ๊ฒ์ฆํด์
4. ๋ฉ์๋ ์ถ๊ฐ: comparePassword๋ก ๋ก๊ทธ์ธ ์ ๋น๋ฐ๋ฒํธ ํ์ธ์ ์ฝ๊ฒ ํ ์ ์์ด์
๐ซ JWT ์ ํธ๋ฆฌํฐ ํจ์ ๋ง๋ค๊ธฐ
JWT๋ฅผ ์์ฑํ๊ณ ๊ฒ์ฆํ๋ ํจ์๋ค์ ๋ฏธ๋ฆฌ ๋ง๋ค์ด๋๋ฉด ์ฝ๋ ์ฌ์ฌ์ฉ์ด ํธํด์ ธ์. ๊น๋ํ๊ฒ ์ ๋ฆฌํด๋ณผ๊น์? โจ
// utils/jwtUtils.js
const jwt = require('jsonwebtoken');
// Access Token ์์ฑ
const generateAccessToken = (userId) => {
return jwt.sign(
{ userId }, // payload
process.env.JWT_SECRET,
{
expiresIn: process.env.JWT_EXPIRE,
issuer: 'your-app-name' // ๋ฐ๊ธ์ ์ ๋ณด
}
);
};
// Refresh Token ์์ฑ
const generateRefreshToken = (userId) => {
return jwt.sign(
{ userId },
process.env.JWT_REFRESH_SECRET,
{
expiresIn: process.env.JWT_REFRESH_EXPIRE,
issuer: 'your-app-name'
}
);
};
// Access Token ๊ฒ์ฆ
const verifyAccessToken = (token) => {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('ํ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค');
}
if (error.name === 'JsonWebTokenError') {
throw new Error('์ ํจํ์ง ์์ ํ ํฐ์
๋๋ค');
}
throw error;
}
};
// Refresh Token ๊ฒ์ฆ
const verifyRefreshToken = (token) => {
try {
return jwt.verify(token, process.env.JWT_REFRESH_SECRET);
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('๋ฆฌํ๋ ์ ํ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค');
}
if (error.name === 'JsonWebTokenError') {
throw new Error('์ ํจํ์ง ์์ ๋ฆฌํ๋ ์ ํ ํฐ์
๋๋ค');
}
throw error;
}
};
module.exports = {
generateAccessToken,
generateRefreshToken,
verifyAccessToken,
verifyRefreshToken
};
Refresh Token: ๊ธด ์๋ช (๋ณดํต 7์ผ~30์ผ), Access Token ์ฌ๋ฐ๊ธ์ฉ
์ด๋ ๊ฒ ๋ ๊ฐ๋ฅผ ์ฌ์ฉํ๋ ์ด์ ๋ ๋ณด์๊ณผ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ท ํ์ ๋ง์ถ๊ธฐ ์ํด์์์. Access Token์ด ํ์ทจ๋๋๋ผ๋ ์งง์ ์๊ฐ ๋ด์ ๋ง๋ฃ๋๊ณ , Refresh Token์ผ๋ก ์๋ก์ด Access Token์ ๋ฐ๊ธ๋ฐ์ ์ ์์ฃ ! ๐ก๏ธ
๐ช ํ์๊ฐ์ API ๊ตฌํํ๊ธฐ
์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ํ์๊ฐ์ ๊ธฐ๋ฅ์ ๋ง๋ค์ด๋ณผ๊ฒ์. ์ฌ์ฉ์๊ฐ ๊ณ์ ์ ๋ง๋ค ์ ์์ด์ผ ๋ก๊ทธ์ธ๋ ํ ์ ์๊ฒ ์ฃ ? ๐
// controllers/authController.js
const User = require('../models/User');
const { generateAccessToken, generateRefreshToken } = require('../utils/jwtUtils');
// ํ์๊ฐ์
exports.register = async (req, res) => {
try {
const { email, password, name } = req.body;
// ์
๋ ฅ๊ฐ ๊ฒ์ฆ
if (!email || !password || !name) {
return res.status(400).json({
success: false,
message: '๋ชจ๋ ํ๋๋ฅผ ์
๋ ฅํด์ฃผ์ธ์'
});
}
// ์ด๋ฉ์ผ ์ค๋ณต ์ฒดํฌ
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(409).json({
success: false,
message: '์ด๋ฏธ ์กด์ฌํ๋ ์ด๋ฉ์ผ์
๋๋ค'
});
}
// ์ฌ์ฉ์ ์์ฑ
const user = await User.create({
email,
password,
name
});
// ํ ํฐ ์์ฑ
const accessToken = generateAccessToken(user._id);
const refreshToken = generateRefreshToken(user._id);
// Refresh Token์ DB์ ์ ์ฅ
user.refreshToken = refreshToken;
await user.save();
// ์๋ต (๋น๋ฐ๋ฒํธ ์ ์ธ)
res.status(201).json({
success: true,
message: 'ํ์๊ฐ์
์ด ์๋ฃ๋์์ต๋๋ค',
data: {
user: {
id: user._id,
email: user.email,
name: user.name
},
accessToken,
refreshToken
}
});
} catch (error) {
console.error('ํ์๊ฐ์
์๋ฌ:', error);
res.status(500).json({
success: false,
message: '์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
ํ์๊ฐ์ ๋ก์ง์์ ์ค์ํ ๋ถ๋ถ๋ค์ ์ง์ด๋ณผ๊ฒ์:
1๏ธโฃ ์ ๋ ฅ๊ฐ ๊ฒ์ฆ: ํ์ ํ๋๊ฐ ๋ชจ๋ ์๋์ง ํ์ธ
2๏ธโฃ ์ค๋ณต ์ฒดํฌ: ์ด๋ฏธ ์กด์ฌํ๋ ์ด๋ฉ์ผ์ธ์ง ํ์ธ
3๏ธโฃ ๋น๋ฐ๋ฒํธ ์ํธํ: User ๋ชจ๋ธ์ pre-save ํ ์์ ์๋ ์ฒ๋ฆฌ
4๏ธโฃ ํ ํฐ ์์ฑ: Access Token๊ณผ Refresh Token ๋ชจ๋ ์์ฑ
5๏ธโฃ Refresh Token ์ ์ฅ: DB์ ์ ์ฅํด์ ๋์ค์ ๊ฒ์ฆ
6๏ธโฃ ์์ ํ ์๋ต: ๋น๋ฐ๋ฒํธ๋ ์ ๋ ์๋ต์ ํฌํจํ์ง ์๊ธฐ
๐ ๋ก๊ทธ์ธ API ๊ตฌํํ๊ธฐ
๋๋์ด ํต์ฌ์ธ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ด์์! ์ด๊ฒ ๋ฐ๋ก JWT ์ธ์ฆ์ ์์์ ์ด์ฃ . ์ฌ์ฉ์๊ฐ ์ด๋ฉ์ผ๊ณผ ๋น๋ฐ๋ฒํธ๋ฅผ ์ ๋ ฅํ๋ฉด JWT ํ ํฐ์ ๋ฐ๊ธํด์ฃผ๋ ๊ฑฐ์์. ๐
// controllers/authController.js (๊ณ์)
// ๋ก๊ทธ์ธ
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
// ์
๋ ฅ๊ฐ ๊ฒ์ฆ
if (!email || !password) {
return res.status(400).json({
success: false,
message: '์ด๋ฉ์ผ๊ณผ ๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์'
});
}
// ์ฌ์ฉ์ ์ฐพ๊ธฐ (๋น๋ฐ๋ฒํธ ํฌํจ)
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({
success: false,
message: '์ด๋ฉ์ผ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค'
});
}
// ๋น๋ฐ๋ฒํธ ํ์ธ
const isPasswordCorrect = await user.comparePassword(password);
if (!isPasswordCorrect) {
return res.status(401).json({
success: false,
message: '์ด๋ฉ์ผ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค'
});
}
// ํ ํฐ ์์ฑ
const accessToken = generateAccessToken(user._id);
const refreshToken = generateRefreshToken(user._id);
// Refresh Token ์
๋ฐ์ดํธ
user.refreshToken = refreshToken;
await user.save();
// ๋ก๊ทธ์ธ ์ฑ๊ณต ์๋ต
res.status(200).json({
success: true,
message: '๋ก๊ทธ์ธ ์ฑ๊ณต',
data: {
user: {
id: user._id,
email: user.email,
name: user.name
},
accessToken,
refreshToken
}
});
} catch (error) {
console.error('๋ก๊ทธ์ธ ์๋ฌ:', error);
res.status(500).json({
success: false,
message: '์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
๐ก๏ธ ์ธ์ฆ ๋ฏธ๋ค์จ์ด ๋ง๋ค๊ธฐ
์ด์ ๋ณดํธ๋ API ์๋ํฌ์ธํธ๋ฅผ ๋ง๋ค ์ฐจ๋ก์์. ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ง ์ ๊ทผํ ์ ์๋ ๊ธฐ๋ฅ๋ค ๋ง์ด์ฃ . ๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํ๋ฉด ์ฝ๋ ์ค๋ณต ์์ด ๊น๋ํ๊ฒ ๊ตฌํํ ์ ์์ด์! ๐ช
// middleware/authMiddleware.js
const { verifyAccessToken } = require('../utils/jwtUtils');
const User = require('../models/User');
// ์ธ์ฆ ๋ฏธ๋ค์จ์ด
exports.authenticate = async (req, res, next) => {
try {
// ํค๋์์ ํ ํฐ ์ถ์ถ
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
message: '์ธ์ฆ ํ ํฐ์ด ํ์ํฉ๋๋ค'
});
}
// Bearer ์ ๊ฑฐํ๊ณ ํ ํฐ๋ง ์ถ์ถ
const token = authHeader.substring(7);
// ํ ํฐ ๊ฒ์ฆ
const decoded = verifyAccessToken(token);
// ์ฌ์ฉ์ ์ ๋ณด ์กฐํ
const user = await User.findById(decoded.userId);
if (!user) {
return res.status(401).json({
success: false,
message: '์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค'
});
}
// req ๊ฐ์ฒด์ ์ฌ์ฉ์ ์ ๋ณด ์ถ๊ฐ
req.user = user;
req.userId = user._id;
next();
} catch (error) {
console.error('์ธ์ฆ ์๋ฌ:', error);
if (error.message.includes('ํ ํฐ')) {
return res.status(401).json({
success: false,
message: error.message
});
}
res.status(500).json({
success: false,
message: '์ธ์ฆ ์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค'
});
}
};
// ์ ํ์ ์ธ์ฆ ๋ฏธ๋ค์จ์ด (๋ก๊ทธ์ธ ์ ํด๋ ๋์ง๋ง, ํ์ผ๋ฉด ์ ๋ณด ์ ๊ณต)
exports.optionalAuthenticate = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return next(); // ํ ํฐ ์์ด๋ ๊ณ์ ์งํ
}
const token = authHeader.substring(7);
const decoded = verifyAccessToken(token);
const user = await User.findById(decoded.userId);
if (user) {
req.user = user;
req.userId = user._id;
}
next();
} catch (error) {
// ์๋ฌ๊ฐ ์์ด๋ ๊ณ์ ์งํ
next();
}
};
์ด ๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ์ ๋ง ๊ฐ๋จํด์:
// routes/protectedRoutes.js
const express = require('express');
const router = express.Router();
const { authenticate } = require('../middleware/authMiddleware');
// ๋ณดํธ๋ ๋ผ์ฐํธ - ๋ก๊ทธ์ธ ํ์
router.get('/profile', authenticate, (req, res) => {
res.json({
success: true,
data: {
user: req.user
}
});
});
// ๋ณดํธ๋ ๋ผ์ฐํธ - ์ฌ์ฉ์ ์ ๋ณด ์์
router.put('/profile', authenticate, async (req, res) => {
try {
const { name } = req.body;
req.user.name = name;
await req.user.save();
res.json({
success: true,
message: 'ํ๋กํ์ด ์
๋ฐ์ดํธ๋์์ต๋๋ค',
data: {
user: req.user
}
});
} catch (error) {
res.status(500).json({
success: false,
message: 'ํ๋กํ ์
๋ฐ์ดํธ ์คํจ'
});
}
});
module.exports = router;
๐ ํ ํฐ ๊ฐฑ์ API ๊ตฌํํ๊ธฐ
Access Token์ ๋ณด์์ ์ํด ์งง์ ์๋ช ์ ๊ฐ์ง๊ณ ์์ด์. ๊ทธ๋์ ๋ง๋ฃ๋๋ฉด Refresh Token์ ์ฌ์ฉํด์ ์๋ก์ด Access Token์ ๋ฐ๊ธ๋ฐ์์ผ ํด์. ์ด ๊ณผ์ ์ ๊ตฌํํด๋ณผ๊ฒ์! ๐
// controllers/authController.js (๊ณ์)
// ํ ํฐ ๊ฐฑ์
exports.refreshToken = async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({
success: false,
message: '๋ฆฌํ๋ ์ ํ ํฐ์ด ํ์ํฉ๋๋ค'
});
}
// Refresh Token ๊ฒ์ฆ
const decoded = verifyRefreshToken(refreshToken);
// ์ฌ์ฉ์ ์ฐพ๊ธฐ
const user = await User.findById(decoded.userId).select('+refreshToken');
if (!user) {
return res.status(401).json({
success: false,
message: '์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค'
});
}
// DB์ ์ ์ฅ๋ Refresh Token๊ณผ ๋น๊ต
if (user.refreshToken !== refreshToken) {
return res.status(401).json({
success: false,
message: '์ ํจํ์ง ์์ ๋ฆฌํ๋ ์ ํ ํฐ์
๋๋ค'
});
}
// ์๋ก์ด ํ ํฐ ์์ฑ
const newAccessToken = generateAccessToken(user._id);
const newRefreshToken = generateRefreshToken(user._id);
// ์๋ก์ด Refresh Token ์ ์ฅ
user.refreshToken = newRefreshToken;
await user.save();
res.json({
success: true,
message: 'ํ ํฐ์ด ๊ฐฑ์ ๋์์ต๋๋ค',
data: {
accessToken: newAccessToken,
refreshToken: newRefreshToken
}
});
} catch (error) {
console.error('ํ ํฐ ๊ฐฑ์ ์๋ฌ:', error);
if (error.message.includes('ํ ํฐ')) {
return res.status(401).json({
success: false,
message: error.message
});
}
res.status(500).json({
success: false,
message: 'ํ ํฐ ๊ฐฑ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค'
});
}
};
์๋ฅผ ๋ค์ด Axios ์ธํฐ์ ํฐ๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋ฐ ๋ก์ง์ ๊น๋ํ๊ฒ ๊ตฌํํ ์ ์์ด์.
๐ช ๋ก๊ทธ์์ API ๊ตฌํํ๊ธฐ
๋ก๊ทธ์์์ ๊ฐ๋จํด ๋ณด์ด์ง๋ง ์ ๋๋ก ๊ตฌํํ๋ ๊ฒ ์ค์ํด์. Refresh Token์ ๋ฌดํจํํด์ ๋ณด์์ ๊ฐํํ ์ ์๊ฑฐ๋ ์! ๐
// controllers/authController.js (๊ณ์)
// ๋ก๊ทธ์์
exports.logout = async (req, res) => {
try {
const userId = req.userId; // ๋ฏธ๋ค์จ์ด์์ ์ค์ ๋จ
// Refresh Token ์ ๊ฑฐ
await User.findByIdAndUpdate(userId, {
refreshToken: null
});
res.json({
success: true,
message: '๋ก๊ทธ์์๋์์ต๋๋ค'
});
} catch (error) {
console.error('๋ก๊ทธ์์ ์๋ฌ:', error);
res.status(500).json({
success: false,
message: '๋ก๊ทธ์์ ์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค'
});
}
};
// ๋ชจ๋ ๊ธฐ๊ธฐ์์ ๋ก๊ทธ์์ (๋ณด์ ๊ฐํ)
exports.logoutAll = async (req, res) => {
try {
const userId = req.userId;
// ๋ชจ๋ Refresh Token ์ ๊ฑฐ
await User.findByIdAndUpdate(userId, {
refreshToken: null
});
res.json({
success: true,
message: '๋ชจ๋ ๊ธฐ๊ธฐ์์ ๋ก๊ทธ์์๋์์ต๋๋ค'
});
} catch (error) {
console.error('์ ์ฒด ๋ก๊ทธ์์ ์๋ฌ:', error);
res.status(500).json({
success: false,
message: '๋ก๊ทธ์์ ์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค'
});
}
};
๐บ๏ธ ๋ผ์ฐํธ ์ค์ ํ๊ธฐ
์ด์ ๋ชจ๋ ๊ธฐ๋ฅ์ ๋ผ์ฐํธ๋ก ์ฐ๊ฒฐํด๋ณผ๊ฒ์. Express ๋ผ์ฐํฐ๋ฅผ ์ฌ์ฉํ๋ฉด ๊น๋ํ๊ฒ ์ ๋ฆฌํ ์ ์์ด์! ๐ฏ
// routes/authRoutes.js
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
const { authenticate } = require('../middleware/authMiddleware');
// ๊ณต๊ฐ ๋ผ์ฐํธ
router.post('/register', authController.register);
router.post('/login', authController.login);
router.post('/refresh-token', authController.refreshToken);
// ๋ณดํธ๋ ๋ผ์ฐํธ (์ธ์ฆ ํ์)
router.post('/logout', authenticate, authController.logout);
router.post('/logout-all', authenticate, authController.logoutAll);
module.exports = router;
๐ ์๋ฒ ์ค์ ๋ฐ ์คํ
๋ง์ง๋ง์ผ๋ก ๋ชจ๋ ๊ฑธ ํ๋๋ก ํฉ์ณ์ ์๋ฒ๋ฅผ ์คํํด๋ณผ๊ฒ์! ์ด์ ๊ฑฐ์ ๋ค ์์ด์. ๐
// server.js
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const authRoutes = require('./routes/authRoutes');
const protectedRoutes = require('./routes/protectedRoutes');
const app = express();
// ๋ฏธ๋ค์จ์ด
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// CORS ์ค์ (ํ์ํ ๊ฒฝ์ฐ)
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
next();
});
// MongoDB ์ฐ๊ฒฐ
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('โ
MongoDB ์ฐ๊ฒฐ ์ฑ๊ณต'))
.catch(err => console.error('โ MongoDB ์ฐ๊ฒฐ ์คํจ:', err));
// ๋ผ์ฐํธ ๋ฑ๋ก
app.use('/api/auth', authRoutes);
app.use('/api', protectedRoutes);
// ๊ธฐ๋ณธ ๋ผ์ฐํธ
app.get('/', (req, res) => {
res.json({
message: 'JWT ์ธ์ฆ API ์๋ฒ',
version: '1.0.0',
endpoints: {
auth: '/api/auth',
protected: '/api'
}
});
});
// 404 ์๋ฌ ํธ๋ค๋ง
app.use((req, res) => {
res.status(404).json({
success: false,
message: '์์ฒญํ ๋ฆฌ์์ค๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค'
});
});
// ์๋ฌ ํธ๋ค๋ง ๋ฏธ๋ค์จ์ด
app.use((err, req, res, next) => {
console.error('์๋ฒ ์๋ฌ:', err);
res.status(500).json({
success: false,
message: '์๋ฒ ๋ด๋ถ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
// ์๋ฒ ์์
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`๐ ์๋ฒ๊ฐ ํฌํธ ${PORT}์์ ์คํ ์ค์
๋๋ค`);
});
์๋ฒ๋ฅผ ์คํํ๋ ค๋ฉด ํฐ๋ฏธ๋์์ ์ด๋ ๊ฒ ์ ๋ ฅํ์ธ์:
npm run dev
package.json์ ์ด ์คํฌ๋ฆฝํธ๋ฅผ ์ถ๊ฐํด๋๋ฉด ํธํด์:
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
๐ป ํด๋ผ์ด์ธํธ ์ธก ๊ตฌํ ์์
์๋ฒ๋ ์์ฑํ์ผ๋ ์ด์ ํด๋ผ์ด์ธํธ์์ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง ๋ณผ๊น์? JavaScript์ Axios๋ฅผ ์ฌ์ฉํ ์์ ๋ฅผ ๋ณด์ฌ๋๋ฆด๊ฒ์! ๐จ
// auth.js - ํด๋ผ์ด์ธํธ ์ธก ์ธ์ฆ ์ ํธ๋ฆฌํฐ
class AuthService {
constructor() {
this.API_URL = 'http://localhost:5000/api';
this.accessToken = localStorage.getItem('accessToken');
this.refreshToken = localStorage.getItem('refreshToken');
}
// ํ์๊ฐ์
async register(email, password, name) {
try {
const response = await fetch(`${this.API_URL}/auth/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password, name })
});
const data = await response.json();
if (data.success) {
this.setTokens(data.data.accessToken, data.data.refreshToken);
return { success: true, user: data.data.user };
}
return { success: false, message: data.message };
} catch (error) {
console.error('ํ์๊ฐ์
์๋ฌ:', error);
return { success: false, message: '๋คํธ์ํฌ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค' };
}
}
// ๋ก๊ทธ์ธ
async login(email, password) {
try {
const response = await fetch(`${this.API_URL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (data.success) {
this.setTokens(data.data.accessToken, data.data.refreshToken);
return { success: true, user: data.data.user };
}
return { success: false, message: data.message };
} catch (error) {
console.error('๋ก๊ทธ์ธ ์๋ฌ:', error);
return { success: false, message: '๋คํธ์ํฌ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค' };
}
}
// ํ ํฐ ์ ์ฅ
setTokens(accessToken, refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
}
// ํ ํฐ ๊ฐฑ์
async refreshAccessToken() {
try {
const response = await fetch(`${this.API_URL}/auth/refresh-token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refreshToken: this.refreshToken })
});
const data = await response.json();
if (data.success) {
this.setTokens(data.data.accessToken, data.data.refreshToken);
return true;
}
// Refresh Token๋ ๋ง๋ฃ๋จ - ์ฌ๋ก๊ทธ์ธ ํ์
this.logout();
return false;
} catch (error) {
console.error('ํ ํฐ ๊ฐฑ์ ์๋ฌ:', error);
return false;
}
}
// ์ธ์ฆ๋ API ์์ฒญ
async authenticatedRequest(url, options = {}) {
try {
// ์ฒซ ๋ฒ์งธ ์๋
let response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${this.accessToken}`
}
});
// 401 ์๋ฌ๋ฉด ํ ํฐ ๊ฐฑ์ ํ ์ฌ์๋
if (response.status === 401) {
const refreshed = await this.refreshAccessToken();
if (refreshed) {
// ํ ํฐ ๊ฐฑ์ ์ฑ๊ณต - ์ฌ์๋
response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${this.accessToken}`
}
});
} else {
// ํ ํฐ ๊ฐฑ์ ์คํจ - ๋ก๊ทธ์ธ ํ์ด์ง๋ก
window.location.href = '/login';
return null;
}
}
return await response.json();
} catch (error) {
console.error('API ์์ฒญ ์๋ฌ:', error);
return null;
}
}
// ๋ก๊ทธ์์
async logout() {
try {
await fetch(`${this.API_URL}/auth/logout`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.accessToken}`
}
});
} catch (error) {
console.error('๋ก๊ทธ์์ ์๋ฌ:', error);
} finally {
// ๋ก์ปฌ ํ ํฐ ์ ๊ฑฐ
this.accessToken = null;
this.refreshToken = null;
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
}
}
// ๋ก๊ทธ์ธ ์ํ ํ์ธ
isAuthenticated() {
return !!this.accessToken;
}
}
// ์ ์ญ ์ธ์คํด์ค ์์ฑ
const authService = new AuthService();
์ด์ ์ค์ ๋ก ์ฌ์ฉํ๋ ์์ ๋ฅผ ๋ณผ๊น์:
// ์ฌ์ฉ ์์
// ๋ก๊ทธ์ธ ํผ ์ฒ๋ฆฌ
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
const result = await authService.login(email, password);
if (result.success) {
alert('๋ก๊ทธ์ธ ์ฑ๊ณต!');
window.location.href = '/dashboard';
} else {
alert(result.message);
}
});
// ํ๋กํ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
async function loadProfile() {
const data = await authService.authenticatedRequest(
'http://localhost:5000/api/profile',
{ method: 'GET' }
);
if (data && data.success) {
document.getElementById('userName').textContent = data.data.user.name;
document.getElementById('userEmail').textContent = data.data.user.email;
}
}
// ๋ก๊ทธ์์ ๋ฒํผ
document.getElementById('logoutBtn').addEventListener('click', () => {
authService.logout();
});
๐ ๋ณด์ ๊ฐํ ํ
๊ธฐ๋ณธ ๊ตฌํ์ ์๋ฃํ์ง๋ง, ์ค์ ํ๋ก๋์ ํ๊ฒฝ์์๋ ๋ ๋ง์ ๋ณด์ ์กฐ์น๊ฐ ํ์ํด์. ์ค์ํ ๋ณด์ ํ๋ค์ ์๋ ค๋๋ฆด๊ฒ์! ๐ก๏ธ
ํ ํฐ์ด ํ๋ฌธ์ผ๋ก ์ ์ก๋๊ธฐ ๋๋ฌธ์ HTTPS๋ ํ์์์. HTTP๋ก๋ ์ ๋ ์ด์ํ๋ฉด ์ ๋ฉ๋๋ค!
2. XSS ๋ฐฉ์ด
์ฌ์ฉ์ ์ ๋ ฅ์ ๊ทธ๋๋ก ๋ ๋๋งํ์ง ๋ง๊ณ ํญ์ ์ด์ค์ผ์ดํ ์ฒ๋ฆฌํ์ธ์. React๋ Vue ๊ฐ์ ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ๋ฉด ๊ธฐ๋ณธ์ ์ผ๋ก ๋ณดํธ๋ฉ๋๋ค.
3. CSRF ๋ฐฉ์ด
์ฟ ํค์ ํ ํฐ์ ์ ์ฅํ๋ค๋ฉด CSRF ํ ํฐ๋ ํจ๊ป ์ฌ์ฉํด์ผ ํด์.
4. Rate Limiting
๋ฌด์ฐจ๋ณ ๋์ ๊ณต๊ฒฉ์ ๋ง๊ธฐ ์ํด ๋ก๊ทธ์ธ ์๋ ํ์๋ฅผ ์ ํํ์ธ์.
// Rate Limiting ์์ (express-rate-limit ์ฌ์ฉ)
const rateLimit = require('express-rate-limit');
// ๋ก๊ทธ์ธ API์ Rate Limiting ์ ์ฉ
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ
max: 5, // ์ต๋ 5๋ฒ ์๋
message: {
success: false,
message: '๋๋ฌด ๋ง์ ๋ก๊ทธ์ธ ์๋๊ฐ ์์์ต๋๋ค. 15๋ถ ํ์ ๋ค์ ์๋ํด์ฃผ์ธ์.'
},
standardHeaders: true,
legacyHeaders: false,
});
// ๋ผ์ฐํธ์ ์ ์ฉ
router.post('/login', loginLimiter, authController.login);
๋ก๊ทธ์ธ ์ IP ์ฃผ์๋ฅผ ๊ธฐ๋กํด์ ์ด์ํ ์ ์์ ๊ฐ์งํ ์ ์์ด์.
๋๋ฐ์ด์ค ํ๊ฑฐํ๋ฆฐํ
์ฌ์ฉ์์ ๋ธ๋ผ์ฐ์ , OS ์ ๋ณด๋ฅผ ๊ธฐ๋กํด์ ์๋ก์ด ๊ธฐ๊ธฐ์์ ๋ก๊ทธ์ธ ์ ์๋ฆผ์ ๋ณด๋ผ ์ ์์ด์.
2๋จ๊ณ ์ธ์ฆ (2FA)
SMS๋ OTP ์ฑ์ ํตํ ์ถ๊ฐ ์ธ์ฆ์ ๊ตฌํํ๋ฉด ๋ณด์์ด ํฌ๊ฒ ๊ฐํ๋ฉ๋๋ค.
๋น๋ฐ๋ฒํธ ์ ์ฑ
์ต์ ๊ธธ์ด, ํน์๋ฌธ์ ํฌํจ ๋ฑ์ ๊ท์น์ ์ ์ฉํ์ธ์.
๐ ์ผ๋ฐ์ ์ธ ๋ฌธ์ ์ ํด๊ฒฐ๋ฒ
JWT ์ธ์ฆ์ ๊ตฌํํ๋ค ๋ณด๋ฉด ์์ฃผ ๋ง์ฃผ์น๋ ๋ฌธ์ ๋ค์ด ์์ด์. ๋ฏธ๋ฆฌ ์์๋๋ฉด ์๊ฐ์ ๋ง์ด ์ ์ฝํ ์ ์์ฃ ! ๐
์์ธ: JWT_SECRET์ด ์๋ฒ์ ํด๋ผ์ด์ธํธ์์ ๋ค๋ฅด๊ฑฐ๋, ํ ํฐ์ด ๋ณ์กฐ๋์์ด์.
ํด๊ฒฐ: .env ํ์ผ์ JWT_SECRET์ ํ์ธํ๊ณ , ์๋ฒ ์ฌ์์ ํ ์๋ก์ด ํ ํฐ์ ๋ฐ๊ธ๋ฐ์ผ์ธ์.
์์ธ: Access Token์ด ๋ง๋ฃ๋์์ด์.
ํด๊ฒฐ: Refresh Token์ ์ฌ์ฉํด์ ์๋ก์ด Access Token์ ๋ฐ๊ธ๋ฐ์ผ์ธ์. ํด๋ผ์ด์ธํธ์์ ์๋์ผ๋ก ์ฒ๋ฆฌํ๋๋ก ๊ตฌํํ๋ ๊ฒ ์ข์์.
์์ธ: ์๋ฒ์์ CORS ์ค์ ์ด ์ ๋์ด ์์ด์.
ํด๊ฒฐ: cors ๋ฏธ๋ค์จ์ด๋ฅผ ์ค์นํ๊ณ ์ค์ ํ์ธ์:
npm install cors
// server.js
const cors = require('cors');
app.use(cors({
origin: 'http://localhost:3000', // ํด๋ผ์ด์ธํธ ์ฃผ์
credentials: true
}));
์์ธ: Payload์ ๋๋ฌด ๋ง์ ๋ฐ์ดํฐ๋ฅผ ๋ฃ์์ด์.
ํด๊ฒฐ: JWT์๋ ์ต์ํ์ ์ ๋ณด๋ง ๋ฃ์ผ์ธ์. ์ฌ์ฉ์ ID ์ ๋๋ง ๋ฃ๊ณ , ๋๋จธ์ง๋ ์๋ฒ์์ ์กฐํํ๋ ๊ฒ ์ข์์.
๐ JWT vs ์ธ์ ๋น๊ต
JWT๋ฅผ ์ฌ์ฉํ ์ง ์ ํต์ ์ธ ์ธ์ ์ ์ฌ์ฉํ ์ง ๊ณ ๋ฏผ๋์๋์? ๊ฐ๊ฐ์ ์ฅ๋จ์ ์ ๋น๊ตํด๋ณผ๊ฒ์! ๐ค
| ํน์ง | JWT | ์ธ์ |
|---|---|---|
| ์ ์ฅ ์์น | ํด๋ผ์ด์ธํธ (LocalStorage, Cookie) | ์๋ฒ (๋ฉ๋ชจ๋ฆฌ, Redis, DB) |
| ํ์ฅ์ฑ | โญโญโญโญโญ ๋งค์ฐ ์ข์ | โญโญโญ ๋ณดํต |
| ๋ณด์ | โญโญโญ ๋ณดํต (XSS ์ทจ์ฝ) | โญโญโญโญ ์ข์ |
| ์๋ฒ ๋ถํ | โญโญโญโญโญ ๋งค์ฐ ๋ฎ์ | โญโญโญ ๋ณดํต |
| ์ฆ์ ๋ฌดํจํ | โญโญ ์ด๋ ค์ | โญโญโญโญโญ ์ฌ์ |
| ๋ชจ๋ฐ์ผ ์ฑ | โญโญโญโญโญ ๋งค์ฐ ์ ํฉ | โญโญโญ ๋ณดํต |
| ๊ตฌํ ๋ณต์ก๋ | โญโญโญ ๋ณดํต | โญโญ ์ฌ์ |
โข ๋ชจ๋ฐ์ผ ์ฑ ๋ฐฑ์๋
โข ์๋ฒ ํ์ฅ์ด ์์ฃผ ํ์ํ ๊ฒฝ์ฐ
โข API ์๋ฒ (RESTful API)
โข ์๋ฒ๋ฆฌ์ค ํ๊ฒฝ
๐ฏ ์ธ์ ์ธ์ ์ ์ฌ์ฉํ ๊น?
โข ์ ํต์ ์ธ ์น ์ ํ๋ฆฌ์ผ์ด์
โข ์ฆ์ ๋ก๊ทธ์์์ด ์ค์ํ ๊ฒฝ์ฐ
โข ์๋ฒ ๋ฆฌ์์ค๊ฐ ์ถฉ๋ถํ ๊ฒฝ์ฐ
โข ๋ณด์์ด ์ต์ฐ์ ์ธ ๊ฒฝ์ฐ
๐ ์ฑ๋ฅ ์ต์ ํ ํ
JWT ์ธ์ฆ ์์คํ ์ ์ฑ๋ฅ์ ๋ ๋์ด์ฌ๋ฆฌ๋ ๋ฐฉ๋ฒ๋ค์ ์์๋ณผ๊ฒ์! ์ค๋ฌด์์ ์ ๋ง ์ ์ฉํ ํ๋ค์ด์์. โก
๋ก๊ทธ์์ํ ํ ํฐ์ ๋ธ๋๋ฆฌ์คํธ์ ์ถ๊ฐํด์ ์ฆ์ ๋ฌดํจํํ ์ ์์ด์. Redis๋ ๋น ๋ฅด๊ณ ๋ง๋ฃ ์๊ฐ ์ค์ ์ด ์ฌ์์ ์๋ฒฝํด์!
const redis = require('redis');
const client = redis.createClient();
// ํ ํฐ ๋ธ๋๋ฆฌ์คํธ์ ์ถ๊ฐ
async function blacklistToken(token, expiresIn) {
await client.setEx(`blacklist:${token}`, expiresIn, 'true');
}
// ํ ํฐ์ด ๋ธ๋๋ฆฌ์คํธ์ ์๋์ง ํ์ธ
async function isTokenBlacklisted(token) {
const result = await client.get(`blacklist:${token}`);
return result !== null;
}
JWT๋ Base64๋ก ์ธ์ฝ๋ฉ๋์ด ๋งค ์์ฒญ๋ง๋ค ์ ์ก๋๋ฏ๋ก, ํฌ๊ธฐ๊ฐ ์์์๋ก ์ข์์. ํ์ํ ์ต์ํ์ ์ ๋ณด๋ง ๋ฃ์ผ์ธ์!
โ ๋์ ์:
{
userId: "12345",
email: "user@example.com",
name: "ํ๊ธธ๋",
address: "์์ธ์...",
phoneNumber: "010-1234-5678",
preferences: {...}
}
โ
์ข์ ์:{
userId: "12345",
role: "user"
}
๋งค ์์ฒญ๋ง๋ค ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์กฐํํ๋ฉด DB ๋ถํ๊ฐ ์ปค์ ธ์. ์บ์ฑ์ ํ์ฉํ์ธ์!
// ๊ฐ๋จํ ๋ฉ๋ชจ๋ฆฌ ์บ์ ์์
const userCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5๋ถ
async function getUserById(userId) {
// ์บ์ ํ์ธ
const cached = userCache.get(userId);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.user;
}
// DB ์กฐํ
const user = await User.findById(userId);
// ์บ์ ์ ์ฅ
userCache.set(userId, {
user,
timestamp: Date.now()
});
return user;
}
๐งช ํ ์คํธ ์ฝ๋ ์์ฑํ๊ธฐ
์ข์ ์ฝ๋๋ ํ ์คํธ๊ฐ ์๋ ์ฝ๋์ฃ ! JWT ์ธ์ฆ ์์คํ ์ ํ ์คํธํ๋ ๋ฐฉ๋ฒ์ ์์๋ณผ๊ฒ์. Jest์ Supertest๋ฅผ ์ฌ์ฉํ ๊ฑฐ์์. ๐งช
// __tests__/auth.test.js
const request = require('supertest');
const app = require('../server');
const User = require('../models/User');
const mongoose = require('mongoose');
describe('์ธ์ฆ API ํ
์คํธ', () => {
let accessToken;
let refreshToken;
// ํ
์คํธ ์ DB ์ฐ๊ฒฐ
beforeAll(async () => {
await mongoose.connect(process.env.TEST_MONGODB_URI);
});
// ๊ฐ ํ
์คํธ ํ DB ์ ๋ฆฌ
afterEach(async () => {
await User.deleteMany({});
});
// ํ
์คํธ ํ DB ์ฐ๊ฒฐ ์ข
๋ฃ
afterAll(async () => {
await mongoose.connection.close();
});
describe('POST /api/auth/register', () => {
it('ํ์๊ฐ์
์ฑ๊ณต', async () => {
const response = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'password123',
name: 'ํ
์คํธ์ ์ '
});
expect(response.status).toBe(201);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('accessToken');
expect(response.body.data).toHaveProperty('refreshToken');
expect(response.body.data.user.email).toBe('test@example.com');
});
it('์ค๋ณต ์ด๋ฉ์ผ๋ก ํ์๊ฐ์
์คํจ', async () => {
// ์ฒซ ๋ฒ์งธ ํ์๊ฐ์
await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'password123',
name: 'ํ
์คํธ์ ์ '
});
// ๊ฐ์ ์ด๋ฉ์ผ๋ก ๋ ๋ฒ์งธ ์๋
const response = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'password456',
name: 'ํ
์คํธ์ ์ 2'
});
expect(response.status).toBe(409);
expect(response.body.success).toBe(false);
});
});
describe('POST /api/auth/login', () => {
beforeEach(async () => {
// ํ
์คํธ์ฉ ์ฌ์ฉ์ ์์ฑ
await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'password123',
name: 'ํ
์คํธ์ ์ '
});
});
it('๋ก๊ทธ์ธ ์ฑ๊ณต', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('accessToken');
accessToken = response.body.data.accessToken;
refreshToken = response.body.data.refreshToken;
});
it('์๋ชป๋ ๋น๋ฐ๋ฒํธ๋ก ๋ก๊ทธ์ธ ์คํจ', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'wrongpassword'
});
expect(response.status).toBe(401);
expect(response.body.success).toBe(false);
});
});
describe('GET /api/profile', () => {
beforeEach(async () => {
// ๋ก๊ทธ์ธํด์ ํ ํฐ ๋ฐ๊ธฐ
const response = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'password123',
name: 'ํ
์คํธ์ ์ '
});
accessToken = response.body.data.accessToken;
});
it('์ธ์ฆ๋ ์ฌ์ฉ์ ํ๋กํ ์กฐํ ์ฑ๊ณต', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${accessToken}`);
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data.user.email).toBe('test@example.com');
});
it('ํ ํฐ ์์ด ํ๋กํ ์กฐํ ์คํจ', async () => {
const response = await request(app)
.get('/api/profile');
expect(response.status).toBe(401);
expect(response.body.success).toBe(false);
});
});
});
ํ ์คํธ๋ฅผ ์คํํ๋ ค๋ฉด:
npm install --save-dev jest supertest
npm test
๐ ์ค์ ํ์ฉ ์๋๋ฆฌ์ค
์ด์ ๋ฐฐ์ด ๋ด์ฉ์ ์ค์ ํ๋ก์ ํธ์ ์ด๋ป๊ฒ ์ ์ฉํ ์ ์๋์ง ์์๋ณผ๊ฒ์! ๋ค์ํ ์๋๋ฆฌ์ค๋ฅผ ์ค๋นํ์ด์. ๐ฏ
// ๋๋ฐ์ด์ค๋ณ Refresh Token ๊ด๋ฆฌ
const userSchema = new mongoose.Schema({
email: String,
password: String,
name: String,
devices: [{
deviceId: String,
deviceName: String,
refreshToken: String,
lastLogin: Date
}]
});
// ๋ก๊ทธ์ธ ์ ๋๋ฐ์ด์ค ์ ๋ณด ์ ์ฅ
exports.login = async (req, res) => {
const { email, password, deviceId, deviceName } = req.body;
// ... ์ธ์ฆ ๋ก์ง ...
const accessToken = generateAccessToken(user._id);
const refreshToken = generateRefreshToken(user._id);
// ๋๋ฐ์ด์ค ์ ๋ณด ์
๋ฐ์ดํธ
const deviceIndex = user.devices.findIndex(d => d.deviceId === deviceId);
if (deviceIndex >= 0) {
user.devices[deviceIndex].refreshToken = refreshToken;
user.devices[deviceIndex].lastLogin = new Date();
} else {
user.devices.push({
deviceId,
deviceName,
refreshToken,
lastLogin: new Date()
});
}
await user.save();
res.json({ accessToken, refreshToken });
};
// Google OAuth ๋ก๊ทธ์ธ ์์
const { OAuth2Client } = require('google-auth-library');
const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
exports.googleLogin = async (req, res) => {
try {
const { token } = req.body;
// Google ํ ํฐ ๊ฒ์ฆ
const ticket = await client.verifyIdToken({
idToken: token,
audience: process.env.GOOGLE_CLIENT_ID
});
const payload = ticket.getPayload();
const { email, name, picture } = payload;
// ์ฌ์ฉ์ ์ฐพ๊ธฐ ๋๋ ์์ฑ
let user = await User.findOne({ email });
if (!user) {
user = await User.create({
email,
name,
profilePicture: picture,
authProvider: 'google'
});
}
// JWT ํ ํฐ ์์ฑ
const accessToken = generateAccessToken(user._id);
const refreshToken = generateRefreshToken(user._id);
res.json({
success: true,
data: { user, accessToken, refreshToken }
});
} catch (error) {
res.status(401).json({
success: false,
message: 'Google ๋ก๊ทธ์ธ ์คํจ'
});
}
};
// ์ญํ ๊ธฐ๋ฐ ๋ฏธ๋ค์จ์ด
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: '์ธ์ฆ์ด ํ์ํฉ๋๋ค'
});
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: '๊ถํ์ด ์์ต๋๋ค'
});
}
next();
};
};
// ์ฌ์ฉ ์์
router.delete('/users/:id',
authenticate,
authorize('admin'),
deleteUser
);
router.post('/posts',
authenticate,
authorize('user', 'admin'),
createPost
);
๐ ์ถ๊ฐ ํ์ต ์๋ฃ
JWT ์ธ์ฆ์ ๋ ๊น์ด ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด ์ด๋ฐ ๊ฒ๋ค์ ์์๋ณด๋ฉด ์ข์์! ๊ณ์ ๋ฐ์ ํ๋ ๊ฐ๋ฐ์๊ฐ ๋๋ ค๋ฉด ํ์ต์ด ์ค์ํ์ฃ . ๐
๋ ๋ณต์กํ ์ธ์ฆ ์๋๋ฆฌ์ค๋ฅผ ์ํ ํ์ค ํ๋กํ ์ฝ์ด์์.
2. Passport.js
Node.js์์ ๊ฐ์ฅ ์ธ๊ธฐ ์๋ ์ธ์ฆ ๋ฏธ๋ค์จ์ด์์. ๋ค์ํ ์ ๋ต์ ์ฝ๊ฒ ๊ตฌํํ ์ ์์ด์.
3. JWT ๋ณด์ ๋ชจ๋ฒ ์ฌ๋ก
OWASP ๊ฐ์ด๋๋ผ์ธ์ ์ฐธ๊ณ ํ๋ฉด ์ข์์.
4. ๋ง์ดํฌ๋ก์๋น์ค ์ธ์ฆ
์ฌ๋ฌ ์๋น์ค ๊ฐ ์ธ์ฆ์ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ง ์์๋ณด์ธ์.
5. WebAuthn
๋น๋ฐ๋ฒํธ ์๋ ์ธ์ฆ์ ๋ฏธ๋์์!
ํ๋ก๊ทธ๋๋ฐ ์ค๋ ฅ์ ๋ ํค์ฐ๊ณ ์ถ๊ฑฐ๋ ์ค์ ํ๋ก์ ํธ ๊ฒฝํ์ด ํ์ํ๋ค๋ฉด, ์ฌ๋ฅ๋ท์์ ๋ค์ํ ๊ฐ๋ฐ ๋ฉํ ๋ค์ ๋ง๋๋ณผ ์ ์์ด์. ์ค๋ฌด ๊ฒฝํ์ด ํ๋ถํ ์ ๋ฌธ๊ฐ๋ค์ด ์ฌ๋ฌ๋ถ์ ์ฑ์ฅ์ ๋์์ค ๊ฑฐ์์! ๐
๐ฏ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ค๋๋ผ ์ ๋ง ์๊ณ ๋ง์ผ์ จ์ด์! ๐ JWT ์ธ์ฆ ์์คํ ์ ์ฒ์๋ถํฐ ๋๊น์ง ๊ตฌํํด๋ดค๋๋ฐ, ์ด๋ ์ จ๋์?
์ฒ์์๋ ๋ณต์กํด ๋ณด์์ง๋ง ํ๋์ฉ ๋ฏ์ด๋ณด๋ ๊ทธ๋ ๊ฒ ์ด๋ ต์ง ์์ฃ ? ํต์ฌ์ ์ด๊ฑฐ์์:
2๏ธโฃ ํด๋ผ์ด์ธํธ๊ฐ ํ ํฐ์ ์ ์ฅํ๊ณ API ์์ฒญ ์ ํจ๊ป ์ ์ก
3๏ธโฃ ์๋ฒ๊ฐ ํ ํฐ์ ๊ฒ์ฆํ๊ณ ์์ฒญ ์ฒ๋ฆฌ
4๏ธโฃ ํ ํฐ์ด ๋ง๋ฃ๋๋ฉด Refresh Token์ผ๋ก ๊ฐฑ์
์ด ๊ธ์์ ๋ค๋ฃฌ ๋ด์ฉ๋ค์ ๋ฐํ์ผ๋ก ์ฌ๋ฌ๋ถ๋ง์ ๋ฉ์ง ์ธ์ฆ ์์คํ ์ ๋ง๋ค์ด๋ณด์ธ์! ์ค์ ๋ก ์ฝ๋๋ฅผ ์์ฑํ๋ฉด์ ๋ถ๋ชํ๋ ๋ฌธ์ ๋ค์ ํด๊ฒฐํ๋ค ๋ณด๋ฉด ์ค๋ ฅ์ด ์ฅ์ฅ ๋ ๊ฑฐ์์. ๐ช
๊ธฐ์ตํ์ธ์:
โข ์๋ฌ๋ฅผ ๋๋ ค์ํ์ง ์๊ธฐ: ์๋ฌ๋ ๋ฐฐ์์ ๊ธฐํ์์
โข ์ฝ๋ ๋ฆฌ๋ทฐ ๋ฐ๊ธฐ: ๋ค๋ฅธ ๊ฐ๋ฐ์์ ํผ๋๋ฐฑ์ ์ ๋ง ์์คํด์
โข ๋ณด์์ ํญ์ ์ผ๋์ ๋๊ธฐ: ํนํ ์ธ์ฆ ์์คํ ์ ๋ณด์์ด ์๋ช ์ด์์
โข ๊ณ์ ํ์ตํ๊ธฐ: ๊ธฐ์ ์ ๊ณ์ ๋ฐ์ ํ๋๊น์!
์ด ๊ธ์ด ์ฌ๋ฌ๋ถ์ ๊ฐ๋ฐ ์ฌ์ ์ ๋์์ด ๋์๊ธฐ๋ฅผ ๋ฐ๋ผ์. ๊ถ๊ธํ ์ ์ด ์๊ฑฐ๋ ๋ ๋ฐฐ์ฐ๊ณ ์ถ์ ๊ฒ ์๋ค๋ฉด ์ฃผ์ ํ์ง ๋ง๊ณ ์ฐพ์๋ณด๊ณ , ๋ฌผ์ด๋ณด๊ณ , ์๋ํด๋ณด์ธ์! ๐
ํดํผ ์ฝ๋ฉ! ๐โจ
๐ ๋ ๋ง์ ๊ฐ๋ฐ ์ง์์ด ํ์ํ์ ๊ฐ์?
์ฌ๋ฅ๋ท์์ ๋ค์ํ ๊ฐ๋ฐ ์ ๋ฌธ๊ฐ๋ค์ ์ฌ๋ฅ์ ๋ง๋๋ณด์ธ์!
ํ๋ก๊ทธ๋๋ฐ, ์น ๊ฐ๋ฐ, ์ฑ ๊ฐ๋ฐ ๋ฑ ๋ค์ํ ๋ถ์ผ์ ๋ฉํ ๋ง๊ณผ ๊ฐ์๊ฐ ์ค๋น๋์ด ์์ต๋๋ค.
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.