๐ก๏ธ ๋ค์ธต ๋ณด์ ์์คํ
1ํธ์์ ๋ค๋ฃฌ JWT ํ ํฐ ์์คํ ์ ๊ธฐ๋ฐ์ผ๋ก, ์ด๋ฒ ํธ์์๋ ์ค์ ์์ ๊ผญ ํ์ํ ๋ณด์ ๊ธฐ๋ฅ๋ค๊ณผ ํ์ฅ ๊ฐ๋ฅํ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค์ ์ดํด๋ณด๊ฒ ์ต๋๋ค. ํ๋์ ์ธ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋จ์ํ ์ธ์ฆ์ ๋์ด ๋ค์ํ ๋ณด์ ์ํ์ ๋ํ ์ข ํฉ์ ์ธ ๋์์ด ํ์ํฉ๋๋ค.
๐ซ Rate Limiting - API ๋จ์ฉ ๋ฐฉ์ง
์ ์ญ Rate Limiting
// index.js์์ ์ ์ญ Rate Limiting ์ค์
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ ์๊ฐ ์ฐฝ
max: 100, // IP๋น ์ต๋ 100ํ ์์ฒญ
message: {
success: false,
message: '๋๋ฌด ๋ง์ ์์ฒญ์
๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.'
},
standardHeaders: true, // `RateLimit-*` ํค๋ ์ถ๊ฐ
legacyHeaders: false, // `X-RateLimit-*` ํค๋ ๋นํ์ฑํ
});
app.use(limiter);
๋ก๊ทธ์ธ ์ ์ฉ Rate Limiting
๋ก๊ทธ์ธ ์๋ํฌ์ธํธ์๋ ๋์ฑ ์๊ฒฉํ ์ ํ์ ์ ์ฉํฉ๋๋ค:
// authRoutes.js์์ ๋ก๊ทธ์ธ ์ ์ฉ ๋ฆฌ๋ฏธํฐ
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ
max: 5, // ์ต๋ 5ํ ์๋
message: {
success: false,
message: '๋๋ฌด ๋ง์ ๋ก๊ทธ์ธ ์๋. 15๋ถ ํ์ ๋ค์ ์๋ํ์ธ์.'
},
skipSuccessfulRequests: true, // ์ฑ๊ณตํ ์์ฒญ์ ์นด์ดํธ์์ ์ ์ธ
});
// ๋ก๊ทธ์ธ ๋ผ์ฐํธ์ ์ ์ฉ
router.post('/login', loginValidator, loginLimiter, authController.login);
๊ณ์ ๋ณ ๋ธ๋ฃจํธํฌ์ค ๋ฐฉ์ง
IP ๊ธฐ๋ฐ ์ ํ๊ณผ ํจ๊ป ๊ณ์ ๋ณ ์ ๊ธ ์์คํ ๋ ๊ตฌํํ์ต๋๋ค:
// User ๋ชจ๋ธ์ ๋ธ๋ฃจํธํฌ์ค ๋ฐฉ์ง ํ๋
const UserSchema = new mongoose.Schema({
loginAttempts: { type: Number, default: 0 },
lockUntil: { type: Number, default: 0 },
// ... ๊ธฐํ ํ๋
});
// ๋ก๊ทธ์ธ ์คํจ ์ฒ๋ฆฌ ๋ก์ง
if (!isMatch) {
user.loginAttempts += 1;
// ๐ 5ํ ์คํจ ์ 30๋ถ ์ ๊ธ
if (user.loginAttempts >= 5) {
user.lockUntil = Date.now() + 30 * 60 * 1000;
user.loginAttempts = 0;
logger.security('๊ณ์ ์ ๊ธ ๋ฐ์', {
userId: user._id,
email: user.email,
ip: req.ip,
attempts: user.loginAttempts
});
}
await user.save();
return res.status(401).json({
success: false,
message: '์ด๋ฉ์ผ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.'
});
}
// โ
๋ก๊ทธ์ธ ์ฑ๊ณต ์ ์ ๊ธ ํด์
user.loginAttempts = 0;
user.lockUntil = undefined;
await user.save();
๐ CSRF ๋ณดํธ - ํฌ๋ก์ค์ฌ์ดํธ ์์ฒญ ์์กฐ ๋ฐฉ์ง
์ ํ์ CSRF ๋ณดํธ
๋ชจ๋ ์๋ํฌ์ธํธ์ CSRF ๋ณดํธ๋ฅผ ์ ์ฉํ๋ฉด ์ฌ์ฉ์ฑ์ด ๋จ์ด์ง ์ ์์ด, ์ ํ์ ๋ณดํธ ์ ๋ต์ ์ฑํํ์ต๋๋ค:
// index.js์์ CSRF ์ค์
const csrf = require('csurf');
const csrfProtection = csrf({
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
}
});
// ๐ซ CSRF ํ ํฐ ์ ๊ณต ์๋ํฌ์ธํธ
app.get('/api/csrf-token', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// ๐ก๏ธ ๋ฏผ๊ฐํ ์์
์๋ง CSRF ๋ณดํธ ์ ์ฉ
// router.post('/register', csrfProtection, registerValidator, authController.register);
// router.post('/login', csrfProtection, loginValidator, loginLimiter, authController.login);
CSRF ์๋ฌ ์ฒ๋ฆฌ
// ์ ์ญ ์๋ฌ ํธ๋ค๋ฌ์์ CSRF ์๋ฌ ์ฒ๋ฆฌ
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
return res.status(403).json({
message: 'CSRF ํ ํฐ์ด ์ ํจํ์ง ์์ต๋๋ค.',
hint: 'CSRF ํ ํฐ์ /api/csrf-token์์ ๋ฐ์์ X-CSRF-TOKEN ํค๋์ ํฌํจํ์ธ์.'
});
}
// ๊ธฐํ ์๋ฌ ์ฒ๋ฆฌ...
});
๐ CORS ์ค์ - ์์ ํ ํฌ๋ก์ค ์ค๋ฆฌ์ง ์์ฒญ
๋์ Origin ๊ฒ์ฆ
// index.js์์ CORS ์ค์
const allowedOrigins = [
'http://localhost:3000',
'http://localhost:8080',
'http://localhost:8081',
process.env.CLIENT_URL
].filter(Boolean);
app.use(cors({
origin: function (origin, callback) {
// ๐ง ๊ฐ๋ฐ ํ๊ฒฝ์์๋ origin์ด undefined์ผ ์ ์์
if (!origin && process.env.NODE_ENV === 'development') {
return callback(null, true);
}
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
console.log('CORS ์ฐจ๋จ๋ ๋๋ฉ์ธ:', origin);
callback(new Error('CORS ์ ์ฑ
์ ์ํด ์ฐจ๋จ๋จ'));
}
},
credentials: true, // ์ฟ ํค ํฌํจ ์์ฒญ ํ์ฉ
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-TOKEN']
}));
๐ค ์์ ๋ก๊ทธ์ธ ๊ตฌํ - Passport.js ์ ๋ต
Passport ์ ๋ต ์ค์
// config/passport.js
const passport = require('passport');
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const KakaoStrategy = require('passport-kakao').Strategy;
const NaverStrategy = require('passport-naver').Strategy;
// ๐ JWT ์ ๋ต
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET
};
passport.use(new JwtStrategy(jwtOptions, async (payload, done) => {
try {
const user = await User.findById(payload.id);
return user ? done(null, user) : done(null, false);
} catch (error) {
return done(error, false);
}
}));
// ๐ก ์นด์นด์ค ๋ก๊ทธ์ธ ์ ๋ต
passport.use(new KakaoStrategy({
clientID: process.env.KAKAO_CLIENT_ID,
callbackURL: process.env.KAKAO_CALLBACK_URL
}, async (accessToken, refreshToken, profile, done) => {
try {
// ๊ธฐ์กด ์ฌ์ฉ์ ์ฐพ๊ธฐ
let user = await User.findOne({ kakaoId: profile.id });
if (!user) {
// ๐ ์ ์ฌ์ฉ์ ์์ฑ
user = await User.create({
name: profile.displayName || profile.username,
email: profile._json.kakao_account.email,
kakaoId: profile.id,
termsAgree: true // ์์
๋ก๊ทธ์ธ ์ ์ฝ๊ด ๋์ ์ฒ๋ฆฌ
});
}
return done(null, user);
} catch (error) {
return done(error, false);
}
}));
// ๐ข ๋ค์ด๋ฒ ๋ก๊ทธ์ธ ์ ๋ต
passport.use(new NaverStrategy({
clientID: process.env.NAVER_CLIENT_ID,
clientSecret: process.env.NAVER_CLIENT_SECRET,
callbackURL: process.env.NAVER_CALLBACK_URL
}, async (accessToken, refreshToken, profile, done) => {
try {
let user = await User.findOne({ naverId: profile.id });
if (!user) {
user = await User.create({
name: profile.displayName,
email: profile.emails[0].value,
naverId: profile.id,
termsAgree: true
});
}
return done(null, user);
} catch (error) {
return done(error, false);
}
}));
์์ ๋ก๊ทธ์ธ ์ฝ๋ฐฑ ์ฒ๋ฆฌ
// authController.js์์ ์นด์นด์ค ์ฝ๋ฐฑ ์ฒ๋ฆฌ
exports.kakaoCallback = async (req, res) => {
try {
const user = req.user;
// ๐ซ ํ ํฐ ์์ฑ
const accessToken = generateAccessToken(user._id);
const refreshToken = generateRefreshToken(user._id);
// ๐พ ๋ฆฌํ๋ ์ ํ ํฐ DB ์ ์ฅ
await RefreshToken.create({
user: user._id,
token: refreshToken,
ip: req.ip,
userAgent: req.get('User-Agent') || 'Unknown',
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
// ๐ช HTTP-only ์ฟ ํค ์ค์
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
// ๐ ํ๋ก ํธ์๋๋ก ๋ฆฌ๋ค์ด๋ ํธ (ํ ํฐ๊ณผ ํจ๊ป)
res.redirect(`${process.env.CLIENT_URL}/?token=${accessToken}&social=kakao`);
} catch (error) {
logger.logError('์นด์นด์ค ๋ก๊ทธ์ธ ์ฝ๋ฐฑ ์ค๋ฅ', error, {
ip: req.ip,
userAgent: req.headers['user-agent']
});
res.redirect(`${process.env.CLIENT_URL}/login?error=kakao_login_failed`);
}
};
โ ํฌ๊ด์ ์ธ ์ ํจ์ฑ ๊ฒ์ฆ
๊ฐ๋ ฅํ ๋น๋ฐ๋ฒํธ ์ ์ฑ
// middleware/validators.js
const passwordStrengthCheck = (value) => {
// ์ต์ 8์, ๋์๋ฌธ์, ์ซ์, ํน์๋ฌธ์ ๊ฐ 1๊ฐ ์ด์
const strongPasswordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
if (!strongPasswordRegex.test(value)) {
throw new Error('๋น๋ฐ๋ฒํธ๋ ์ต์ 8์ ์ด์์ด๋ฉฐ, ๋๋ฌธ์, ์๋ฌธ์, ์ซ์, ํน์๋ฌธ์๋ฅผ ๊ฐ๊ฐ ํ๋ ์ด์ ํฌํจํด์ผ ํฉ๋๋ค.');
}
// ๐ซ ์ผ๋ฐ์ ์ธ ํจํด ์ฐจ๋จ
const commonPatterns = ['123456', 'password', 'qwerty', 'admin', '111111'];
if (commonPatterns.some(pattern => value.toLowerCase().includes(pattern))) {
throw new Error('ํํ ์ฌ์ฉ๋๋ ๋น๋ฐ๋ฒํธ ํจํด์ด ํฌํจ๋์ด ์์ต๋๋ค.');
}
return true;
};
// ๐ซ ๊ธ์ง์ด ๊ฒ์ฌ
const forbiddenWordsCheck = (value) => {
const forbiddenWords = ['๋น์์ด', '์์ค', 'badword'];
if (forbiddenWords.some(word => value.toLowerCase().includes(word))) {
throw new Error('๋ถ์ ์ ํ ๋จ์ด๊ฐ ํฌํจ๋์ด ์์ต๋๋ค.');
}
return true;
};
ํ์๊ฐ์ ์ ํจ์ฑ ๊ฒ์ฆ
const registerValidator = [
body('name')
.notEmpty().withMessage('์ด๋ฆ์ ํ์ ์
๋ ฅ์ฌํญ์
๋๋ค.')
.isLength({ min: 2, max: 50}).withMessage('์ด๋ฆ์ 2~50์ ์ฌ์ด์ฌ์ผ ํฉ๋๋ค.')
.trim()
.escape() // HTML ์ด์ค์ผ์ดํ
.custom(forbiddenWordsCheck),
body('email')
.notEmpty().withMessage('์ด๋ฉ์ผ์ ํ์ ์
๋ ฅ์ฌํญ์
๋๋ค.')
.isEmail().withMessage('์ ํจํ ์ด๋ฉ์ผ ํ์์ด ์๋๋๋ค.')
.normalizeEmail(), // ์ด๋ฉ์ผ ์ ๊ทํ
body('password')
.notEmpty().withMessage('๋น๋ฐ๋ฒํธ๋ ํ์ ์
๋ ฅ์ฌํญ์
๋๋ค.')
.isLength({ min: 8 }).withMessage('๋น๋ฐ๋ฒํธ๋ ์ต์ 8์ ์ด์์ด์ด์ผ ํฉ๋๋ค.')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/)
.withMessage('๋น๋ฐ๋ฒํธ๋ ๋๋ฌธ์, ์๋ฌธ์, ์ซ์, ํน์๋ฌธ์๋ฅผ ๊ฐ๊ฐ ํ๋ ์ด์ ํฌํจํด์ผ ํฉ๋๋ค.')
.custom(passwordStrengthCheck),
body('confirmPassword')
.custom((value, { req }) => {
if (value !== req.body.password) {
throw new Error('๋น๋ฐ๋ฒํธ์ ๋น๋ฐ๋ฒํธ ํ์ธ์ด ์ผ์นํ์ง ์์ต๋๋ค.');
}
return true;
}),
body('phone')
.optional()
.isMobilePhone('ko-KR').withMessage('์ ํจํ ํ๊ตญ ์ ํ๋ฒํธ ํ์์ด ์๋๋๋ค.'),
body('termsAgree')
.isBoolean()
.custom(value => {
if (value !== true) {
throw new Error('์๋น์ค ์ด์ฉ์ ์ํด ์ฝ๊ด์ ๋์ํด์ผ ํฉ๋๋ค.');
}
return true;
}),
validate // ์ ํจ์ฑ ๊ฒ์ฌ ๊ฒฐ๊ณผ ํ์ธ ๋ฏธ๋ค์จ์ด
];
๋ก๊ทธ์ธ ์ด๋ฉ์ผ ์ ๊ทํ
const loginValidator = [
body('email')
.notEmpty().withMessage('์ด๋ฉ์ผ์ ํ์ ์
๋ ฅ์ฌํญ์
๋๋ค.')
.isEmail().withMessage('์ ํจํ ์ด๋ฉ์ผ ํ์์ด ์๋๋๋ค.')
.normalizeEmail({
gmail_remove_dots: false, // Gmail์ ์ ์ ๊ฑฐ ์ํจ
gmail_remove_subaddress: true, // +๋ถ๋ถ ์ ๊ฑฐ
all_lowercase: true, // ์๋ฌธ์ ๋ณํ
gmail_convert_googlemaildotcom: true // googlemail.com → gmail.com
}),
body('password')
.notEmpty().withMessage('๋น๋ฐ๋ฒํธ๋ ํ์ ์
๋ ฅ์ฌํญ์
๋๋ค.'),
validate
];
๐ Winston์ ํ์ฉํ ๊ตฌ์กฐํ๋ ๋ก๊น
๋ก๊ทธ ๋ ๋ฒจ๋ณ ํ์ผ ๋ถ๋ฆฌ
// utils/logger.js
const winston = require('winston');
const { format, transports } = winston;
require('winston-daily-rotate-file');
// ๐ ๋ก๊ทธ ๋๋ ํ ๋ฆฌ ์์ฑ
const logDir = 'logs';
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir);
}
// ๐จ ์ปค์คํ
๋ก๊ทธ ํฌ๋งท
const logFormat = format.printf(({ level, message, timestamp, ...meta }) => {
return `${timestamp} [${level.toUpperCase()}]: ${message} ${
Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''
}`;
});
const logger = winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
format: format.combine(
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.errors({ stack: true }),
logFormat
),
defaultMeta: { service: 'guardian-auth' },
transports: [
// ๐ฅ๏ธ ์ฝ์ ์ถ๋ ฅ
new transports.Console({
format: format.combine(format.colorize(), logFormat)
}),
// ๐ ์ผ๋ฐ ๋ก๊ทธ (์ผ๋ณ ์ํ)
new transports.DailyRotateFile({
filename: path.join(logDir, 'application-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d',
level: 'info'
}),
// โ ์๋ฌ ๋ก๊ทธ (์ฅ๊ธฐ ๋ณด๊ด)
new transports.DailyRotateFile({
filename: path.join(logDir, 'error-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '30d',
level: 'error'
}),
// ๐ ๋ณด์ ์ด๋ฒคํธ ์ ์ฉ ๋ก๊ทธ
new transports.DailyRotateFile({
filename: path.join(logDir, 'security-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '90d',
level: 'info',
format: format.combine(
format.timestamp(),
format.json(),
format((info) => info.security ? info : false)()
)
})
]
});
๋ณด์ ์ด๋ฒคํธ ๋ก๊น
// ์ ์ฉ ๋ก๊น
๋ฉ์๋
logger.logError = (message, error, meta = {}) => {
logger.error(message, {
error: {
message: error.message,
stack: error.stack
},
...meta
});
};
logger.security = (message, meta = {}) => {
logger.info(message, { security: true, ...meta });
};
// ์ฌ์ฉ ์์ - ์์ฌ์ค๋ฌ์ด ํ ํฐ ์ฌ์ฉ
logger.security('์ ํจํ์ง ์์ ๋ฆฌํ๋ ์ ํ ํฐ ์ฌ์ฉ', {
ip: req.ip,
userAgent: req.headers['user-agent'],
tokenFound: !!storedToken,
tokenRevoked: storedToken ? storedToken.isRevoked : false,
timestamp: new Date().toISOString()
});
// ์ฌ์ฉ ์์ - ๊ณ์ ์์ฑ
logger.security('์ฌ์ฉ์ ๊ณ์ ์์ฑ ์ฑ๊ณต', {
userId: user._id.toString(),
email,
ip: req.ip,
userAgent: req.headers['user-agent']
});
๐ง ๋ณด์ ํค๋์ ๋ฏธ๋ค์จ์ด ์ค์
Helmet์ ํตํ ๋ณด์ ํค๋
// index.js์์ Helmet ์ค์
const helmet = require('helmet');
// ๐ก๏ธ ๋ณด์ ํค๋ ์ค์ (๊ฐ์ฅ ๋จผ์ ์ ์ฉ)
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
์์ฒญ ๋ก๊น ๋ฏธ๋ค์จ์ด
// ์์ธํ ์์ฒญ ๋ก๊น
app.use((req, res, next) => {
const startTime = Date.now();
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
console.log('Origin:', req.headers.origin);
console.log('User-Agent:', req.headers['user-agent']);
res.on('finish', () => {
const duration = Date.now() - startTime;
logger.info('์์ฒญ ์๋ฃ', {
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: `${duration}ms`,
ip: req.ip
});
});
next();
});
๐ ํ์ฅ ๊ฐ๋ฅํ ์๋ฌ ์ฒ๋ฆฌ
์ค์ํ๋ ์๋ฌ ํธ๋ค๋ง
// ์ ์ญ ์๋ฌ ํธ๋ค๋ฌ
app.use((err, req, res, next) => {
console.error('์๋ฒ ์๋ฌ:', err);
// CSRF ํ ํฐ ์๋ฌ
if (err.code === 'EBADCSRFTOKEN') {
return res.status(403).json({
message: 'CSRF ํ ํฐ์ด ์ ํจํ์ง ์์ต๋๋ค.',
hint: 'CSRF ํ ํฐ์ /api/csrf-token์์ ๋ฐ์์ X-CSRF-TOKEN ํค๋์ ํฌํจํ์ธ์.'
});
}
// Mongoose ์ ํจ์ฑ ๊ฒ์ฆ ์๋ฌ
if (err.name === 'ValidationError') {
const errors = Object.values(err.errors).map(e => e.message);
return res.status(400).json({
success: false,
message: '์
๋ ฅ ๋ฐ์ดํฐ๊ฐ ์ ํจํ์ง ์์ต๋๋ค.',
errors
});
}
// MongoDB ์ค๋ณต ํค ์๋ฌ
if (err.code === 11000) {
return res.status(400).json({
success: false,
message: '์ด๋ฏธ ์ฌ์ฉ ์ค์ธ ๊ฐ์
๋๋ค.'
});
}
// ๊ธฐ๋ณธ ์๋ฌ ์๋ต
res.status(500).json({
success: false,
message: '์๋ฒ ๋ด๋ถ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.',
error: process.env.NODE_ENV === 'development' ? err.message : '๋ด๋ถ ์๋ฒ ์ค๋ฅ'
});
});
๐ ์ฑ๋ฅ ์ต์ ํ์ ๋ชจ๋ํฐ๋ง
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธ๋ฑ์ค
// User ๋ชจ๋ธ์์ ์ฑ๋ฅ ์ต์ ํ
UserSchema.index({ email: 1 }); // ์ด๋ฉ์ผ ๊ฒ์ ์ต์ ํ
UserSchema.index({ kakaoId: 1 }, { sparse: true }); // ์์
ID ๊ฒ์
UserSchema.index({ naverId: 1 }, { sparse: true });
UserSchema.index({ lastLocation: '2dsphere' }); // ์์น ๊ธฐ๋ฐ ๊ฒ์
UserSchema.index({ createdAt: 1 }); // ์๊ฐ์ ์ ๋ ฌ
// RefreshToken ๋ชจ๋ธ
RefreshTokenSchema.index({ token: 1 }); // ํ ํฐ ๊ฒ์ ์ต์ ํ
RefreshTokenSchema.index({ user: 1 }); // ์ฌ์ฉ์๋ณ ํ ํฐ ์กฐํ
RefreshTokenSchema.index({ expiresAt: 1 }); // ๋ง๋ฃ ํ ํฐ ์ ๋ฆฌ
๐ฏ ๊ฒฐ๋ก
์ด๋ฒ ์ธ์ฆ ์์คํ ๊ตฌ์ถ์ ํตํด ๋ค์๊ณผ ๊ฐ์ ํ๋์ ๋ณด์ ์๊ตฌ์ฌํญ์ ๋ชจ๋ ๋ง์กฑํ๋ ์์คํ ์ ์์ฑํ์ต๋๋ค:
โ ๊ตฌํ๋ ๋ณด์ ๊ธฐ๋ฅ
- ๋ค์ธต ์ธ์ฆ ์์คํ : JWT + Refresh Token
- ๋ธ๋ฃจํธํฌ์ค ๋ฐฉ์ง: Rate Limiting + ๊ณ์ ์ ๊ธ
- CSRF ๋ณดํธ: ์ ํ์ ํ ํฐ ๊ฒ์ฆ
- ์์ ๋ก๊ทธ์ธ: ์์ ํ OAuth ๊ตฌํ
- ์ ๋ ฅ ๊ฒ์ฆ: ํฌ๊ด์ ์ ํจ์ฑ ๊ฒ์ฌ
- ๋ณด์ ๋ก๊น : ๊ตฌ์กฐํ๋ ์ด๋ฒคํธ ์ถ์
๐ ํ์ฅ์ฑ ๊ณ ๋ ค์ฌํญ
- ๋ชจ๋ํ๋ ๊ตฌ์กฐ: ๊ธฐ๋ฅ๋ณ ํ์ผ ๋ถ๋ฆฌ
- ํ๊ฒฝ๋ณ ์ค์ : ๊ฐ๋ฐ/์ด์ ํ๊ฒฝ ๋์
- ์ฑ๋ฅ ์ต์ ํ: ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธ๋ฑ์ฑ
- ๋ชจ๋ํฐ๋ง: ์์ธํ ๋ก๊น ๊ณผ ์๋ฌ ์ถ์
๐ก ์ค์ ์ ์ฉ ํ
- ๋ณด์ ์ ์ฑ ์๋ฆฝ: ๋น๋ฐ๋ฒํธ ๊ฐ๋, ํ ํฐ ๋ง๋ฃ ์๊ฐ ๋ฑ
- ๋ชจ๋ํฐ๋ง ์ฒด๊ณ: ๋ณด์ ์ด๋ฒคํธ ์๋ฆผ ์์คํ ๊ตฌ์ถ
- ์ ๊ธฐ ๋ณด์ ์ ๊ฒ: ๋ก๊ทธ ๋ถ์๊ณผ ์ทจ์ฝ์ ์ ๊ฒ
- ์ฌ์ฉ์ ๊ต์ก: ์์ ํ ๋น๋ฐ๋ฒํธ ์ฌ์ฉ ๊ฐ์ด๋
์ด๋ฌํ ์ฒด๊ณ์ ์ธ ์ ๊ทผ์ ํตํด ์์ ํ๊ณ ํ์ฅ ๊ฐ๋ฅํ ์ธ์ฆ ์์คํ ์ ๊ตฌ์ถํ ์ ์์ผ๋ฉฐ, ์ค์ ์๋น์ค์์ ์๊ตฌ๋๋ ๋ค์ํ ๋ณด์ ์๊ตฌ์ฌํญ์ ํจ๊ณผ์ ์ผ๋ก ๋์ํ ์ ์์ต๋๋ค.
๋ณด์์ ํ ๋ฒ ๊ตฌ์ถํ๊ณ ๋๋๋ ๊ฒ์ด ์๋๋ผ ์ง์์ ์ผ๋ก ๊ฐ์ ํด์ผ ํ๋ ์์ญ์ ๋๋ค. ์๋ก์ด ์ํ์ ๋์ํ๊ณ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํ๊ธฐ ์ํด ๊พธ์คํ ์ ๋ฐ์ดํธ์ ๋ชจ๋ํฐ๋ง์ด ํ์ํฉ๋๋ค.
'MyStory > SafeLink Project' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| Node.js ์ธ์ฆ ์์คํ ๊ตฌ์ถ๊ธฐ (1ํธ) - ์ํคํ ์ฒ์ ํต์ฌ ๊ธฐ์ (2) | 2025.06.09 |
|---|---|
| SafeLink ํ๋ก์ ํธ ์๊ฐ - ์ง์ง ์ฌํด ๋์ ํตํฉ ํ๋ซํผ (7) | 2025.06.09 |