์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ” JWT(JSON Web Token) ์ธ์ฆ์˜ ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ: ์•ˆ์ „ํ•˜๊ฒŒ ํ† ํฐ ๋‹ค๋ฃจ๋Š” ๋ฒ•

๐Ÿ” JWT(JSON Web Token) ์ธ์ฆ์˜ ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ: ์•ˆ์ „ํ•˜๊ฒŒ ํ† ํฐ ๋‹ค๋ฃจ๋Š” ๋ฒ•

๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๋ฐ˜๋“œ์‹œ ์•Œ์•„์•ผ ํ•  JWT ๋ณด์•ˆ ํ•ต์‹ฌ ๊ฐ€์ด๋“œ

๐ŸŽฏ JWT, ์™œ ์ด๋ ‡๊ฒŒ ๋งŽ์ด ์“ฐ์ผ๊นŒ?

์•ˆ๋…•! ์˜ค๋Š˜์€ ์›น ๊ฐœ๋ฐœํ•˜๋ฉด์„œ ์ •๋ง ์ž์ฃผ ๋งˆ์ฃผ์น˜๋Š” JWT์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ๊ฒŒ. ์š”์ฆ˜ API ๊ฐœ๋ฐœํ•˜๊ฑฐ๋‚˜ ์ธ์ฆ ์‹œ์Šคํ…œ ๊ตฌ์ถ•ํ•  ๋•Œ JWT ์•ˆ ์“ฐ๋Š” ๊ณณ์ด ๊ฑฐ์˜ ์—†์„ ์ •๋„์ž–์•„? ๐Ÿ˜Š

JWT๋Š” JSON Web Token์˜ ์•ฝ์ž๋กœ, ์‚ฌ์šฉ์ž ์ธ์ฆ ์ •๋ณด๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•œ ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ ๋ฐฉ์‹์ด์•ผ. ์ „ํ†ต์ ์ธ ์„ธ์…˜ ๋ฐฉ์‹๊ณผ ๋‹ฌ๋ฆฌ ์„œ๋ฒ„์— ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š๋Š” Stateless ํŠน์„ฑ ๋•๋ถ„์— ํ™•์žฅ์„ฑ์ด ๋›ฐ์–ด๋‚˜๊ณ , ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜์—์„œ๋„ ์ •๋ง ์œ ์šฉํ•˜๊ฒŒ ์“ฐ์ด์ง€.

ํ•˜์ง€๋งŒ ์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ํฌ์ธํŠธ! JWT๊ฐ€ ํŽธ๋ฆฌํ•˜๋‹ค๊ณ  ํ•ด์„œ ๋ณด์•ˆ์„ ์†Œํ™€ํžˆ ํ•˜๋ฉด ํฐ์ผ ๋‚˜. ์‹ค์ œ๋กœ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด JWT๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ ๋ณด์•ˆ ์ทจ์•ฝ์ ์„ ๋งŒ๋“ค์–ด๋‚ด๊ณ  ์žˆ์–ด. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์‚ฌ์šฉ์ž ์ธ์ฆ์€ ์ •๋ง ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด๋‹ˆ๊นŒ, ์˜ค๋Š˜ ์ œ๋Œ€๋กœ ํŒŒํ—ค์ณ๋ณด์ž! ๐Ÿ’ช

JWT ๊ตฌ์กฐ์™€ ๋ณด์•ˆ ํ๋ฆ„ Header ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ •๋ณด ํ† ํฐ ํƒ€์ž… Payload ์‚ฌ์šฉ์ž ์ •๋ณด ๊ถŒํ•œ ๋ฐ์ดํ„ฐ Signature ์„œ๋ช… ๊ฒ€์ฆ ๋ฌด๊ฒฐ์„ฑ ๋ณด์žฅ โš ๏ธ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ณต๊ฒฉ ๐Ÿ”“ ๋ฏผ๊ฐ์ •๋ณด ๋…ธ์ถœ ๐Ÿ”‘ ํ‚ค ๊ด€๋ฆฌ ์ทจ์•ฝ์  ๋ณด์•ˆ ๊ฐ•ํ™” ํฌ์ธํŠธ ๊ฐ•๋ ฅํ•œ ์•Œ๊ณ ๋ฆฌ์ฆ˜ + ์•ˆ์ „ํ•œ ์ €์žฅ + ์งง์€ ๋งŒ๋ฃŒ์‹œ๊ฐ„

