Ver3.0 ๐ JWT(JSON Web Token) ์ธ์ฆ์ ๋ณด์ ๊ณ ๋ ค์ฌํญ: ์์ ํ๊ฒ ํ ํฐ ๋ค๋ฃจ๋ ๋ฒ

๐ JWT(JSON Web Token) ์ธ์ฆ์ ๋ณด์ ๊ณ ๋ ค์ฌํญ: ์์ ํ๊ฒ ํ ํฐ ๋ค๋ฃจ๋ ๋ฒ
๊ฐ๋ฐ์๋ผ๋ฉด ๋ฐ๋์ ์์์ผ ํ JWT ๋ณด์ ํต์ฌ ๊ฐ์ด๋
๐ฏ JWT, ์ ์ด๋ ๊ฒ ๋ง์ด ์ฐ์ผ๊น?
์๋
! ์ค๋์ ์น ๊ฐ๋ฐํ๋ฉด์ ์ ๋ง ์์ฃผ ๋ง์ฃผ์น๋ JWT์ ๋ํด ์ด์ผ๊ธฐํด๋ณผ๊ฒ. ์์ฆ API ๊ฐ๋ฐํ๊ฑฐ๋ ์ธ์ฆ ์์คํ
๊ตฌ์ถํ ๋ JWT ์ ์ฐ๋ ๊ณณ์ด ๊ฑฐ์ ์์ ์ ๋์์? ๐
JWT๋ JSON Web Token์ ์ฝ์๋ก, ์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด๋ฅผ ์์ ํ๊ฒ ์ ๋ฌํ๊ธฐ ์ํ ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ ๋ฐฉ์์ด์ผ. ์ ํต์ ์ธ ์ธ์
๋ฐฉ์๊ณผ ๋ฌ๋ฆฌ ์๋ฒ์ ์ํ๋ฅผ ์ ์ฅํ์ง ์๋ Stateless ํน์ฑ ๋๋ถ์ ํ์ฅ์ฑ์ด ๋ฐ์ด๋๊ณ , ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ
์ฒ์์๋ ์ ๋ง ์ ์ฉํ๊ฒ ์ฐ์ด์ง.
ํ์ง๋ง ์ฌ๊ธฐ์ ์ค์ํ ํฌ์ธํธ! JWT๊ฐ ํธ๋ฆฌํ๋ค๊ณ ํด์ ๋ณด์์ ์ํํ ํ๋ฉด ํฐ์ผ ๋. ์ค์ ๋ก ๋ง์ ๊ฐ๋ฐ์๋ค์ด JWT๋ฅผ ์ฌ์ฉํ๋ฉด์ ๋ณด์ ์ทจ์ฝ์ ์ ๋ง๋ค์ด๋ด๊ณ ์์ด. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ์ฌ์ฉ์ ์ธ์ฆ์ ์ ๋ง ์ค์ํ ๋ถ๋ถ์ด๋๊น, ์ค๋ ์ ๋๋ก ํํค์ณ๋ณด์! ๐ช
๐ JWT์ ๊ธฐ๋ณธ ๊ตฌ์กฐ ์ดํดํ๊ธฐ
JWT๋ฅผ ์ ๋๋ก ๋ณด์ํ๋ ค๋ฉด ๋จผ์ ๊ตฌ์กฐ๋ฅผ ์ ํํ ์์์ผ ํด. JWT๋ ์ (.)์ผ๋ก ๊ตฌ๋ถ๋ ์ธ ๋ถ๋ถ์ผ๋ก ์ด๋ฃจ์ด์ ธ ์์ด:
Header.Payload.Signature
์ด๋ ๊ฒ ์๊ธด ๊ฑฐ์ง. ๊ฐ ๋ถ๋ถ์ ์์ธํ ์ดํด๋ณผ๊น? ๐ง
๐ Header (ํค๋) ํค๋๋ ํ ํฐ์ ํ์ ๊ณผ ํด์ฑ ์๊ณ ๋ฆฌ์ฆ ์ ๋ณด๋ฅผ ๋ด๊ณ ์์ด. ์ผ๋ฐ์ ์ผ๋ก ์ด๋ฐ ํํ์ผ:
{
"alg": "HS256",
"typ": "JWT"
}
์ฌ๊ธฐ์ alg๋ ์๋ช ์ ์ฌ์ฉ๋ ์๊ณ ๋ฆฌ์ฆ์ ์ง์ ํ๋๋ฐ, ์ด๊ฒ ๋ฐ๋ก ์ฒซ ๋ฒ์งธ ๋ณด์ ํฌ์ธํธ์ผ! ๋ง์ ๊ณต๊ฒฉ์ด ์ด ์๊ณ ๋ฆฌ์ฆ ๋ถ๋ถ์ ๋ ธ๋ฆฌ๊ฑฐ๋ . ๐ฑ
๐ Payload (ํ์ด๋ก๋) ํ์ด๋ก๋๋ ์ค์ ์ ๋ฌํ๋ ค๋ ๋ฐ์ดํฐ๋ฅผ ๋ด๋ ๋ถ๋ถ์ด์ผ. ์ฌ์ฉ์ ID, ๊ถํ, ๋ง๋ฃ์๊ฐ ๊ฐ์ ์ ๋ณด๋ค์ด ๋ค์ด๊ฐ์ง:
{
"sub": "1234567890",
"name": "ํ๊ธธ๋",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
์ฌ๊ธฐ์ ์ค์ํ ๊ฑด, ํ์ด๋ก๋๋ ์ํธํ๋์ง ์๊ณ ๋จ์ํ Base64๋ก ์ธ์ฝ๋ฉ๋ง ๋๋ค๋ ๊ฑฐ์ผ! ๋๊ตฌ๋ ๋์ฝ๋ฉํด์ ๋ด์ฉ์ ๋ณผ ์ ์๋ค๋ ๋ป์ด์ง. ๊ทธ๋์ ๋ฏผ๊ฐํ ์ ๋ณด๋ฅผ ๋ฃ์ผ๋ฉด ์ ๋ ์ ๋ผ! ๐ซ
๐ Signature (์๋ช ) ์๋ช ์ ํค๋์ ํ์ด๋ก๋๋ฅผ ํฉ์ณ์ ๋น๋ฐํค๋ก ํด์ฑํ ๊ฐ์ด์ผ. ์ด๊ฒ JWT์ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํ๋ ํต์ฌ ๋ฉ์ปค๋์ฆ์ด์ง:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
์๋ช ๋๋ถ์ ๋๊ตฐ๊ฐ ํ ํฐ ๋ด์ฉ์ ์กฐ์ํ๋ฉด ๋ฐ๋ก ์ ์ ์์ด. ํ์ง๋ง ๋น๋ฐํค๊ฐ ๋ ธ์ถ๋๋ฉด? ๊ฒ์ ์ค๋ฒ์ผ! ๐ฎ
โ ๏ธ ๊ฐ์ฅ ํํ JWT ๋ณด์ ์ทจ์ฝ์ ๋ค
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก JWT ์ฌ์ฉํ ๋ ์กฐ์ฌํด์ผ ํ ๋ณด์ ์ทจ์ฝ์ ๋ค์ ์์๋ณด์. ์ค์ ํ๋ก์ ํธ์์ ์ ๋ง ์์ฃผ ๋ฐ์ํ๋ ๋ฌธ์ ๋ค์ด๋๊น ๊ผญ ์ฒดํฌํด! โ
์ด๊ฑด ์ ๋ง ์ ๋ช
ํ ๊ณต๊ฒฉ ๋ฐฉ๋ฒ์ด์ผ. JWT ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด ํค๋์ alg ํ๋๋ฅผ ์ ๋ขฐํด์ ๋ฐ์ํ๋ ๋ฌธ์ ์ง.
๊ณต๊ฒฉ์๊ฐ ์๊ณ ๋ฆฌ์ฆ์ HS256์์ none์ผ๋ก ๋ฐ๊พธ๋ฉด ์ด๋ป๊ฒ ๋ ๊น?
โ ์ทจ์ฝํ ์ฝ๋ ์์: ์๋ฒ๊ฐ ํค๋์ ์๊ณ ๋ฆฌ์ฆ์ ๊ทธ๋๋ก ๋ฏฟ๊ณ ๊ฒ์ฆํ๋ ๊ฒฝ์ฐ, ๊ณต๊ฒฉ์๋ ์๋ช ์์ด๋ ์ ํจํ ํ ํฐ์ ๋ง๋ค ์ ์์ด!
{
"alg": "none",
"typ": "JWT"
}
{
"sub": "1234567890",
"role": "admin" // ๊ถํ ์์น!
}
// ์๋ช
๋ถ๋ถ ์์!
โ
์์ ํ ๋ฐฉ์ด ๋ฐฉ๋ฒ:
1. ์๋ฒ์์ ํ์ฉํ ์๊ณ ๋ฆฌ์ฆ์ ๋ช
์์ ์ผ๋ก ์ง์ ํ๊ธฐ
2. none ์๊ณ ๋ฆฌ์ฆ ์ ๋ ํ์ฉํ์ง ์๊ธฐ
3. ์๊ณ ๋ฆฌ์ฆ ํ์ดํธ๋ฆฌ์คํธ ๋ฐฉ์ ์ฌ์ฉํ๊ธฐ
// Node.js + jsonwebtoken ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์
jwt.verify(token, secretKey, {
algorithms: ['HS256'] // ๋ช
์์ ์ผ๋ก ์๊ณ ๋ฆฌ์ฆ ์ง์ !
});
์์ ๋งํ๋ฏ์ด JWT์ ํ์ด๋ก๋๋ ์ํธํ๋์ง ์์. ๊ทธ๋ฅ Base64 ์ธ์ฝ๋ฉ๋ง ๋์ด ์์ด์ ๋๊ตฌ๋ ๋์ฝ๋ฉํ ์ ์์ง. ๊ทธ๋ฐ๋ฐ๋ ๋ง์ ๊ฐ๋ฐ์๋ค์ด ์ค์๋ก ๋ฏผ๊ฐํ ์ ๋ณด๋ฅผ ๋ฃ๋ ๊ฒฝ์ฐ๊ฐ ์์ด. ๐ฐ
โ ์ ๋ ํ๋ฉด ์ ๋๋ ๊ฒ๋ค:
โข ๋น๋ฐ๋ฒํธ๋ ๋น๋ฐ๋ฒํธ ํด์๊ฐ
โข ์ ์ฉ์นด๋ ์ ๋ณด
โข ์ฃผ๋ฏผ๋ฑ๋ก๋ฒํธ๋ ๊ฐ์ธ์๋ณ์ ๋ณด
โข API ์ํฌ๋ฆฟ ํค
โข ๋ด๋ถ ์์คํ
๊ฒฝ๋ก๋ ์ค์ ์ ๋ณด
JWT๋ ๋ธ๋ผ์ฐ์ ์ ๋ก์ปฌ ์คํ ๋ฆฌ์ง๋ ์ฟ ํค์ ์ ์ฅ๋๋ ๊ฒฝ์ฐ๊ฐ ๋ง์๋ฐ, XSS ๊ณต๊ฒฉ์ ๋ ธ์ถ๋๋ฉด ์ด ์ ๋ณด๋ค์ด ๊ทธ๋๋ก ํ์ทจ๋ ์ ์์ด. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ค๋ฃฐ ๋๋ ํนํ ์กฐ์ฌํด์ผ ํด! ๐ก๏ธ
โ
์์ ํ ํ์ด๋ก๋ ๊ตฌ์ฑ:
โข ์ฌ์ฉ์ ID (๊ณ ์ ์๋ณ์๋ง)
โข ํ ํฐ ๋ฐ๊ธ ์๊ฐ (iat)
โข ๋ง๋ฃ ์๊ฐ (exp)
โข ๊ธฐ๋ณธ์ ์ธ ๊ถํ ์ ๋ณด
โข ๊ณต๊ฐ๋์ด๋ ๊ด์ฐฎ์ ๋ฉํ๋ฐ์ดํฐ๋ง
JWT ์๋ช ์ ์ฌ์ฉ๋๋ ๋น๋ฐํค๊ฐ ์ฝํ๋ฉด ๋ธ๋ฃจํธํฌ์ค ๊ณต๊ฒฉ์ผ๋ก ๋ซ๋ฆด ์ ์์ด. ์ค์ ๋ก "secret", "password", "123456" ๊ฐ์ ํค๋ฅผ ์ฐ๋ ๊ฒฝ์ฐ๋ ์๋ค๋๊น! ๐ฑ
โก ์ค์ ์ฌ๋ก: 2019๋ ์ ํ ์ ๋ช ์๋น์ค์์ JWT ๋น๋ฐํค๋ก "secret"์ ์ฌ์ฉํ๋ค๊ฐ ํดํน๋นํ ์ฌ๋ก๊ฐ ์์์ด. ๊ณต๊ฒฉ์๋ ๋ช ์ด ๋ง์ ํค๋ฅผ ์์๋ด๊ณ ๊ด๋ฆฌ์ ๊ถํ ํ ํฐ์ ์์ฑํ์ง.
โ
๊ฐ๋ ฅํ ๋น๋ฐํค ์์ฑ ๋ฐฉ๋ฒ:
โข ์ต์ 256๋นํธ (32๋ฐ์ดํธ) ์ด์์ ๋๋ค ๋ฌธ์์ด
โข ์ํธํ์ ์ผ๋ก ์์ ํ ๋์ ์์ฑ๊ธฐ ์ฌ์ฉ
โข ํ๊ฒฝ ๋ณ์๋ก ๊ด๋ฆฌํ๊ณ ์ฝ๋์ ํ๋์ฝ๋ฉ ๊ธ์ง
โข ์ ๊ธฐ์ ์ธ ํค ๋กํ
์ด์
// Node.js์์ ์์ ํ ๋น๋ฐํค ์์ฑ
const crypto = require('crypto');
const secretKey = crypto.randomBytes(64).toString('hex');
// ํ๊ฒฝ ๋ณ์๋ก ๊ด๋ฆฌ
process.env.JWT_SECRET = secretKey;
ํ ํฐ์ ๋ง๋ฃ ์๊ฐ์ ์ค์ ํ์ง ์๊ฑฐ๋, ๋๋ฌด ๊ธด ๋ง๋ฃ ์๊ฐ์ ์ค์ ํ๋ ๊ฒ๋ ํฐ ๋ณด์ ์ํ์ด์ผ. ํ ํฐ์ด ํ์ทจ๋๋ฉด ๋ง๋ฃ๋ ๋๊น์ง ๊ณ์ ์ ์ฉ๋ ์ ์๊ฑฐ๋ ! ๐จ
โ ์ํํ ์ค์ :
โข ๋ง๋ฃ ์๊ฐ ์์ (exp ํด๋ ์ ๋ฏธ์ค์ )
โข 30์ผ, 90์ผ ๊ฐ์ ๊ธด ๋ง๋ฃ ์๊ฐ
โข ๋ฆฌํ๋ ์ ํ ํฐ ์์ด ๊ธด ์ก์ธ์ค ํ ํฐ๋ง ์ฌ์ฉ
โ
๊ถ์ฅ ์ค์ :
โข ์ก์ธ์ค ํ ํฐ: 15๋ถ ~ 1์๊ฐ
โข ๋ฆฌํ๋ ์ ํ ํฐ: 1์ผ ~ 2์ฃผ
โข ๋ฏผ๊ฐํ ์์
์ฉ ํ ํฐ: 5๋ถ ์ดํ
โข ๋ง๋ฃ ์๊ฐ ๊ฒ์ฆ ๋ก์ง ํ์ ๊ตฌํ
// JWT ์์ฑ ์ ๋ง๋ฃ ์๊ฐ ์ค์
const token = jwt.sign(
{ userId: user.id, role: user.role },
secretKey,
{
expiresIn: '15m', // 15๋ถ ํ ๋ง๋ฃ
issuer: 'jaenung.net',
audience: 'api.jaenung.net'
}
);
๐ก๏ธ JWT๋ฅผ ์์ ํ๊ฒ ์ ์ฅํ๋ ๋ฐฉ๋ฒ
JWT๋ฅผ ์์ฑํ๊ณ ๊ฒ์ฆํ๋ ๊ฒ๋ ์ค์ํ์ง๋ง, ํด๋ผ์ด์ธํธ์์ ์ด๋ป๊ฒ ์ ์ฅํ๋๋๋ ์์ฒญ ์ค์ํด! ์ ์ฅ ๋ฐฉ์์ ๋ฐ๋ผ ๋ณด์ ์์ค์ด ์์ ํ ๋ฌ๋ผ์ง๊ฑฐ๋ . ๐
๊ฐ ์ ์ฅ ๋ฐฉ์์ ์ฅ๋จ์ ์ ๋น๊ตํด๋ณผ๊ฒ:
1๏ธโฃ ๋ก์ปฌ์คํ ๋ฆฌ์ง (LocalStorage)
์ฅ์ :
โข ๊ฐ๋จํ๊ฒ ์ฌ์ฉ ๊ฐ๋ฅ
โข ์ฉ๋ ์ ํ์ด ์ฟ ํค๋ณด๋ค ํผ (5-10MB)
โข ์๋ฒ๋ก ์๋ ์ ์ก๋์ง ์์ ๋คํธ์ํฌ ํธ๋ํฝ ์ ์ฝ
๋จ์ :
โข XSS ๊ณต๊ฒฉ์ ๋งค์ฐ ์ทจ์ฝ! ๐ฑ
โข JavaScript๋ก ์ ๊ทผ ๊ฐ๋ฅํด์ ์
์ฑ ์คํฌ๋ฆฝํธ๊ฐ ์ฝ๊ฒ ํ์ทจ
โข ๋ง๋ฃ ์๊ฐ ์๋ ๊ด๋ฆฌ ์ ๋จ
2๏ธโฃ ์ธ์
์คํ ๋ฆฌ์ง (SessionStorage)
์ฅ์ :
โข ํญ์ ๋ซ์ผ๋ฉด ์๋ ์ญ์
โข ๋ก์ปฌ์คํ ๋ฆฌ์ง๋ณด๋ค๋ ์กฐ๊ธ ๋ ์์
๋จ์ :
โข ์ฌ์ ํ XSS ๊ณต๊ฒฉ์ ์ทจ์ฝ
โข ํญ ๊ฐ ๊ณต์ ์ ๋จ (๋ถํธํ ์ ์์)
3๏ธโฃ HTTP-Only ์ฟ ํค (๊ถ์ฅ!) โจ
์ฅ์ :
โข JavaScript๋ก ์ ๊ทผ ๋ถ๊ฐ (XSS ๋ฐฉ์ด!)
โข Secure ํ๋๊ทธ๋ก HTTPS์์๋ง ์ ์ก
โข SameSite ์์ฑ์ผ๋ก CSRF ๋ฐฉ์ด
โข ๋ง๋ฃ ์๊ฐ ์๋ ๊ด๋ฆฌ
๋จ์ :
โข ์๋ฒ ์ค์ ํ์
โข CORS ํ๊ฒฝ์์ ์ค์ ๋ณต์ก
โข ์ฉ๋ ์ ํ (4KB)
โ ๊ฐ์ฅ ์์ ํ ๋ฐฉ๋ฒ: HTTP-Only + Secure + SameSite ์ฟ ํค
// Express.js ์๋ฒ์์ ์์ ํ ์ฟ ํค ์ค์
res.cookie('accessToken', token, {
httpOnly: true, // JavaScript ์ ๊ทผ ์ฐจ๋จ
secure: true, // HTTPS์์๋ง ์ ์ก
sameSite: 'strict', // CSRF ๊ณต๊ฒฉ ๋ฐฉ์ด
maxAge: 900000, // 15๋ถ (๋ฐ๋ฆฌ์ด)
path: '/'
});
// ๋ฆฌํ๋ ์ ํ ํฐ์ ๋ ๊ธด ์๊ฐ์ผ๋ก
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 604800000, // 7์ผ
path: '/api/refresh' // ํน์ ๊ฒฝ๋ก์๋ง ์ ์ก
});
XSS(Cross-Site Scripting)๋ JWT๋ฅผ ๋ ธ๋ฆฌ๋ ๊ฐ์ฅ ํํ ๊ณต๊ฒฉ์ด์ผ. ์ ์ฑ ์คํฌ๋ฆฝํธ๋ฅผ ์ฃผ์ ํด์ ํ ํฐ์ ํ์ณ๊ฐ๋ ๊ฑฐ์ง. ๋ฐฉ์ด ๋ฐฉ๋ฒ์ ์์๋ณด์! ๐ก๏ธ
โ ๏ธ XSS ๊ณต๊ฒฉ ์๋๋ฆฌ์ค:
1. ๊ณต๊ฒฉ์๊ฐ ๊ฒ์ํ์ ์
์ฑ ์คํฌ๋ฆฝํธ ์ฝ์
2. ์ฌ์ฉ์๊ฐ ํด๋น ๊ฒ์๊ธ ์กฐํ
3. ์คํฌ๋ฆฝํธ ์คํ๋์ด localStorage์ JWT ํ์ทจ
4. ๊ณต๊ฒฉ์ ์๋ฒ๋ก ํ ํฐ ์ ์ก
5. ๊ณต๊ฒฉ์๊ฐ ํผํด์ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ
โ
XSS ๋ฐฉ์ด ์ ๋ต:
1. ์
๋ ฅ๊ฐ ๊ฒ์ฆ ๋ฐ ์ด์ค์ผ์ดํ
โข ๋ชจ๋ ์ฌ์ฉ์ ์
๋ ฅ์ ์ ๋ขฐํ์ง ๋ง ๊ฒ
โข HTML ํน์๋ฌธ์ ์ด์ค์ผ์ดํ
โข ํ์ดํธ๋ฆฌ์คํธ ๋ฐฉ์์ ์
๋ ฅ ๊ฒ์ฆ
2. Content Security Policy (CSP) ์ค์
โข ์ธ๋ผ์ธ ์คํฌ๋ฆฝํธ ์คํ ์ฐจ๋จ
โข ํ์ฉ๋ ๋๋ฉ์ธ์์๋ง ์คํฌ๋ฆฝํธ ๋ก๋
3. HTTP-Only ์ฟ ํค ์ฌ์ฉ
โข JavaScript ์ ๊ทผ ์์ฒ ์ฐจ๋จ
4. ์ต์ ํ๋ ์์ํฌ ์ฌ์ฉ
โข React, Vue, Angular ๋ฑ์ ๊ธฐ๋ณธ์ ์ผ๋ก XSS ๋ฐฉ์ด
โข dangerouslySetInnerHTML ๊ฐ์ ์ํํ API ์ฌ์ฉ ์์
// CSP ํค๋ ์ค์ ์์
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://trusted-cdn.com; " +
"style-src 'self' 'unsafe-inline';"
);
next();
});
// ์
๋ ฅ๊ฐ ์ด์ค์ผ์ดํ (DOMPurify ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ)
import DOMPurify from 'dompurify';
const cleanInput = DOMPurify.sanitize(userInput, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong'],
ALLOWED_ATTR: []
});
CSRF(Cross-Site Request Forgery)๋ ์ฌ์ฉ์๊ฐ ์๋ํ์ง ์์ ์์ฒญ์ ๋ณด๋ด๋๋ก ํ๋ ๊ณต๊ฒฉ์ด์ผ. JWT๋ฅผ ์ฟ ํค์ ์ ์ฅํ ๋ ํนํ ์กฐ์ฌํด์ผ ํด! ๐ฏ
โ ๏ธ CSRF ๊ณต๊ฒฉ ์๋๋ฆฌ์ค:
1. ์ฌ์ฉ์๊ฐ ์ ์ ์ฌ์ดํธ์ ๋ก๊ทธ์ธ (JWT ์ฟ ํค ์ ์ฅ)
2. ๊ณต๊ฒฉ์๊ฐ ๋ง๋ ์
์ฑ ์ฌ์ดํธ ๋ฐฉ๋ฌธ
3. ์
์ฑ ์ฌ์ดํธ์์ ์ ์ ์ฌ์ดํธ๋ก ์์ฒญ ์ ์ก
4. ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก ์ฟ ํค ํฌํจํด์ ์ ์ก
5. ์ฌ์ฉ์ ๋ชจ๋ฅด๊ฒ ๊ณ์ ์ ๋ณด ๋ณ๊ฒฝ, ์ก๊ธ ๋ฑ ์คํ
โ
CSRF ๋ฐฉ์ด ์ ๋ต:
1. SameSite ์ฟ ํค ์์ฑ ์ฌ์ฉ
โข Strict: ๊ฐ์ ์ฌ์ดํธ์์๋ง ์ฟ ํค ์ ์ก
โข Lax: GET ์์ฒญ์๋ง ์ฟ ํค ์ ์ก (๊ธฐ๋ณธ๊ฐ)
โข None: ๋ชจ๋ ์์ฒญ์ ์ ์ก (Secure ํ์)
2. CSRF ํ ํฐ ์ฌ์ฉ
โข ๊ฐ ์์ฒญ๋ง๋ค ๊ณ ์ ํ ํ ํฐ ์์ฑ
โข ์๋ฒ์์ ํ ํฐ ๊ฒ์ฆ
3. Custom Header ์ฌ์ฉ
โข X-Requested-With ๊ฐ์ ์ปค์คํ
ํค๋ ์ถ๊ฐ
โข CORS ์ ์ฑ
์ผ๋ก ์ธ๋ถ ์ฌ์ดํธ ์ฐจ๋จ
4. Referer ๊ฒ์ฆ
โข ์์ฒญ ์ถ์ฒ ํ์ธ
// SameSite ์ฟ ํค ์ค์
res.cookie('token', jwt, {
httpOnly: true,
secure: true,
sameSite: 'strict' // ๊ฐ์ฅ ๊ฐ๋ ฅํ ๋ณดํธ
});
// CSRF ํ ํฐ ๋ฏธ๋ค์จ์ด (csurf ๋ผ์ด๋ธ๋ฌ๋ฆฌ)
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.post('/api/transfer', csrfProtection, (req, res) => {
// CSRF ํ ํฐ ์๋ ๊ฒ์ฆ๋จ
// ๋น์ฆ๋์ค ๋ก์ง ์คํ
});
// ํด๋ผ์ด์ธํธ์์ CSRF ํ ํฐ ํฌํจ
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data)
});
๐ ๋ฆฌํ๋ ์ ํ ํฐ ์ ๋ต
์ก์ธ์ค ํ ํฐ์ ๋ง๋ฃ ์๊ฐ์ ์งง๊ฒ ํ๋ฉด ๋ณด์์ ์ข์์ง์ง๋ง, ์ฌ์ฉ์๊ฐ ์์ฃผ ๋ก๊ทธ์ธํด์ผ ํด์ ๋ถํธํ์์? ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๊ฒ ๋ฐ๋ก ๋ฆฌํ๋ ์ ํ ํฐ์ด์ผ! ๐ซ
์ก์ธ์ค ํ ํฐ (Access Token)
โข ์ค์ API ์์ฒญ์ ์ฌ์ฉ
โข ์งง์ ๋ง๋ฃ ์๊ฐ (15๋ถ ~ 1์๊ฐ)
โข ํ์ทจ๋์ด๋ ํผํด ์ต์ํ
โข ๋งค ์์ฒญ๋ง๋ค ์ ์ก
๋ฆฌํ๋ ์ ํ ํฐ (Refresh Token)
โข ์๋ก์ด ์ก์ธ์ค ํ ํฐ ๋ฐ๊ธ์ฉ
โข ๊ธด ๋ง๋ฃ ์๊ฐ (1์ผ ~ 2์ฃผ)
โข ๋ ์์ ํ๊ฒ ์ ์ฅ (HTTP-Only ์ฟ ํค)
โข ํน์ ์๋ํฌ์ธํธ์๋ง ์ ์ก
โข ์ผํ์ฑ ์ฌ์ฉ ๊ถ์ฅ (Rotation)
๋ฆฌํ๋ ์ ํ ํฐ์ ๋ ์์ ํ๊ฒ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ด ๋ฐ๋ก ๋กํ ์ด์ ์ด์ผ. ํ ๋ฒ ์ฌ์ฉํ ๋ฆฌํ๋ ์ ํ ํฐ์ ํ๊ธฐํ๊ณ ์๋ก์ด ๋ฆฌํ๋ ์ ํ ํฐ์ ๋ฐ๊ธํ๋ ๊ฑฐ์ง! ๐
โ
๋ฆฌํ๋ ์ ํ ํฐ ๋กํ
์ด์
์ ์ฅ์ :
โข ํ ํฐ ํ์ทจ ์ ํผํด ์ต์ํ
โข ์ฌ์ฌ์ฉ ๊ณต๊ฒฉ ๋ฐฉ์ง
โข ์์ฌ์ค๋ฌ์ด ํ๋ ๊ฐ์ง ๊ฐ๋ฅ
โข ๋ณด์ ๊ฐ์ฌ ์ถ์ ์ฉ์ด
// ๋ฆฌํ๋ ์ ํ ํฐ ๋กํ
์ด์
๊ตฌํ ์์
app.post('/api/refresh', async (req, res) => {
const { refreshToken } = req.cookies;
try {
// 1. ๋ฆฌํ๋ ์ ํ ํฐ ๊ฒ์ฆ
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
// 2. DB์์ ํ ํฐ ํ์ธ (์ด๋ฏธ ์ฌ์ฉ๋์๋์ง)
const storedToken = await db.getRefreshToken(decoded.userId);
if (!storedToken || storedToken !== refreshToken) {
// ์ฌ์ฌ์ฉ ์๋ ๊ฐ์ง! ๋ชจ๋ ํ ํฐ ๋ฌดํจํ
await db.revokeAllTokens(decoded.userId);
return res.status(401).json({ error: '์์ฌ์ค๋ฌ์ด ํ๋ ๊ฐ์ง' });
}
// 3. ๊ธฐ์กด ๋ฆฌํ๋ ์ ํ ํฐ ํ๊ธฐ
await db.revokeRefreshToken(refreshToken);
// 4. ์๋ก์ด ์ก์ธ์ค ํ ํฐ ์์ฑ
const newAccessToken = jwt.sign(
{ userId: decoded.userId, role: decoded.role },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
// 5. ์๋ก์ด ๋ฆฌํ๋ ์ ํ ํฐ ์์ฑ
const newRefreshToken = jwt.sign(
{ userId: decoded.userId },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
// 6. ์ ๋ฆฌํ๋ ์ ํ ํฐ DB์ ์ ์ฅ
await db.saveRefreshToken(decoded.userId, newRefreshToken);
// 7. ์ฟ ํค๋ก ์ ์ก
res.cookie('accessToken', newAccessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 900000
});
res.cookie('refreshToken', newRefreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 604800000
});
res.json({ success: true });
} catch (error) {
res.status(401).json({ error: 'ํ ํฐ ๊ฐฑ์ ์คํจ' });
}
});
๋ฆฌํ๋ ์ ํ ํฐ์ ์๋ฒ ์ธก์์๋ ๊ด๋ฆฌํด์ผ ํด. ๋จ์ํ ๊ฒ์ฆ๋ง ํ๋ ๊ฒ ์๋๋ผ, ๋ฐ๊ธ๋ ํ ํฐ์ ์ถ์ ํ๊ณ ํ์์ ๋ฌดํจํํ ์ ์์ด์ผ ํ๊ฑฐ๋ ! ๐
๋ฆฌํ๋ ์ ํ ํฐ ์ ์ฅ์ ์ ํ์ง:
1. Redis (๊ถ์ฅ!)
โข ๋น ๋ฅธ ์๋
โข TTL ์๋ ๊ด๋ฆฌ
โข ๋ฉ๋ชจ๋ฆฌ ๊ธฐ๋ฐ์ด๋ผ ํจ์จ์
2. ๊ด๊ณํ DB (MySQL, PostgreSQL)
โข ์๊ตฌ ์ ์ฅ
โข ๋ณต์กํ ์ฟผ๋ฆฌ ๊ฐ๋ฅ
โข ๊ฐ์ฌ ๋ก๊ทธ ์ ์ง
3. NoSQL (MongoDB)
โข ์ ์ฐํ ์คํค๋ง
โข ํ์ฅ์ฑ ์ข์
// Redis๋ฅผ ์ฌ์ฉํ ๋ฆฌํ๋ ์ ํ ํฐ ๊ด๋ฆฌ
const redis = require('redis');
const client = redis.createClient();
// ๋ฆฌํ๋ ์ ํ ํฐ ์ ์ฅ (7์ผ TTL)
async function saveRefreshToken(userId, token) {
const key = `refresh:${userId}`;
await client.setEx(key, 604800, token); // 7์ผ
}
// ๋ฆฌํ๋ ์ ํ ํฐ ์กฐํ
async function getRefreshToken(userId) {
const key = `refresh:${userId}`;
return await client.get(key);
}
// ๋ฆฌํ๋ ์ ํ ํฐ ํ๊ธฐ
async function revokeRefreshToken(userId) {
const key = `refresh:${userId}`;
await client.del(key);
}
// ์ฌ์ฉ์์ ๋ชจ๋ ํ ํฐ ๋ฌดํจํ (๋ณด์ ์ฌ๊ณ ์)
async function revokeAllTokens(userId) {
const keys = await client.keys(`refresh:${userId}*`);
if (keys.length > 0) {
await client.del(keys);
}
}
๐ JWT ์ํธํ ์๊ณ ๋ฆฌ์ฆ ์ ํ
JWT์์ ์ฌ์ฉํ๋ ์ํธํ ์๊ณ ๋ฆฌ์ฆ ์ ํ์ ์ ๋ง ์ค์ํด! ์๋ชป ์ ํํ๋ฉด ๋ณด์์ด ์์ ํ ๋ฌด๋์ง ์ ์๊ฑฐ๋ . ์ฃผ์ ์๊ณ ๋ฆฌ์ฆ๋ค์ ๋น๊ตํด๋ณด์! ๐ฏ
๋์นญํค ์๊ณ ๋ฆฌ์ฆ (HMAC)
HS256, HS384, HS512
ํน์ง:
โข ๊ฐ์ ํค๋ก ์๋ช
ํ๊ณ ๊ฒ์ฆ
โข ๋น ๋ฅธ ์๋
โข ๊ตฌํ ๊ฐ๋จ
์ฅ์ :
โข ์ฑ๋ฅ์ด ๋ฐ์ด๋จ
โข ๋จ์ผ ์๋ฒ ํ๊ฒฝ์ ์ ํฉ
๋จ์ :
โข ํค ๊ณต์ ํ์ (๋ณด์ ์ํ)
โข ํค ๋
ธ์ถ ์ ๋ชจ๋ ํ ํฐ ์ํ
โข ๋ง์ดํฌ๋ก์๋น์ค์์ ํค ๊ด๋ฆฌ ๋ณต์ก
๋น๋์นญํค ์๊ณ ๋ฆฌ์ฆ (RSA, ECDSA)
RS256, RS384, RS512, ES256, ES384, ES512
ํน์ง:
โข ๊ฐ์ธํค๋ก ์๋ช
, ๊ณต๊ฐํค๋ก ๊ฒ์ฆ
โข ๋๋ฆฐ ์๋
โข ๊ตฌํ ๋ณต์ก
์ฅ์ :
โข ๊ณต๊ฐํค๋ง ๋ฐฐํฌํ๋ฉด ๋จ (์์ !)
โข ๋ง์ดํฌ๋ก์๋น์ค์ ์ ํฉ
โข ํค ๋
ธ์ถ ์ ํผํด ์ ํ์
๋จ์ :
โข ์ฑ๋ฅ ์ค๋ฒํค๋
โข ํค ๊ด๋ฆฌ ๋ณต์ก
โข ํ ํฐ ํฌ๊ธฐ ์ฆ๊ฐ
โ
์๊ณ ๋ฆฌ์ฆ ์ ํ ๊ฐ์ด๋:
HS256 ์ฌ์ฉ ๊ถ์ฅ:
โข ๋จ์ผ ์๋ฒ ์ ํ๋ฆฌ์ผ์ด์
โข ๋์ ์ฑ๋ฅ ์๊ตฌ
โข ๊ฐ๋จํ ๊ตฌ์กฐ
RS256 ์ฌ์ฉ ๊ถ์ฅ:
โข ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ
์ฒ
โข ์ฌ๋ฌ ์๋น์ค๊ฐ ํ ํฐ ๊ฒ์ฆ
โข ๋์ ๋ณด์ ์๊ตฌ
โข OAuth 2.0, OpenID Connect ๊ตฌํ
ES256 ์ฌ์ฉ ๊ถ์ฅ:
โข RS256๋ณด๋ค ๋น ๋ฅธ ์ฑ๋ฅ ํ์
โข ๋ชจ๋ฐ์ผ ํ๊ฒฝ
โข ์ต์ ๋ณด์ ํ์ค ์ค์
// HS256 (๋์นญํค) ์ฌ์ฉ ์์
const jwt = require('jsonwebtoken');
const secretKey = process.env.JWT_SECRET;
const token = jwt.sign(
{ userId: 123, role: 'user' },
secretKey,
{ algorithm: 'HS256', expiresIn: '15m' }
);
// RS256 (๋น๋์นญํค) ์ฌ์ฉ ์์
const fs = require('fs');
const privateKey = fs.readFileSync('private.key');
const publicKey = fs.readFileSync('public.key');
// ํ ํฐ ์์ฑ (๊ฐ์ธํค ์ฌ์ฉ)
const token = jwt.sign(
{ userId: 123, role: 'user' },
privateKey,
{ algorithm: 'RS256', expiresIn: '15m' }
);
// ํ ํฐ ๊ฒ์ฆ (๊ณต๊ฐํค ์ฌ์ฉ)
jwt.verify(token, publicKey, { algorithms: ['RS256'] }, (err, decoded) => {
if (err) {
console.error('๊ฒ์ฆ ์คํจ:', err);
} else {
console.log('๊ฒ์ฆ ์ฑ๊ณต:', decoded);
}
});
โ ์ํํ ์ค์ ๋ค:
1. none ์๊ณ ๋ฆฌ์ฆ
โข ์๋ช
์์ด ํ ํฐ ์์ฑ
โข ๋๊ตฌ๋ ํ ํฐ ์กฐ์ ๊ฐ๋ฅ
โข ์ ๋ ํ๋ก๋์
์์ ์ฌ์ฉ ๊ธ์ง!
2. ์ฝํ ๋น๋ฐํค
โข "secret", "password" ๊ฐ์ ๋จ์ ํค
โข ์งง์ ๊ธธ์ด์ ํค (256๋นํธ ๋ฏธ๋ง)
โข ํ๋์ฝ๋ฉ๋ ํค
3. ์๊ณ ๋ฆฌ์ฆ ์๋ ๊ฐ์ง
โข ํค๋์ alg ํ๋๋ฅผ ๊ทธ๋๋ก ์ ๋ขฐ
โข ์๊ณ ๋ฆฌ์ฆ ํผ๋ ๊ณต๊ฒฉ์ ์ทจ์ฝ
4. ๊ตฌํ ์๊ณ ๋ฆฌ์ฆ
โข MD5, SHA1 ๊ธฐ๋ฐ ์๊ณ ๋ฆฌ์ฆ
โข ์ด๋ฏธ ์ทจ์ฝ์ ์ด ์๋ ค์ง ์๊ณ ๋ฆฌ์ฆ
// โ ์ ๋ ํ์ง ๋ง์์ผ ํ ๊ฒ๋ค
// 1. none ์๊ณ ๋ฆฌ์ฆ ํ์ฉ
jwt.verify(token, secretKey, {
algorithms: ['HS256', 'none'] // ์ํ!
});
// 2. ์๊ณ ๋ฆฌ์ฆ ์๋ ๊ฐ์ง
jwt.verify(token, secretKey); // ์ํ! ์๊ณ ๋ฆฌ์ฆ ๋ช
์ ์ ํจ
// 3. ์ฝํ ๋น๋ฐํค
const secretKey = 'secret'; // ์ํ!
// โ
์ฌ๋ฐ๋ฅธ ๋ฐฉ๋ฒ
// 1. ์๊ณ ๋ฆฌ์ฆ ๋ช
์์ ์ง์
jwt.verify(token, secretKey, {
algorithms: ['HS256'] // ์์ !
});
// 2. ๊ฐ๋ ฅํ ๋น๋ฐํค ์์ฑ
const crypto = require('crypto');
const secretKey = crypto.randomBytes(64).toString('hex');
// 3. ํ๊ฒฝ ๋ณ์๋ก ๊ด๋ฆฌ
const secretKey = process.env.JWT_SECRET;
๐ ๏ธ JWT ๊ฒ์ฆ ์ฒดํฌ๋ฆฌ์คํธ
JWT๋ฅผ ๊ฒ์ฆํ ๋๋ ๋จ์ํ ์๋ช ๋ง ํ์ธํ๋ฉด ์ ๋ผ. ์ฌ๋ฌ ๊ฐ์ง๋ฅผ ์ฒดํฌํด์ผ ์์ ํด! ์ค๋ฌด์์ ๊ผญ ํ์ธํด์ผ ํ ํญ๋ชฉ๋ค์ ์ ๋ฆฌํด๋ดค์ด. โ
1๏ธโฃ ์๋ช
๊ฒ์ฆ (Signature Verification)
โข ํ ํฐ์ด ์์กฐ๋์ง ์์๋์ง ํ์ธ
โข ์ฌ๋ฐ๋ฅธ ๋น๋ฐํค๋ก ์๋ช
๋์๋์ง ๊ฒ์ฆ
โข ์๊ณ ๋ฆฌ์ฆ ์ผ์น ์ฌ๋ถ ํ์ธ
2๏ธโฃ ๋ง๋ฃ ์๊ฐ ๊ฒ์ฆ (exp)
โข ํ ํฐ์ด ๋ง๋ฃ๋์ง ์์๋์ง ํ์ธ
โข ํ์ฌ ์๊ฐ๊ณผ ๋น๊ต
โข ํด๋ญ ์คํ(Clock Skew) ๊ณ ๋ ค
3๏ธโฃ ๋ฐ๊ธ ์๊ฐ ๊ฒ์ฆ (iat)
โข ํ ํฐ์ด ๋ฏธ๋์ ๋ฐ๊ธ๋์ง ์์๋์ง ํ์ธ
โข ์๊ฐ ์กฐ์ ๋ฐฉ์ง
4๏ธโฃ ์ฌ์ฉ ์์ ์๊ฐ ๊ฒ์ฆ (nbf)
โข Not Before ํด๋ ์ ํ์ธ
โข ํ ํฐ์ด ์์ง ์ ํจํ์ง ์์ ๊ฒฝ์ฐ ๊ฑฐ๋ถ
5๏ธโฃ ๋ฐ๊ธ์ ๊ฒ์ฆ (iss)
โข ์ ๋ขฐํ ์ ์๋ ๋ฐ๊ธ์์ธ์ง ํ์ธ
โข ๋ค๋ฅธ ์์คํ
์ ํ ํฐ ๊ฑฐ๋ถ
6๏ธโฃ ๋์ ๊ฒ์ฆ (aud)
โข ํ ํฐ์ด ์ด ์๋น์ค๋ฅผ ์ํ ๊ฒ์ธ์ง ํ์ธ
โข ๋ค๋ฅธ ์๋น์ค์ฉ ํ ํฐ ๊ฑฐ๋ถ
7๏ธโฃ ํ ํฐ ID ๊ฒ์ฆ (jti)
โข ํ ํฐ์ ๊ณ ์ ์๋ณ์ ํ์ธ
โข ์ฌ์ฌ์ฉ ๊ณต๊ฒฉ ๋ฐฉ์ง
โข ๋ธ๋๋ฆฌ์คํธ ํ์ธ
// ์๋ฒฝํ JWT ๊ฒ์ฆ ์์
const jwt = require('jsonwebtoken');
const redis = require('redis');
const client = redis.createClient();
async function verifyToken(token) {
try {
// 1. ๊ธฐ๋ณธ ๊ฒ์ฆ (์๋ช
, ๋ง๋ฃ์๊ฐ ๋ฑ)
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'], // ์๊ณ ๋ฆฌ์ฆ ๋ช
์
issuer: 'jaenung.net', // ๋ฐ๊ธ์ ํ์ธ
audience: 'api.jaenung.net', // ๋์ ํ์ธ
clockTolerance: 30 // 30์ด ํด๋ญ ์คํ ํ์ฉ
});
// 2. ํ ํฐ ๋ธ๋๋ฆฌ์คํธ ํ์ธ
const isBlacklisted = await client.get(`blacklist:${decoded.jti}`);
if (isBlacklisted) {
throw new Error('ํ ํฐ์ด ๋ฌดํจํ๋์์ต๋๋ค');
}
// 3. ์ฌ์ฉ์ ์ํ ํ์ธ (์ ํ์ )
const user = await db.getUser(decoded.userId);
if (!user || !user.isActive) {
throw new Error('์ฌ์ฉ์ ๊ณ์ ์ด ๋นํ์ฑํ๋์์ต๋๋ค');
}
// 4. ๊ถํ ํ์ธ
if (decoded.role !== 'admin' && decoded.role !== 'user') {
throw new Error('์ ํจํ์ง ์์ ๊ถํ์
๋๋ค');
}
// 5. ์ถ๊ฐ ๋ณด์ ๊ฒ์ฆ
// IP ์ฃผ์ ํ์ธ, ๋๋ฐ์ด์ค ํ์ธ ๋ฑ
return decoded;
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('ํ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค');
} else if (error.name === 'JsonWebTokenError') {
throw new Error('์ ํจํ์ง ์์ ํ ํฐ์
๋๋ค');
} else if (error.name === 'NotBeforeError') {
throw new Error('ํ ํฐ์ด ์์ง ์ ํจํ์ง ์์ต๋๋ค');
}
throw error;
}
}
// Express ๋ฏธ๋ค์จ์ด๋ก ์ฌ์ฉ
async function authMiddleware(req, res, next) {
try {
const token = req.cookies.accessToken ||
req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'ํ ํฐ์ด ์์ต๋๋ค' });
}
const decoded = await verifyToken(token);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: error.message });
}
}
JWT๋ ๊ธฐ๋ณธ์ ์ผ๋ก Stateless๋ผ์ ํ ๋ฒ ๋ฐ๊ธ๋๋ฉด ๋ง๋ฃ ์ ๊น์ง ๋ฌดํจํํ๊ธฐ ์ด๋ ค์. ํ์ง๋ง ๋ณด์ ์ฌ๊ณ ๊ฐ ๋ฐ์ํ๋ฉด ์ฆ์ ํ ํฐ์ ๋ฌดํจํํด์ผ ํ์์? ์ด๋ด ๋ ์ฌ์ฉํ๋ ์ ๋ต๋ค์ด์ผ! ๐
๋ธ๋๋ฆฌ์คํธ ๋ฐฉ์
โข ๋ฌดํจํํ ํ ํฐ์ ๋ธ๋๋ฆฌ์คํธ์ ์ถ๊ฐ
โข ๋งค ์์ฒญ๋ง๋ค ๋ธ๋๋ฆฌ์คํธ ํ์ธ
โข Redis ๊ฐ์ ๋น ๋ฅธ ์ ์ฅ์ ์ฌ์ฉ
โข TTL ์ค์ ์ผ๋ก ์๋ ์ ๋ฆฌ
// ๋ธ๋๋ฆฌ์คํธ ๊ตฌํ
async function revokeToken(token) {
const decoded = jwt.decode(token);
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
// ๋ง๋ฃ ์๊ฐ๊น์ง๋ง ๋ธ๋๋ฆฌ์คํธ์ ์ ์ง
await client.setEx(`blacklist:${decoded.jti}`, ttl, 'revoked');
}
// ๋ก๊ทธ์์ ์ ํ ํฐ ๋ฌดํจํ
app.post('/api/logout', authMiddleware, async (req, res) => {
const token = req.cookies.accessToken;
await revokeToken(token);
res.clearCookie('accessToken');
res.clearCookie('refreshToken');
res.json({ message: '๋ก๊ทธ์์ ์ฑ๊ณต' });
});
ํ์ดํธ๋ฆฌ์คํธ ๋ฐฉ์
โข ์ ํจํ ํ ํฐ๋ง ํ์ดํธ๋ฆฌ์คํธ์ ์ ์ฅ
โข ๋ฆฌํ๋ ์ ํ ํฐ์ ์ฃผ๋ก ์ฌ์ฉ
โข ๋ ์์ ํ์ง๋ง Stateless ์ฅ์ ์์ค
โข ์ ์ฅ์ ์ฉ๋ ๋ ๋ง์ด ํ์
// ํ์ดํธ๋ฆฌ์คํธ ๊ตฌํ
async function issueToken(userId, role) {
const jti = crypto.randomBytes(16).toString('hex');
const token = jwt.sign(
{ userId, role, jti },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
// ํ์ดํธ๋ฆฌ์คํธ์ ์ถ๊ฐ
await client.setEx(`whitelist:${jti}`, 900, userId);
return token;
}
// ๊ฒ์ฆ ์ ํ์ดํธ๋ฆฌ์คํธ ํ์ธ
async function verifyWhitelistedToken(token) {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = await client.get(`whitelist:${decoded.jti}`);
if (!userId || userId !== decoded.userId.toString()) {
throw new Error('์ ํจํ์ง ์์ ํ ํฐ์
๋๋ค');
}
return decoded;
}
๋ฒ์ ๊ด๋ฆฌ ๋ฐฉ์
โข ์ฌ์ฉ์๋ณ ํ ํฐ ๋ฒ์ ๊ด๋ฆฌ
โข ๋ฒ์ ๋ถ์ผ์น ์ ํ ํฐ ๊ฑฐ๋ถ
โข ๋ชจ๋ ํ ํฐ ์ผ๊ด ๋ฌดํจํ ๊ฐ๋ฅ
โข ์ ์ฅ์ ๋ถ๋ด ์ต์ํ
// ๋ฒ์ ๊ด๋ฆฌ ๊ตฌํ
// ํ ํฐ ๋ฐ๊ธ ์ ๋ฒ์ ํฌํจ
async function issueVersionedToken(userId, role) {
const version = await db.getUserTokenVersion(userId);
const token = jwt.sign(
{ userId, role, version },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
return token;
}
// ๊ฒ์ฆ ์ ๋ฒ์ ํ์ธ
async function verifyVersionedToken(token) {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const currentVersion = await db.getUserTokenVersion(decoded.userId);
if (decoded.version !== currentVersion) {
throw new Error('ํ ํฐ ๋ฒ์ ์ด ์ผ์นํ์ง ์์ต๋๋ค');
}
return decoded;
}
// ๋ชจ๋ ํ ํฐ ๋ฌดํจํ (๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ ์ ๋ฑ)
async function invalidateAllUserTokens(userId) {
await db.incrementUserTokenVersion(userId);
}
๐ ์ค์ ์๋๋ฆฌ์ค๋ณ ๋ณด์ ๊ฐ์ด๋
์ด๋ก ์ ์ถฉ๋ถํ ๋ฐฐ์ ์ผ๋, ์ด์ ์ค์ ์ํฉ์์ ์ด๋ป๊ฒ ์ ์ฉํ๋์ง ์์๋ณด์! ์ฌ๋ฅ๋ท ๊ฐ์ ์ค์ ์๋น์ค์์ ๋ง์ฃผ์น ์ ์๋ ์๋๋ฆฌ์ค๋ค์ด์ผ. ๐ผ
React, Vue, Angular ๊ฐ์ SPA์์ JWT๋ฅผ ์ฌ์ฉํ ๋์ ๋ณด์ ์ ๋ต์ด์ผ. ๊ฐ์ฅ ํํ ์ผ์ด์ค์ง! ๐
โ
๊ถ์ฅ ์ํคํ
์ฒ:
1. ํ ํฐ ์ ์ฅ
โข ์ก์ธ์ค ํ ํฐ: ๋ฉ๋ชจ๋ฆฌ (๋ณ์)
โข ๋ฆฌํ๋ ์ ํ ํฐ: HTTP-Only ์ฟ ํค
2. API ์์ฒญ
โข Authorization ํค๋์ ์ก์ธ์ค ํ ํฐ ํฌํจ
โข ์๋์ผ๋ก ์ฟ ํค๋ ์ ์ก๋จ (๋ฆฌํ๋ ์์ฉ)
3. ํ ํฐ ๊ฐฑ์
โข 401 ์๋ฌ ์ ์๋์ผ๋ก ๋ฆฌํ๋ ์
โข ์ ์ก์ธ์ค ํ ํฐ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅ
4. ๋ณด์ ๊ฐํ
โข CSP ํค๋ ์ค์
โข CORS ์ ์ฑ
์๊ฒฉํ ๊ด๋ฆฌ
โข XSS ๋ฐฉ์ด ์ฒ ์ ํ
// React์์ ์์ ํ JWT ๊ด๋ฆฌ
import axios from 'axios';
// ์ก์ธ์ค ํ ํฐ์ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅ
let accessToken = null;
// Axios ์ธํฐ์
ํฐ ์ค์
axios.interceptors.request.use(
(config) => {
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
// ์๋ต ์ธํฐ์
ํฐ (ํ ํฐ ๊ฐฑ์ )
axios.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// 401 ์๋ฌ์ด๊ณ ์ฌ์๋๊ฐ ์๋ ๊ฒฝ์ฐ
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// ๋ฆฌํ๋ ์ ํ ํฐ์ผ๋ก ์ ์ก์ธ์ค ํ ํฐ ๋ฐ๊ธ
const response = await axios.post('/api/refresh', {}, {
withCredentials: true // ์ฟ ํค ํฌํจ
});
accessToken = response.data.accessToken;
// ์๋ ์์ฒญ ์ฌ์๋
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axios(originalRequest);
} catch (refreshError) {
// ๋ฆฌํ๋ ์ ์คํจ ์ ๋ก๊ทธ์์
accessToken = null;
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
// ๋ก๊ทธ์ธ ํจ์
async function login(email, password) {
try {
const response = await axios.post('/api/login',
{ email, password },
{ withCredentials: true } // ์ฟ ํค ๋ฐ๊ธฐ
);
// ์ก์ธ์ค ํ ํฐ๋ง ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅ
accessToken = response.data.accessToken;
// ๋ฆฌํ๋ ์ ํ ํฐ์ HTTP-Only ์ฟ ํค๋ก ์๋ ์ ์ฅ๋จ
return response.data;
} catch (error) {
console.error('๋ก๊ทธ์ธ ์คํจ:', error);
throw error;
}
}
// ๋ก๊ทธ์์ ํจ์
async function logout() {
try {
await axios.post('/api/logout', {}, {
withCredentials: true
});
accessToken = null;
window.location.href = '/login';
} catch (error) {
console.error('๋ก๊ทธ์์ ์คํจ:', error);
}
}
iOS, Android ๋ค์ดํฐ๋ธ ์ฑ์์๋ ์น๊ณผ ๋ค๋ฅธ ๋ณด์ ์ ๋ต์ด ํ์ํด! ๐ฒ
โ
๋ชจ๋ฐ์ผ ์ฑ ๋ณด์ ์ ๋ต:
1. ํ ํฐ ์ ์ฅ
โข iOS: Keychain
โข Android: EncryptedSharedPreferences
โข ์ ๋ ์ผ๋ฐ SharedPreferences ์ฌ์ฉ ๊ธ์ง!
2. ์ถ๊ฐ ๋ณด์
โข ์์ฒด ์ธ์ฆ ์ฐ๋
โข ์ฑ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ
โข ๋ฃจํ
/ํ์ฅ ๊ฐ์ง
โข SSL Pinning
3. ํ ํฐ ๊ฐฑ์
โข ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์๋ ๊ฐฑ์
โข ์ฑ ์ฌ์์ ์ ํ ํฐ ์ ํจ์ฑ ํ์ธ
// React Native์์ ์์ ํ ํ ํฐ ์ ์ฅ
import * as Keychain from 'react-native-keychain';
import * as LocalAuthentication from 'expo-local-authentication';
// ํ ํฐ ์ ์ฅ (์์ฒด ์ธ์ฆ ํ์)
async function saveTokens(accessToken, refreshToken) {
try {
// ์์ฒด ์ธ์ฆ ํ์ธ
const hasHardware = await LocalAuthentication.hasHardwareAsync();
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
if (hasHardware && isEnrolled) {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'ํ ํฐ์ ์์ ํ๊ฒ ์ ์ฅํ๊ธฐ ์ํด ์ธ์ฆ์ด ํ์ํฉ๋๋ค',
});
if (!result.success) {
throw new Error('์ธ์ฆ ์คํจ');
}
}
// Keychain์ ์ ์ฅ
await Keychain.setGenericPassword(
'tokens',
JSON.stringify({ accessToken, refreshToken }),
{
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
securityLevel: Keychain.SECURITY_LEVEL.SECURE_HARDWARE,
}
);
} catch (error) {
console.error('ํ ํฐ ์ ์ฅ ์คํจ:', error);
throw error;
}
}
// ํ ํฐ ๋ถ๋ฌ์ค๊ธฐ
async function loadTokens() {
try {
const credentials = await Keychain.getGenericPassword();
if (credentials) {
const tokens = JSON.parse(credentials.password);
return tokens;
}
return null;
} catch (error) {
console.error('ํ ํฐ ๋ถ๋ฌ์ค๊ธฐ ์คํจ:', error);
return null;
}
}
// ํ ํฐ ์ญ์
async function deleteTokens() {
try {
await Keychain.resetGenericPassword();
} catch (error) {
console.error('ํ ํฐ ์ญ์ ์คํจ:', error);
}
}
์ฌ๋ฌ ์๋น์ค๊ฐ JWT๋ฅผ ๊ณต์ ํด์ผ ํ ๋๋ ์ด๋ป๊ฒ ํด์ผ ํ ๊น? ๋ณต์กํ์ง๋ง ์ค์ํ ์ฃผ์ ์ผ! ๐๏ธ
โ
๋ง์ดํฌ๋ก์๋น์ค JWT ์ ๋ต:
1. ์ค์ ์ธ์ฆ ์๋น์ค
โข ํ๋์ ์๋น์ค์์๋ง ํ ํฐ ๋ฐ๊ธ
โข ๋ค๋ฅธ ์๋น์ค๋ค์ ๊ฒ์ฆ๋ง ์ํ
โข API Gateway์์ ์ธ์ฆ ์ฒ๋ฆฌ
2. ๋น๋์นญํค ์ฌ์ฉ (RS256)
โข ์ธ์ฆ ์๋น์ค: ๊ฐ์ธํค๋ก ์๋ช
โข ๋ค๋ฅธ ์๋น์ค๋ค: ๊ณต๊ฐํค๋ก ๊ฒ์ฆ
โข ํค ๊ณต์ ๋ถํ์!
3. ์๋น์ค ๊ฐ ํต์
โข ๋ด๋ถ ํต์ ์ฉ ๋ณ๋ ํ ํฐ
โข mTLS (์ํธ TLS) ์ฌ์ฉ
โข ์๋น์ค ๋ฉ์ ํ์ฉ
4. ํ ํฐ ํ์ด๋ก๋
โข ์ต์ํ์ ์ ๋ณด๋ง ํฌํจ
โข ์๋น์ค๋ณ ๊ถํ ์ ๋ณด
โข ํ
๋ํธ ID (๋ฉํฐํ
๋์)
// ์ธ์ฆ ์๋น์ค (ํ ํฐ ๋ฐ๊ธ)
const fs = require('fs');
const jwt = require('jsonwebtoken');
const privateKey = fs.readFileSync('private.key');
function issueToken(user) {
return jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role,
permissions: user.permissions,
tenantId: user.tenantId
},
privateKey,
{
algorithm: 'RS256',
expiresIn: '15m',
issuer: 'auth.jaenung.net',
audience: ['api.jaenung.net', 'admin.jaenung.net']
}
);
}
// ๋ค๋ฅธ ๋ง์ดํฌ๋ก์๋น์ค (ํ ํฐ ๊ฒ์ฆ)
const publicKey = fs.readFileSync('public.key');
function verifyToken(token, requiredPermission) {
try {
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'auth.jaenung.net',
audience: 'api.jaenung.net'
});
// ๊ถํ ํ์ธ
if (!decoded.permissions.includes(requiredPermission)) {
throw new Error('๊ถํ์ด ์์ต๋๋ค');
}
return decoded;
} catch (error) {
throw error;
}
}
// API Gateway ๋ฏธ๋ค์จ์ด
async function gatewayAuthMiddleware(req, res, next) {
try {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'ํ ํฐ์ด ์์ต๋๋ค' });
}
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256']
});
// ๊ฒ์ฆ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ํค๋์ ์ถ๊ฐ
req.headers['X-User-Id'] = decoded.userId;
req.headers['X-User-Role'] = decoded.role;
req.headers['X-Tenant-Id'] = decoded.tenantId;
next();
} catch (error) {
res.status(401).json({ error: '์ธ์ฆ ์คํจ' });
}
}
์์ ๋ก๊ทธ์ธ์ด๋ ์ 3์ ์ธ์ฆ์ ๊ตฌํํ ๋๋ ํ์ค ํ๋กํ ์ฝ์ ๋ฐ๋ผ์ผ ํด! ๐
OAuth 2.0 + JWT ์กฐํฉ:
์ก์ธ์ค ํ ํฐ:
โข JWT ํ์ ์ฌ์ฉ ๊ฐ๋ฅ
โข ๋ฆฌ์์ค ์๋ฒ์์ ๊ฒ์ฆ
โข ์งง์ ๋ง๋ฃ ์๊ฐ
ID ํ ํฐ (OpenID Connect):
โข ํญ์ JWT ํ์
โข ์ฌ์ฉ์ ์ ๋ณด ํฌํจ
โข ํด๋ผ์ด์ธํธ์์ ๊ฒ์ฆ
๋ฆฌํ๋ ์ ํ ํฐ:
โข ๋ถํฌ๋ช
ํ ํ ํฐ ๊ถ์ฅ
โข ์ธ์ฆ ์๋ฒ์์๋ง ๊ฒ์ฆ
โข ๊ธด ๋ง๋ฃ ์๊ฐ
// OpenID Connect ID ํ ํฐ ๊ฒ์ฆ
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'https://auth.jaenung.net/.well-known/jwks.json',
cache: true,
cacheMaxAge: 86400000 // 24์๊ฐ
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) {
callback(err);
} else {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
}
});
}
function verifyIdToken(idToken) {
return new Promise((resolve, reject) => {
jwt.verify(idToken, getKey, {
algorithms: ['RS256'],
issuer: 'https://auth.jaenung.net',
audience: 'your-client-id'
}, (err, decoded) => {
if (err) {
reject(err);
} else {
// ID ํ ํฐ ํด๋ ์ ๊ฒ์ฆ
if (!decoded.sub || !decoded.email) {
reject(new Error('ํ์ ํด๋ ์์ด ์์ต๋๋ค'));
}
resolve(decoded);
}
});
});
}
๐ ๋ณด์ ๋ชจ๋ํฐ๋ง๊ณผ ๋ก๊น
์๋ฌด๋ฆฌ ๋ณด์์ ์ ๊ตฌํํด๋ ๋ชจ๋ํฐ๋งํ์ง ์์ผ๋ฉด ๊ณต๊ฒฉ์ ๋์น ์ ์์ด. ์ค์๊ฐ์ผ๋ก ์์ฌ์ค๋ฌ์ด ํ๋์ ๊ฐ์งํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์! ๐
ํ์ ๋ก๊น
ํญ๋ชฉ:
1. ์ธ์ฆ ๊ด๋ จ
โข ๋ก๊ทธ์ธ ์ฑ๊ณต/์คํจ
โข ๋ก๊ทธ์์
โข ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ
โข ๊ณ์ ์ ๊ธ
2. ํ ํฐ ๊ด๋ จ
โข ํ ํฐ ๋ฐ๊ธ
โข ํ ํฐ ๊ฐฑ์
โข ํ ํฐ ๊ฒ์ฆ ์คํจ
โข ํ ํฐ ๋ฌดํจํ
3. ์์ฌ์ค๋ฌ์ด ํ๋
โข ์งง์ ์๊ฐ ๋ด ๋ค์ ์คํจ
โข ๋น์ ์์ ์ธ IP ์ฃผ์
โข ์ ์ ์๋ ๋๋ฐ์ด์ค
โข ๊ถํ ์์น ์๋
4. ๋ฉํ๋ฐ์ดํฐ
โข ํ์์คํฌํ
โข IP ์ฃผ์
โข User-Agent
โข ์ง๋ฆฌ์ ์์น
// Winston์ ์ฌ์ฉํ ๋ณด์ ๋ก๊น
const winston = require('winston');
const securityLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({
filename: 'security.log',
level: 'warn'
}),
new winston.transports.File({
filename: 'auth.log'
})
]
});
// ๋ก๊ทธ์ธ ์๋ ๋ก๊น
function logLoginAttempt(req, success, userId = null) {
const logData = {
event: 'login_attempt',
success,
userId,
ip: req.ip,
userAgent: req.headers['user-agent'],
timestamp: new Date().toISOString()
};
if (success) {
securityLogger.info('๋ก๊ทธ์ธ ์ฑ๊ณต', logData);
} else {
securityLogger.warn('๋ก๊ทธ์ธ ์คํจ', logData);
}
}
// ํ ํฐ ๊ฒ์ฆ ์คํจ ๋ก๊น
function logTokenVerificationFailure(req, error) {
securityLogger.warn('ํ ํฐ ๊ฒ์ฆ ์คํจ', {
event: 'token_verification_failed',
error: error.message,
ip: req.ip,
userAgent: req.headers['user-agent'],
timestamp: new Date().toISOString()
});
}
// ์์ฌ์ค๋ฌ์ด ํ๋ ๋ก๊น
function logSuspiciousActivity(req, reason, userId = null) {
securityLogger.error('์์ฌ์ค๋ฌ์ด ํ๋ ๊ฐ์ง', {
event: 'suspicious_activity',
reason,
userId,
ip: req.ip,
userAgent: req.headers['user-agent'],
timestamp: new Date().toISOString()
});
// ์๋ฆผ ์ ์ก (Slack, ์ด๋ฉ์ผ ๋ฑ)
sendSecurityAlert(reason, userId, req.ip);
}
๋ก๊ทธ๋ฅผ ์๋ ๊ฒ๋ง์ผ๋ก๋ ๋ถ์กฑํด. ์ค์๊ฐ์ผ๋ก ๋ถ์ํด์ ๊ณต๊ฒฉ์ ๋ง์์ผ ํด! ๐ก๏ธ
โ
์ํ ๊ฐ์ง ํจํด:
1. ๋ธ๋ฃจํธํฌ์ค ๊ณต๊ฒฉ
โข ์งง์ ์๊ฐ ๋ด ๋ค์ ๋ก๊ทธ์ธ ์คํจ
โข ๋์: ๊ณ์ ์ผ์ ์ ๊ธ, CAPTCHA
2. ํ ํฐ ์ฌ์ฌ์ฉ
โข ์ด๋ฏธ ์ฌ์ฉ๋ ๋ฆฌํ๋ ์ ํ ํฐ ์ฌ์ฌ์ฉ
โข ๋์: ๋ชจ๋ ํ ํฐ ๋ฌดํจํ
3. ๋น์ ์์ ์ธ ์์น
โข ์งง์ ์๊ฐ ๋ด ๋ค๋ฅธ ๊ตญ๊ฐ์์ ์ ์
โข ๋์: ์ถ๊ฐ ์ธ์ฆ ์๊ตฌ
4. ๊ถํ ์์น ์๋
โข ํ์ด๋ก๋ ์กฐ์ ์๋
โข ๋์: ์ฆ์ ์ฐจ๋จ, ๋ก๊ทธ ๊ธฐ๋ก
// Redis๋ฅผ ์ฌ์ฉํ ๋ธ๋ฃจํธํฌ์ค ๋ฐฉ์ด
const redis = require('redis');
const client = redis.createClient();
async function checkBruteForce(identifier) {
const key = `login_attempts:${identifier}`;
const attempts = await client.get(key);
if (attempts && parseInt(attempts) >= 5) {
const ttl = await client.ttl(key);
throw new Error(`๋๋ฌด ๋ง์ ๋ก๊ทธ์ธ ์๋. ${ttl}์ด ํ ๋ค์ ์๋ํ์ธ์.`);
}
}
async function recordLoginAttempt(identifier, success) {
const key = `login_attempts:${identifier}`;
if (success) {
// ์ฑ๊ณต ์ ์นด์ดํฐ ๋ฆฌ์
await client.del(key);
} else {
// ์คํจ ์ ์นด์ดํฐ ์ฆ๊ฐ
const attempts = await client.incr(key);
if (attempts === 1) {
// ์ฒซ ์คํจ ์ 15๋ถ TTL ์ค์
await client.expire(key, 900);
}
if (attempts >= 5) {
// 5ํ ์คํจ ์ ์๋ฆผ
logSuspiciousActivity({ ip: identifier }, 'brute_force_detected');
}
}
}
// ๋ก๊ทธ์ธ ์๋ํฌ์ธํธ์ ์ ์ฉ
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const identifier = req.ip; // ๋๋ email
try {
// ๋ธ๋ฃจํธํฌ์ค ์ฒดํฌ
await checkBruteForce(identifier);
// ์ธ์ฆ ์๋
const user = await authenticateUser(email, password);
if (user) {
await recordLoginAttempt(identifier, true);
const accessToken = issueToken(user);
const refreshToken = issueRefreshToken(user);
res.cookie('accessToken', accessToken, cookieOptions);
res.cookie('refreshToken', refreshToken, cookieOptions);
logLoginAttempt(req, true, user.id);
res.json({ success: true });
} else {
await recordLoginAttempt(identifier, false);
logLoginAttempt(req, false);
res.status(401).json({ error: '์ธ์ฆ ์คํจ' });
}
} catch (error) {
res.status(429).json({ error: error.message });
}
});
์ฃผ์ ๋ณด์ ์งํ๋ค์ ๋์๋ณด๋๋ก ๋ชจ๋ํฐ๋งํ๋ฉด ๋ฌธ์ ๋ฅผ ๋น ๋ฅด๊ฒ ํ์ ํ ์ ์์ด! ๐
๋ชจ๋ํฐ๋งํ ๋ฉํธ๋ฆญ:
โข ๋ก๊ทธ์ธ ์ฑ๊ณต๋ฅ
โข ํ ํฐ ๊ฒ์ฆ ์คํจ์จ
โข ํ๊ท ํ ํฐ ์๋ช
โข ๋ฆฌํ๋ ์ ํ ํฐ ์ฌ์ฉ ๋น๋
โข ์์ฌ์ค๋ฌ์ด ํ๋ ๊ฑด์
โข ์ง์ญ๋ณ ์ ์ ๋ถํฌ
โข ๋๋ฐ์ด์ค ์ ํ ๋ถํฌ
โข API ์๋ต ์๊ฐ
// Prometheus๋ฅผ ์ฌ์ฉํ ๋ฉํธ๋ฆญ ์์ง
const promClient = require('prom-client');
// ๋ฉํธ๋ฆญ ์ ์
const loginAttempts = new promClient.Counter({
name: 'login_attempts_total',
help: '์ด ๋ก๊ทธ์ธ ์๋ ํ์',
labelNames: ['status']
});
const tokenVerifications = new promClient.Counter({
name: 'token_verifications_total',
help: '์ด ํ ํฐ ๊ฒ์ฆ ํ์',
labelNames: ['result']
});
const activeTokens = new promClient.Gauge({
name: 'active_tokens',
help: 'ํ์ฌ ํ์ฑ ํ ํฐ ์'
});
// ๋ฉํธ๋ฆญ ๊ธฐ๋ก
function recordLoginMetric(success) {
loginAttempts.inc({ status: success ? 'success' : 'failure' });
}
function recordTokenVerificationMetric(result) {
tokenVerifications.inc({ result });
}
// ๋ฉํธ๋ฆญ ์๋ํฌ์ธํธ
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(await promClient.register.metrics());
});
๐ ๋ณด์ ์ฒดํฌ๋ฆฌ์คํธ ์ด์ ๋ฆฌ
์, ์ด์ ๋ง์ง๋ง์ผ๋ก JWT ๋ณด์์ ์ํ ์๋ฒฝํ ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ์ ๋ฆฌํด๋ณผ๊ฒ! ํ๋ก์ ํธ์ ์ ์ฉํ ๋ ํ๋์ฉ ์ฒดํฌํ๋ฉด์ ์ฌ์ฉํ๋ฉด ์ข์. โ
โก JWT ๋ณด์ ๋ง์คํฐ ์ฒดํฌ๋ฆฌ์คํธ
๐ ํ ํฐ ์์ฑ ๋จ๊ณ
โ๏ธ ๊ฐ๋ ฅํ ๋น๋ฐํค ์ฌ์ฉ (์ต์ 256๋นํธ)
โ๏ธ ํ๊ฒฝ ๋ณ์๋ก ํค ๊ด๋ฆฌ
โ๏ธ ์ ์ ํ ์๊ณ ๋ฆฌ์ฆ ์ ํ (HS256 ๋๋ RS256)
โ๏ธ none ์๊ณ ๋ฆฌ์ฆ ์ ๋ ํ์ฉ ์ ํจ
โ๏ธ ์งง์ ๋ง๋ฃ ์๊ฐ ์ค์ (15๋ถ ~ 1์๊ฐ)
โ๏ธ ํ์ ํด๋ ์ ํฌํจ (exp, iat, iss, aud)
โ๏ธ ๋ฏผ๊ฐํ ์ ๋ณด ํ์ด๋ก๋์ ๋ฃ์ง ์๊ธฐ
โ๏ธ ๊ณ ์ ์๋ณ์ (jti) ํฌํจ
โ๏ธ ๋ฆฌํ๋ ์ ํ ํฐ ๋ณ๋ ๋ฐ๊ธ
๐ก๏ธ ํ ํฐ ์ ์ฅ ๋จ๊ณ
โ๏ธ ์น: HTTP-Only + Secure + SameSite ์ฟ ํค
โ๏ธ ๋ชจ๋ฐ์ผ: Keychain (iOS) / EncryptedSharedPreferences (Android)
โ๏ธ LocalStorage ์ฌ์ฉ ์์
โ๏ธ ์ก์ธ์ค ํ ํฐ์ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅ (SPA)
โ๏ธ ๋ฆฌํ๋ ์ ํ ํฐ์ ์์ ํ ์ ์ฅ์์
โ๏ธ ํ ํฐ ์ํธํ ๊ณ ๋ ค (์ถ๊ฐ ๋ณด์์ธต)
โ
ํ ํฐ ๊ฒ์ฆ ๋จ๊ณ
โ๏ธ ์๋ช
๊ฒ์ฆ
โ๏ธ ๋ง๋ฃ ์๊ฐ ๊ฒ์ฆ
โ๏ธ ๋ฐ๊ธ์ ๊ฒ์ฆ
โ๏ธ ๋์ ๊ฒ์ฆ
โ๏ธ ์๊ณ ๋ฆฌ์ฆ ๋ช
์์ ์ง์
โ๏ธ ๋ธ๋๋ฆฌ์คํธ ํ์ธ
โ๏ธ ์ฌ์ฉ์ ์ํ ํ์ธ
โ๏ธ ๊ถํ ๊ฒ์ฆ
โ๏ธ ํด๋ญ ์คํ ๊ณ ๋ ค
๐ ํ ํฐ ๊ฐฑ์ ๋จ๊ณ
โ๏ธ ๋ฆฌํ๋ ์ ํ ํฐ ๋กํ
์ด์
๊ตฌํ
โ๏ธ ์ฌ์ฌ์ฉ ๊ฐ์ง ๋ฉ์ปค๋์ฆ
โ๏ธ ๋ฆฌํ๋ ์ ํ ํฐ ์ ์ฅ์ ๊ด๋ฆฌ
โ๏ธ ์๋ ๊ฐฑ์ ๋ก์ง ๊ตฌํ
โ๏ธ ๊ฐฑ์ ์คํจ ์ ๋ก๊ทธ์์
โ๏ธ ๊ฐฑ์ ์ด๋ฒคํธ ๋ก๊น
๐ซ ๊ณต๊ฒฉ ๋ฐฉ์ด ๋จ๊ณ
โ๏ธ XSS ๋ฐฉ์ด (CSP, ์
๋ ฅ ๊ฒ์ฆ, ์ด์ค์ผ์ดํ)
โ๏ธ CSRF ๋ฐฉ์ด (SameSite, CSRF ํ ํฐ)
โ๏ธ ๋ธ๋ฃจํธํฌ์ค ๋ฐฉ์ด (Rate Limiting)
โ๏ธ ์๊ณ ๋ฆฌ์ฆ ํผ๋ ๊ณต๊ฒฉ ๋ฐฉ์ด
โ๏ธ ํ ํฐ ์ฌ์ฌ์ฉ ๊ณต๊ฒฉ ๋ฐฉ์ด
โ๏ธ ์ค๊ฐ์ ๊ณต๊ฒฉ ๋ฐฉ์ด (HTTPS ํ์)
โ๏ธ SQL Injection ๋ฐฉ์ด
โ๏ธ ๊ถํ ์์น ๊ณต๊ฒฉ ๋ฐฉ์ด
๐ ๋ชจ๋ํฐ๋ง ๋จ๊ณ
โ๏ธ ๋ณด์ ์ด๋ฒคํธ ๋ก๊น
โ๏ธ ์์ฌ์ค๋ฌ์ด ํ๋ ๊ฐ์ง
โ๏ธ ์ค์๊ฐ ์๋ฆผ ์ค์
โ๏ธ ๋ฉํธ๋ฆญ ์์ง ๋ฐ ๋์๋ณด๋
โ๏ธ ์ ๊ธฐ์ ์ธ ๋ณด์ ๊ฐ์ฌ
โ๏ธ ๋ก๊ทธ ๋ณด๊ด ์ ์ฑ
โ๏ธ ์ธ์๋ํธ ๋์ ๊ณํ
๐ง ์ด์ ๋จ๊ณ
โ๏ธ ์ ๊ธฐ์ ์ธ ํค ๋กํ
์ด์
โ๏ธ ๋ณด์ ํจ์น ์ ์ฉ
โ๏ธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์
๋ฐ์ดํธ
โ๏ธ ์นจํฌ ํ
์คํธ ์ํ
โ๏ธ ๋ณด์ ๊ต์ก ์ค์
โ๏ธ ๋ฐฑ์
๋ฐ ๋ณต๊ตฌ ๊ณํ
โ๏ธ ๊ท์ ์ค์ ํ์ธ (GDPR, CCPA ๋ฑ)
๐ ๋ชจ๋ ํญ๋ชฉ์ ์ฒดํฌํ๋ค๋ฉด ์ถํํด! ๋น์ ์ JWT ์ธ์ฆ ์์คํ ์ ์ด์ ํ๋ก๋์ ๋ ๋ฒจ์ ๋ณด์์ ๊ฐ์ท์ด. ํ์ง๋ง ๋ณด์์ ํ ๋ฒ ์ค์ ํ๊ณ ๋๋๋ ๊ฒ ์๋์ผ. ์ง์์ ์ผ๋ก ๋ชจ๋ํฐ๋งํ๊ณ ์ ๋ฐ์ดํธํด์ผ ํด! ๐ช
๐ก ๋ง์น๋ฉฐ
์, ์ ๋ง ๊ธด ์ฌ์ ์ด์์ง? ๐
JWT ๋ณด์์ ๋ํด ์ ๋ง ๋ง์ ๋ด์ฉ์ ๋ค๋ค์ด. ์ฒ์์๋ ๋ณต์กํด ๋ณด์ผ ์ ์์ง๋ง, ํ๋์ฉ ์ ์ฉํ๋ค ๋ณด๋ฉด ์์ฐ์ค๋ฝ๊ฒ ์ต์ํด์ง ๊ฑฐ์ผ!
JWT๋ ์ ๋ง ๊ฐ๋ ฅํ๊ณ ํธ๋ฆฌํ ์ธ์ฆ ๋ฐฉ์์ด์ง๋ง, ๊ทธ๋งํผ ๋ณด์์ ์ ๊ฒฝ ์จ์ผ ํ ๋ถ๋ถ๋ ๋ง์. ํนํ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ์ฌ์ฉ์๋ค์ ์์คํ ์ ๋ณด์ ์ฌ๋ฅ์ ๋ค๋ฃจ๊ธฐ ๋๋ฌธ์ ๋ณด์์ด ๋์ฑ ์ค์ํ์ง. ๐
๊ธฐ์ตํด! ๋ณด์์ ์ ํ์ด ์๋๋ผ ํ์์ผ. ๊ทธ๋ฆฌ๊ณ ์๋ฒฝํ ๋ณด์์ ์์ด. ํญ์ ์ต์ ๋ณด์ ํธ๋ ๋๋ฅผ ๋ฐ๋ผ๊ฐ๊ณ , ์๋ก์ด ์ทจ์ฝ์ ์ ๋๋นํด์ผ ํด. ์ ๊ธฐ์ ์ผ๋ก ๋ณด์ ๊ฐ์ฌ๋ฅผ ํ๊ณ , ํ์๋ค๊ณผ ๋ณด์ ์ง์์ ๊ณต์ ํ๋ ๊ฒ๋ ์์ง ๋ง! ๐ฅ
์ด ๊ธ์ด JWT ๋ณด์์ ๊ตฌํํ๋ ๋ฐ ๋์์ด ๋์์ผ๋ฉด ์ข๊ฒ ์ด. ๊ถ๊ธํ ์ ์ด ์๊ฑฐ๋ ๋ ์๊ณ ์ถ์ ๋ด์ฉ์ด ์๋ค๋ฉด, ์ฌ๋ฅ๋ท์ '์ง์์ธ์ ์ฒ'์์ ๋ค๋ฅธ ์ ๋ฌธ๊ฐ๋ค๊ณผ ์ํตํด๋ณด๋ ๊ฒ๋ ์ข์ ๊ฑฐ์ผ! ๐ณ
์์ ํ ์ฝ๋ฉ ๋๊ธธ ๋ฐ๋ผ! ํ์ดํ
! ๐โจ
๐ ํต์ฌ ์์ฝ
1๏ธโฃ ๊ฐ๋ ฅํ ์๊ณ ๋ฆฌ์ฆ๊ณผ ๋น๋ฐํค ์ฌ์ฉ
2๏ธโฃ ์งง์ ๋ง๋ฃ ์๊ฐ + ๋ฆฌํ๋ ์ ํ ํฐ ์ ๋ต
3๏ธโฃ HTTP-Only ์ฟ ํค๋ก ์์ ํ๊ฒ ์ ์ฅ
4๏ธโฃ XSS, CSRF ๊ณต๊ฒฉ ์ฒ ์ ํ ๋ฐฉ์ด
5๏ธโฃ ์ค์๊ฐ ๋ชจ๋ํฐ๋ง๊ณผ ๋ก๊น
6๏ธโฃ ์ ๊ธฐ์ ์ธ ๋ณด์ ๊ฐ์ฌ์ ์
๋ฐ์ดํธ
๋ณด์์ ํ ๋ฒ์ ์์ฑ๋๋ ๊ฒ ์๋๋ผ ์ง์์ ์ธ ๋
ธ๋ ฅ์ด ํ์ํด! ๐ช
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.