๐Ÿ” 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 ์‚ฌ์šฉํ•  ๋•Œ ์กฐ์‹ฌํ•ด์•ผ ํ•  ๋ณด์•ˆ ์ทจ์•ฝ์ ๋“ค์„ ์•Œ์•„๋ณด์ž. ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ ์ •๋ง ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋“ค์ด๋‹ˆ๊นŒ ๊ผญ ์ฒดํฌํ•ด! โœ…

๐ŸŽญ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ํ˜ผ๋™ ๊ณต๊ฒฉ (Algorithm Confusion Attack)

์ด๊ฑด ์ •๋ง ์œ ๋ช…ํ•œ ๊ณต๊ฒฉ ๋ฐฉ๋ฒ•์ด์•ผ. 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๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š” ๊ฒƒ๋„ ์ค‘์š”ํ•˜์ง€๋งŒ, ํด๋ผ์ด์–ธํŠธ์—์„œ ์–ด๋–ป๊ฒŒ ์ €์žฅํ•˜๋А๋ƒ๋„ ์—„์ฒญ ์ค‘์š”ํ•ด! ์ €์žฅ ๋ฐฉ์‹์— ๋”ฐ๋ผ ๋ณด์•ˆ ์ˆ˜์ค€์ด ์™„์ „ํžˆ ๋‹ฌ๋ผ์ง€๊ฑฐ๋“ . ๐Ÿ”

๐Ÿช ์ฟ ํ‚ค vs ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€ vs ์„ธ์…˜์Šคํ† ๋ฆฌ์ง€

๊ฐ ์ €์žฅ ๋ฐฉ์‹์˜ ์žฅ๋‹จ์ ์„ ๋น„๊ตํ•ด๋ณผ๊ฒŒ:

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 ๊ณต๊ฒฉ ๋ฐฉ์–ดํ•˜๊ธฐ

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 ๊ณต๊ฒฉ ๋ฐฉ์–ดํ•˜๊ธฐ

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)
});

๐Ÿ”„ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์ „๋žต

์•ก์„ธ์Šค ํ† ํฐ์˜ ๋งŒ๋ฃŒ ์‹œ๊ฐ„์„ ์งง๊ฒŒ ํ•˜๋ฉด ๋ณด์•ˆ์€ ์ข‹์•„์ง€์ง€๋งŒ, ์‚ฌ์šฉ์ž๊ฐ€ ์ž์ฃผ ๋กœ๊ทธ์ธํ•ด์•ผ ํ•ด์„œ ๋ถˆํŽธํ•˜์ž–์•„? ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๊ฒŒ ๋ฐ”๋กœ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ด์•ผ! ๐ŸŽซ

๐ŸŽญ ์•ก์„ธ์Šค ํ† ํฐ vs ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ

์•ก์„ธ์Šค ํ† ํฐ (Access Token) โ€ข ์‹ค์ œ API ์š”์ฒญ์— ์‚ฌ์šฉ
โ€ข ์งง์€ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ (15๋ถ„ ~ 1์‹œ๊ฐ„)
โ€ข ํƒˆ์ทจ๋˜์–ด๋„ ํ”ผํ•ด ์ตœ์†Œํ™”
โ€ข ๋งค ์š”์ฒญ๋งˆ๋‹ค ์ „์†ก

๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ (Refresh Token) โ€ข ์ƒˆ๋กœ์šด ์•ก์„ธ์Šค ํ† ํฐ ๋ฐœ๊ธ‰์šฉ
โ€ข ๊ธด ๋งŒ๋ฃŒ ์‹œ๊ฐ„ (1์ผ ~ 2์ฃผ)
โ€ข ๋” ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅ (HTTP-Only ์ฟ ํ‚ค)
โ€ข ํŠน์ • ์—”๋“œํฌ์ธํŠธ์—๋งŒ ์ „์†ก
โ€ข ์ผํšŒ์„ฑ ์‚ฌ์šฉ ๊ถŒ์žฅ (Rotation)

๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ํ๋ฆ„๋„ ํด๋ผ์ด์–ธํŠธ ๋กœ๊ทธ์ธ ์š”์ฒญ ID/PW ์„œ๋ฒ„ ์ธ์ฆ ์ฒ˜๋ฆฌ ์•ก์„ธ์Šค + ๋ฆฌํ”„๋ ˆ์‹œ API ์š”์ฒญ ์•ก์„ธ์Šค ํ† ํฐ ์‚ฌ์šฉ ์ •์ƒ ์‘๋‹ต ํ† ํฐ ๋งŒ๋ฃŒ 401 ์—๋Ÿฌ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์œผ๋กœ ์ƒˆ ์•ก์„ธ์Šค ํ† ํฐ ๋ฐœ๊ธ‰ ์ƒˆ ํ† ํฐ ๋ฐœ๊ธ‰ ๊ณ„์† ์‚ฌ์šฉ ๋ณด์•ˆ ์œ ์ง€ + ํŽธ์˜์„ฑ ์ตœ์ ์˜ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜
๐Ÿ”„ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๋กœํ…Œ์ด์…˜ (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์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์•”ํ˜ธํ™” ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์„ ํƒ์€ ์ •๋ง ์ค‘์š”ํ•ด! ์ž˜๋ชป ์„ ํƒํ•˜๋ฉด ๋ณด์•ˆ์ด ์™„์ „ํžˆ ๋ฌด๋„ˆ์งˆ ์ˆ˜ ์žˆ๊ฑฐ๋“ . ์ฃผ์š” ์•Œ๊ณ ๋ฆฌ์ฆ˜๋“ค์„ ๋น„๊ตํ•ด๋ณด์ž! ๐ŸŽฏ

๐Ÿ”‘ ๋Œ€์นญํ‚ค vs ๋น„๋Œ€์นญํ‚ค

๋Œ€์นญํ‚ค ์•Œ๊ณ ๋ฆฌ์ฆ˜ (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 });
  }
}
๐Ÿšจ ํ† ํฐ ๋ฌดํšจํ™” (Revocation) ์ „๋žต

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);
}

๐ŸŒ ์‹ค์ „ ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ๋ณด์•ˆ ๊ฐ€์ด๋“œ

์ด๋ก ์€ ์ถฉ๋ถ„ํžˆ ๋ฐฐ์› ์œผ๋‹ˆ, ์ด์ œ ์‹ค์ œ ์ƒํ™ฉ์—์„œ ์–ด๋–ป๊ฒŒ ์ ์šฉํ•˜๋Š”์ง€ ์•Œ์•„๋ณด์ž! ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ์‹ค์ œ ์„œ๋น„์Šค์—์„œ ๋งˆ์ฃผ์น  ์ˆ˜ ์žˆ๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค๋“ค์ด์•ผ. ๐Ÿ’ผ

๐ŸŽจ ์‹œ๋‚˜๋ฆฌ์˜ค 1: ์‹ฑ๊ธ€ ํŽ˜์ด์ง€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ (SPA)

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);
  }
}
๐Ÿ“ฑ ์‹œ๋‚˜๋ฆฌ์˜ค 2: ๋ชจ๋ฐ”์ผ ์•ฑ

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);
  }
}
๐Ÿ”— ์‹œ๋‚˜๋ฆฌ์˜ค 3: ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜

์—ฌ๋Ÿฌ ์„œ๋น„์Šค๊ฐ€ 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: '์ธ์ฆ ์‹คํŒจ' });
  }
}
๐Ÿ” ์‹œ๋‚˜๋ฆฌ์˜ค 4: OAuth 2.0 / OpenID Connect

์†Œ์…œ ๋กœ๊ทธ์ธ์ด๋‚˜ ์ œ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
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.