๐Ÿ”ย ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”ํ•ด๋ณด์ž (Feat: Spring)


ํ•ด๋‹น ๊ธ€์—์„œ ๊ตฌํ˜„ ์–ธ๊ธ‰ํ•œ ์ฝ”๋“œ๋Š” practice-member-api ์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

์‹œ์ž‘ํ•˜๋ฉฐ

์ดˆ๋ฐ˜ ์—ฐ์Šต์šฉ ํ”„๋กœ์ ํŠธ์—์„œ ํšŒ์›๊ฐ€์ž…์„ ๊ตฌํ˜„ํ•˜๋ฉฐ ๊ทธ๋ƒฅ ์ผ๋ฐ˜ ํ…์ŠคํŠธ๋กœ ํšŒ์› ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๊ด€๋ฆฌํ•ด์™”์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๋ณด์•ˆ์ƒ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ด๋ ‡๊ฒŒ ํ‰๋ฌธ์œผ๋กœ ์ €์žฅํ•˜๋Š” ์ผ์€ ๋ฐ˜๋“œ์‹œ ์ง€์–‘๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋งˆ์นจ ํšŒ์›๊ฐ€์ž…๊ณผ Jwt ํ† ํฐ์„ ์‚ฌ์šฉํ•˜๋Š” ์ธ์ฆ/์ธ๊ฐ€๋Š” ๋งค์šฐ ์ž์ฃผ ์“ฐ๋Š” ๋กœ์ง์ด๋ผ ๋‚˜๋งŒ์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋งŒ๋“ค๊ณ  ์‹ถ์—ˆ๋Š”๋ฐ์š”. ์›ํ‹ฐ๋“œ ํ”„๋ฆฌ์˜จ๋ณด๋”ฉ ๋ฐฑ์—”๋“œ ์‚ฌ์ „ ๋ฏธ์…˜์ด ๋”ฑ ๊ธฐ๋ณธ ํšŒ์›๊ฐ€์ž… + jwt ํ† ํฐ ๋ฐฉ์‹์˜ ์ธ์ฆ์ธ๊ฐ€ + ๊ฒŒ์‹œํŒ REST API๋ผ์„œ ์ด์ฐธ์— ์ด ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค๐Ÿ’ช

Spring Framework ๋Š” Spring Security ๋ผ๋Š” ํ•˜์œ„ ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ์ด๋Ÿฌํ•œ ๋ณด์•ˆ ์ธ์ฆ์ธ๊ฐ€ ๋ฅผ ํŽธ๋ฆฌํ•˜๊ฒŒ ์ œ๊ณตํ•ด์ฃผ๊ณ  ์žˆ์ง€๋งŒ ํ•ด๋‹น ์•”ํ˜ธํ™” ๊ธฐ์ˆ  ๋Œ€ํ•œ ์ดํ•ด๋ฅผ ๋†’์ด๊ธฐ ์œ„ํ•ด Spring Security ์—†์ด ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด ์•”ํ˜ธํ™” ํ•˜๋‚˜๋งŒ์„ ์œ„ํ•ด์„œ ์˜์กด์„ฑ์„ ์ฃผ์ž…ํ•˜๊ณ , ์ž˜ ๋ชจ๋ฅด๋Š” ํ”„๋ ˆ์ž„ ์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์ง€ ์•Š๊ธฐ๋„ ํ–ˆ๊ณ ์š”... ๐Ÿฅฒ

๐Ÿค“ ๋จผ์ € ๊ณต๋ถ€ํ•ฉ์‹œ๋‹ค, ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”

์•”ํ˜ธํ™” Encryption ์™€ ํ•ด์‹ฑ Hashing ์˜ ์ฐจ์ด์ 

๋ณด์•ˆ ๋ถ„์•ผ์—์„œ๋Š” ์•”ํ˜ธํ™”์™€ ํ•ด์‹ฑ์˜ ๊ฐœ๋…์ด ๋‹ค๋ฅด๋‹ค๋Š” ๊ฒƒ์„ ๋จผ์ € ์งš๊ณ  ๋„˜์–ด๊ฐ€์•ผ๊ฒ ์Šต๋‹ˆ๋‹ค.

์•”ํ˜ธํ™”๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ์ผ๋ฐ˜ ํ…์ŠคํŠธ๋กœ ์ „๋‹ฌ๋˜๊ณ  ์ฝ์„ ์ˆ˜ ์—†๋Š” ์•”ํ˜ธํ™”๋กœ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๊ณ , ์ด๋ฅผ ๋‹ค์‹œ ํ•ด๋…ํ•˜์—ฌ ์ผ๋ฐ˜ ํ…์ŠคํŠธ๋กœ ์ฝ์„ ์ˆ˜ ์žˆ๋Š” ์–‘๋ฐฉํ–ฅ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. ์•”ํ˜ธํ™”๋Š” ํšŒ์›์˜ ์˜ˆ๋ฏผํ•  ์ˆ˜ ์žˆ๋Š” ์ •๋ณด๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. (์ด๋ฆ„, ์ฃผ์†Œ, ๊ณ„์ขŒ๋ฒˆํ˜ธ ๋“ฑ) ๋ฐ์ดํ„ฐ ๋…ธ์ถœ์„ ์ตœ์†Œํ™” ์‹œํ‚ค๊ธฐ ์œ„ํ•œ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

๋ฐ˜๋ฉด ํ•ด์‹ฑ์€ ๋‹จ๋ฐฉํ–ฅ์ž…๋‹ˆ๋‹ค. ํ•œ๋ฒˆ ์•”ํ˜ธํ™”ํ•˜๋ฉด ๋‹ค์‹œ ์ด๋ฅผ ํ‰๋ฌธํ™” ์‹œํ‚ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋Œ€์‹  ๊ฐ™์€ ๋ฌธ์ž์—ด์„ ๊ฐ™์€ ํ•ด์‹œ ํ•จ์ˆ˜๋ฅผ ๊ฑฐ์น˜๋ฉด ๊ฒฐ๊ณผ๊ฐ’์€ ๋Š˜ ๊ฐ™๋‹ค๋Š” ๊ฒƒ์„ ์ด์šฉํ•˜์—ฌ ๊ฐ’์ด ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•ด์‹ฑ์€ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋“ฑ ํ‰๋ฌธํ™” ํ•  ์ผ์ด ์—†๋Š” ์ค‘์š”ํ•œ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๋Š” ๋ฐ ์ฃผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์•”ํ˜ธํ™”์˜ ๋ชฉ์ ๊ณผ ๋‹ค๋ฅธ ์ ์€ ์›๋ž˜ ๋ฐ์ดํ„ฐ์™€ ๋น„๊ตํ•ด ๋ฐ์ดํ„ฐ ๋ณ€์กฐ๊ฐ€ ์—†์—ˆ๋Š”์ง€ ๋ฌด๊ฒฐํ•จ์„ ์ฆ๋ช…ํ•˜๋Š” ๊ฒƒ์ด ๋ชฉํ‘œ ๋ผ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์šฐ๋ฆฌ๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋˜์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

์„œ๋ฒ„์— ์žˆ๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์•”ํ˜ธํ™”๋œ ๊ฒฐ๊ณผ๊ฐ’(์ดํ•˜ ๋‹ค์ด์ œ์ŠคํŠธ, ํ•ด์‹œ๋ผ๋Š” ์šฉ์–ด๋กœ ํ‘œํ˜„ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.)์ผ ๋ฟ์ด์ง€ ์›๋ž˜ ํ˜•ํƒœ๋Š” ์˜์˜ ๋˜์ฐพ์„ ์ˆ˜ ์—†๋Š”๊ฑฐ์ฃ . ๊ทธ๋ž˜์„œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ฆฌ์…‹ํ•ด์ฃผ๊ฑฐ๋‚˜ ์ž„์‹œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

((( ๋งŒ์•ฝ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋˜์ฐพ์•„์ฃผ๋Š” ์‚ฌ์ดํŠธ๊ฐ€ ์žˆ๋‹ค๋ฉด ๋‹น์žฅ ํƒˆํ‡ดํ•˜๋„๋ก ํ•˜์„ธ์š”)))

์š”์ฆ˜์€ ๋ชป ๋”ฐ๋ผ๊ฐ„๋‹ค๋Š” ๊ทธ๋•Œ ๊ทธ ์‹œ์ ˆ ๊ฐฌ์„ฑ ๋“œ๋ผ๋งˆ | ์Šค๋ธŒ์Šค๋…ธ๋ฆฌํ„ฐ
๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ๋Œ์•„์˜ค์ง€ ์•Š๋Š” ๊ฑฐ์•ผ ๐Ÿชƒ

๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ•ด์‹ฑํ•œ๋‹ค๋‹ˆ! ํ•ด์‹œํ…Œ์ด๋ธ”์„ ๊ณต๋ถ€ํ•ด๋ณธ ๋ถ„๋“ค์ด๋ผ๋ฉด ๋‹ค๋“ค ์•Œ๊ฒ ์ง€๋งŒ ํ•ด์‹œ์ถฉ๋Œ(Hash Collision)์ด๋ผ๋Š” ๊ฒƒ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ•ด์‹œ ํ…Œ์ด๋ธ”์˜ ์ถœ๋ ฅ๊ฐ’์ด ์œ ํ•œํ•  ๊ฒฝ์šฐ ๋ถ„๋ช… ๋‹ค๋ฅธ ๊ฐ’์„ ๋„ฃ์—ˆ๋Š”๋ฐ ๊ฐ™์€ ์ถœ๋ ฅ๊ฐ’์ด ๋‚˜์˜ค๋Š” ๊ฒฝ์šฐ์ด์ฃ .

(์ด๊ฑฐ ์œ„ํ‚ค์—์„œ ๋‹ค๋“ค ๋ณด์…จ์ฃ ?)

๊ทธ๋ž˜์„œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” ํ•ด์‹ฑ์„ ํ•˜๋ฉด ์ถฉ๋Œ์ด ๋‚˜์ง€ ์•Š์„๊นŒ ์ž ๊น ๊ฑฑ์ •ํ–ˆ์ง€๋งŒ ๊ทธ๋Ÿด ํ™•๋ฅ ์€ ๋งค์šฐ๋งค์šฐ ์ ์€ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๐Ÿ‘‰ ์ฐธ๊ณ ๋งํฌ : Potential collision with hash password

์—ฌํ•˜ํŠผ ์•”ํ˜ธํ™”์™€ ํ•ด์‹ฑ์„ ๊ฐ€๋ณ๊ฒŒ ์ •๋ฆฌํ•˜์ž๋ฉด ๋‹ค์Œ์˜ ํ‘œ์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์•”ํ˜ธํ™” ํ•ด์‹ฑ
๋ณตํ˜ธํ™” ๊ฐ€๋Šฅ์„ฑ ๊ฐ€๋Šฅ ๋ถˆ๊ฐ€๋Šฅ
(๊ฒฐ๊ณผ๊ฐ’์˜) ๊ธธ์ด ๊ฐ€๋ณ€์  ๊ณ ์ • ๊ธธ์ด
์ฃผ์š” ์‚ฌ์šฉ ์•Œ๊ณ ๋ฆฌ์ฆ˜ AES, RC4, DES, RSA, ECDSAโ€ฆ SHA-1, SHA-2, MD5, CRC32โ€ฆ

๊ทธ๋Ÿผ ํ•ด์‹ฑ์ด ์™„๋ฒฝํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” ๋ฐฉ๋ฒ•์ผ๊นŒ?

๋ฌผ๋ก  ๋ชจ๋“  ์•”ํ˜ธ๋Š” ๋šซ๋ฆด ์ˆ˜ ์žˆ๊ฒ ์ง€๋งŒ, ํ•ด์‹ฑ์ด ๊ฐ€์ง€๋Š” ๋ฌธ์ œ์ ์ด ๋‘ ๊ฐ€์ง€ ์žˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ‘‰ ์ฐธ๊ณ ๋งํฌ D2

์ฒซ๋ฒˆ์งธ๋Š” ๋Š๋ฆฌ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ํ•ด์‹ฑ์€ ํ•ด์‹ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜๋งˆ๋‹ค ์†๋„๊ฐ€ ๋‹ค๋ฅธ๋ฐ์š”. ์ผ๋ฐ˜์ ์œผ๋กœ ๊ฐ•๋ ฅํ•  ์ˆ˜๋ก ๋” ๋Š๋ฆฝ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์ž์ฃผ ์“ฐ์˜€๋˜ SHA-1 ์•Œ๊ณ ๋ฆฌ์ฆ˜๊ณผ ์ตœ๊ทผ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” PBKDF2๋ฅผ ๋น„๊ตํ•ด๋ณธ๋‹ค๋ฉด ํ›„์ž ์ชฝ์ด ๋ธŒ๋ฃจํŠธ ํฌ์Šค ๊ณต๊ฒฉ์— ๋” ๊ฐ•๋ ฅํ•˜์ง€๋งŒ ์†๋„๋Š” ๋งค์šฐ ๋Š๋ฆฝ๋‹ˆ๋‹ค.

์˜๋„์ ์œผ๋กœ ํ•ด์‹ฑ ํšŸ์ˆ˜๋ฅผ ์ถ”์ธกํ•  ์ˆ˜ ์—†๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋Š๋ฆฌ๊ฒŒ ํ•˜๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ! ๋ณด์•ˆ์„ ์œ„ํ•ด์„œ ๋ช‡ ์ดˆ์˜ ๊ธฐ๋‹ค๋ฆผ์€ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ทธ๋‹ฅ ๋ฌธ์ œ๊ฐ€ ๋˜์ง€ ์•Š์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๋‘๋ฒˆ์งธ๋Š” ๋” ์‹ฌ๊ฐํ•œ ๋ฌธ์ œ์ธ ์ธ์‹ ๊ฐ€๋Šฅ์„ฑ์ž…๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‹ˆ๊นŒ ์ž์ฃผ ์“ฐ๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ์˜ ๊ฒฝ์šฐ์—๋Š” ๊ทธ๋ƒฅ ๋‹ค์ด์ œ์ŠคํŠธ(ํ•ด์‹œ ํ•จ์ˆ˜๋ฅผ ๊ฑฐ์ณค์„ ๋•Œ ๊ฒฐ๊ณผ ๊ฐ’)๋ฅผ ๋ฏธ๋ฆฌ ํ‘œ๋กœ ์ •๋ฆฌํ•ด๋‘˜ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋™์ผํ•œ ๋ฉ”์‹œ์ง€์— ๋Œ€ํ•ด์„œ ๋Š˜ ๋™์ผํ•œ ๋‹ค์ด์ œ์ŠคํŠธ๋ฅผ ๊ฐ–๊ธฐ ๋•Œ๋ฌธ์—, ํ•ด์ปค๊ฐ€ ๋‹ค์ด์ œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€๋Šฅํ•œ ๋งŽ์ด ํ™•๋ณดํ•œ ๋‹ค์Œ ์ด๋ฅผ ํƒˆ์ทจํ•œ ๋ฐ์ดํ„ฐ ๋ฒ ์ด์Šค ๋ฐ์ดํ„ฐ์™€ ๋น„๊ตํ•˜์—ฌ ์›๋ณธ ๋ฉ”์‹œ์ง€๋ฅผ ์ฐพ์•„๋‚ผ ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š”๊ฑฐ์ฃ . ์–ด๋–ค ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ–ˆ๋Š”์ง€ ์•ˆ๋‹ค๋ฉด์š”.

์ด๋Ÿฌํ•œ ๋‹ค์ด์ œ์ŠคํŠธ๋ฅผ ๋ชจ์•„์„œ ๋ ˆ์ธ๋ณด์šฐ ํ…Œ์ด๋ธ”์ด๋ผ๊ณ  ํ•˜๋Š”๋ฐ์š”. ํ•˜๋‚˜์˜ ํŒจ์Šค์›Œ๋“œ์—์„œ ์‹œ์ž‘ํ•ด ๋ณ€์ด๋œ ํ˜•ํƒœ์˜ ์—ฌ๋Ÿฌ ํŒจ์Šค์›Œ๋“œ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๊ทธ ํŒจ์Šค์›Œ๋“œ์˜ ํ•ด์‹œ๋ฅผ ๊ณ ๋ฆฌ์ฒ˜๋Ÿผ ์—ฐ๊ฒฐํ•ด ์ผ์ • ์ˆ˜์˜ ํŒจ์Šค์›Œ๋“œ์™€ ํ•ด์‹œ๋กœ ์ด๋ฃจ์–ด์ง„ ํ…Œ์ด๋ธ”์ž…๋‹ˆ๋‹ค. ๐Ÿ‘‰ ์ฐธ๊ณ  ๋งํฌ

์ด๋ฏธ์ง€ ์ถœ์ฒ˜ : https://www.thesecurityblogger.com/understanding-rainbow-tables/

์ด๋ฅผ ์ด์šฉํ•˜๋ฉด ๋ธŒ๋ฃจํŠธ ํฌ์Šค ๊ณต๊ฒฉ์ด ๋” ์‰ฌ์›Œ์ง€๊ฒ ์ฃ .

์•„๋ž˜๋Š” SHA-1์„ ์‚ฌ์šฉํ–ˆ์„ ๋•Œ ๋ ˆ์ธ๋ณด์šฐ ํ…Œ์ด๋ธ”์˜ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 a
e9d71f5ee7c92d6dc9e92ffdad17b8bd49418f98 b
84a516841ba77a5b4648de2cd0dfcb30ea46dbb4 c
...
948291f2d6da8e32b007d5270a0a5d094a455a02 ZZZZZX
151bfc7ba4995bfa22c723ebe7921b6ddc6961bc ZZZZZY
18f30f1ba4c62e2b460e693306b39a0de27d747c ZZZZZZ

5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8 password
e38ad214943daad1d64c102faec29de4afe9da3d password1
b7a875fc1ea228b9061041b7cec4bd3c52ab3ce3 letmein
5cec175b165e3d5e62c9e13ce848ef6feac81bff qwerty123

Salt, Pepper, Key Stretch ๐Ÿ˜ต

์œ„์—์„œ ์–ธ๊ธ‰ํ•œ ์ธ์‹ ๊ฐ€๋Šฅ์„ฑ์— ๋Œ€ํ•œ ๋‹จ์ ์„ ๋ณด์™„ํ•˜๊ธฐ ์œ„ํ•ด ์†”ํŠธSalt, ํŽ˜ํผpepper, ์ŠคํŠธ๋ ˆ์น˜Key Stretch ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜๋Š”๋ฐ์š”. ์ด๊ฒƒ์— ๋Œ€ํ•ด์„œ๋„ ๊ฐ„๋‹จํžˆ ๋‹ค๋ฃจ์–ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

์ด ์„ธ๊ฐ€์ง€๊ฐ€ ์ ์˜๋œ ํ•ด์‹œ๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ˆ˜์‹์œผ๋กœ ํ‘œํ˜„ํ•˜์ž๋ฉด ์ด๋ ‡๊ฒŒ ๋  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

pepper = {came from somewhere}
salt = {generated random}
hash = H(H(H(H(H(H(H(H(H(H(H(H(H(H(H(...H(password + salt + pepper) + salt + pepper) + salt + pepper) ... )

Salt

์†”ํŠธ Salt๋Š” ๋‹จ๋ฐฉํ–ฅ ํ•ด์‹ฑ์‹œ ๋‹ค์ด์ œ์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ์ถ”๊ฐ€๋˜๋Š” ๋ฐ”์ดํŠธ ๋‹จ์œ„์˜ ์ž„์˜ ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค. ๋ณดํ†ต 128๋น„ํŠธ ์ด์ƒ์œผ๋กœ ๋žœ๋ค ๋ฌธ์ž์—ด์„ ์ƒ์„ฑํ•˜์—ฌ ์‚ฌ์šฉํ•˜๊ณ  ์ด ์ƒ์„ฑ๋˜๋Š” ๋ฌธ์ž์—ด์€ ํšŒ์›๋งˆ๋‹ค ๊ฐ๊ฐ ๋‹ค๋ฅด๊ฒŒ ์ ์šฉํ•œ๋‹ค๋ฉด ๋”์šฑ ๊ฐ•๋ ฅํ•˜๊ฒŒ ์ž‘์šฉํ•ฉ๋‹ˆ๋‹ค.

์†”ํŠธ์˜ ๋ชฉ์ ์€ ํ•ด์ปค๊ฐ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ํƒˆ์ทจํ•˜๊ธฐ ์ „์— ๋ ˆ์ธ๋ณด์šฐ ํ…Œ์ด๋ธ”๊ณผ ๊ฐ™์€ ์˜ˆ์ธก๊ฐ€๋Šฅํ•œ ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•˜์ง€ ๋ชปํ•˜๋Š” ๋ฐ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์†”ํŠธ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ•˜๊ณ  ์ด๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋Š” ์œ ์ผํ•œ ๋ฐฉ๋ฒ•์„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ํ•ฉ๋‹ˆ๋‹ค.

Pepper

ํŽ˜ํผ Pepper๋Š” ๋ชจ๋“  ๋น„๋ฐ€๋ฒˆํ˜ธ์— ์ผ์ •ํ•˜๊ฒŒ ์ ์šฉ๋˜์ง€๋งŒ โœŒ๏ธ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—๋Š” ์ €์žฅํ•˜์ง€ ์•Š๋Š”โœŒ๏ธ ๋˜๋‹ค๋ฅธ ์†Œ๊ธˆ์ž…๋‹ˆ๋‹ค.

๋ชจ๋‘์—๊ฒŒ ๋™์ผํ•˜๊ฒŒ ์ ์šฉ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์ด ๋ฐฉ๋ฒ•์ด ์œ ํšจํ•˜์ง€ ์•Š๋‹ค๋Š” ์˜๊ฒฌ๋„ ์žˆ์Šต๋‹ˆ๋‹ค๋งŒ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™ธ์— ๋‹ค๋ฅธ ์„ค์ • ํŒŒ์ผ์„ ํ†ตํ•ด์„œ ๋„ฃ์–ด์ฃผ๋Š” ํŽ˜ํผ๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ์— ๋Œ€ํ•œ ์•ˆ์ „์„ฑ์„ ๋” ๋†’์ผ ์ˆ˜ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Key Stretch

salt์™€ pepper์™€ ๋ณธ๋ž˜ ๋ฌธ์ž์—ด์„ ์—ฌ๋Ÿฌ๋ฒˆ ํ•ด์‹œ ํ•จ์ˆ˜๋ฅผ ๊ฑฐ์ณ ๋‹ค์ด์ œ์ŠคํŠธ๋ฅผ ์ค‘์ ‘ํ•˜์—ฌ ์ƒ์„ฑํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ์ƒ์„ฑ๋œ ๋‹ค์ด์ œ์ŠคํŠธ๋Š” ๋ธŒ๋ฃจํŠธ ํฌ์Šค ๊ณต๊ฒฉ์‹œ ์‹œ๊ฐ„์ด ๋” ๊ฑธ๋ฆฌ๋„๋ก ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฏธ์ง€ ์ถœ์ฒ˜ : https://d2.naver.com/helloworld/318732

๊ทธ๋ฆฌ๊ณ  ๋” ์–ด๋ ต๊ฒŒ!

๋‹ค์ด์ œ์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ์†”ํŠธ์™€ ํŒจ์Šค์›Œ๋“œ ์™ธ์—๋„ ๋˜ ๋‹ค๋ฅธ ๊ฐ’์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋‹ค์ด์ œ์ŠคํŠธ๋ฅผ ์ถ”์ธกํ•˜๊ธฐ ๋” ์–ด๋ ต๊ฒŒ ๊ฐ•๋ ฅํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋Œ€ํ‘œ์ ์ธ ์˜ˆ๋กœ KDF(Key Derivation Function)๊ฐ€ ์žˆ๋Š”๋ฐ์š”. ๋น„๋ฐ€๋ฒˆํ˜ธ๋‚˜ ์†”ํŠธ ๋“ฑ์—์„œ ํ‚ค๋ฅผ ํŒŒ์ƒ์‹œ์ผœ ์•”ํ˜ธํ™” ์•Œ๊ณ ๋ฆฌ์ฆ˜์— ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ‘‰ ์ฐธ๊ณ ๋งํฌ

hash = KDF(password, salt, workFactor)

๊ฐ€์žฅ ๋„๋ฆฌ ์‚ฌ์šฉ๋˜๋Š” ๋‘ ๊ฐ€์ง€ KDF๋Š” PBKDF2 ์™€ bcrypt ์ž…๋‹ˆ๋‹ค. PBKDF2๋Š” ํ‚ค๊ฐ€ ์žˆ๋Š” HMAC (๋ธ”๋ก ์•”ํ˜ธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ) ์˜ ๋ฐ˜๋ณต์„ ์ˆ˜ํ–‰ํ•˜์—ฌ ์ž‘๋™ํ•˜๋ฉฐ bcrypt๋Š” Blowfish ๋ธ”๋ก ์•”ํ˜ธ์—์„œ ๋งŽ์€ ์ˆ˜์˜ ์•”ํ˜ธ๋ฌธ ๋ธ”๋ก์„ ๊ณ„์‚ฐํ•˜๊ณ  ๊ฒฐํ•ฉํ•˜์—ฌ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

Spring ์—์„œ ๊ตฌํ˜„ํ•ด๋ณด์ž

ํ…Œ์ด๋ธ” ๊ตฌ์กฐ

๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ DB์— ์ง์ ‘์ ์œผ๋กœ ์ €์žฅํ•˜์ง€ ์•Š๊ณ , ํ•ด์‹œ์™€ ์†”ํŠธ๋ฅผ ์ €์žฅํ•˜์—ฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ์„ ํ•˜๋Š” ํ˜•ํƒœ๋กœ ๊ธฐํšํ•ด๋ณด์•˜๋Š”๋ฐ์š”. ํŠนํžˆ ํšŒ์›๊ฐ€์ž…๊ณผ ๋กœ๊ทธ์ธ ์™ธ์—๋Š” ํšŒ์› ๋„๋ฉ”์ธ๊ณผ ๋ณ„๋„๋กœ ์›€์ง์ผ ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐ๋˜์–ด ๋ณ„๋„ ํ…Œ์ด๋ธ”๋กœ ERD๋ฅผ ์งœ๋ณด์•˜์Šต๋‹ˆ๋‹ค.

  • member_password ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ €์žฅํ•˜๋Š” ํ…Œ์ด๋ธ”์ž…๋‹ˆ๋‹ค.
    • member ์™€ ์‹๋ณ„๊ด€๊ณ„๋กœ ์„ค๊ณ„ํ•ด๋„ ๊ดœ์ฐฎ์•˜๊ฒ ์ง€๋งŒ member_auth ์™€ ํ†ต์ผ๊ฐ์žˆ๊ฒŒ ์„ค๊ณ„ํ•˜๊ณ  ์‹ถ์–ด ๋น„์‹๋ณ„ ๊ด€๊ณ„๋กœ ์„ค๊ณ„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
    • member ์™€๋Š” 1:1 ๊ด€๊ณ„์ž…๋‹ˆ๋‹ค. (ERD๊ฐ€ ์กฐ๊ธˆ ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค ๐Ÿฅฒ)

Entity ๊ตฌํ˜„

ORM์€ Spring Data JPA๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ํŠน์ด์‚ฌํ•ญ์€ ์—†๊ณ  ํ…Œ์ด๋ธ” ๋ ˆ์ฝ”๋“œ์™€ ๋งคํ•‘์ด ๋  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

@Entity
@Table(name = "member")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SecondaryTable(name = "member_password", pkJoinColumns = @PrimaryKeyJoinColumn(name = "member_id"))
public class Member extends CreatedEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Email
    private String email;
    @Embedded
    private Password password = new Password();
    private String username;
}
@Getter
@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Password {

    @Column(table = "member_password")
    private String hash;
    @Column(columnDefinition = "BINARY(16)", table = "member_password")
    private byte[] salt;

    public static Password of (String hash, byte[] salt) {
        return new Password(hash, salt);
    }
}

PasswordEncoder

์ธํ„ฐํŽ˜์ด์Šค๋กœ ๋จผ์ € ์—ญํ• ์„ ๊ทœ์ •ํ•ด์ค€ ๋’ค, ์•Œ๊ณ ๋ฆฌ์ฆ˜๋งˆ๋‹ค ํ•˜์œ„ ๊ตฌํ˜„ ํด๋ž˜์Šค๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋‹จ๋ฐฉํ–ฅ ์•”ํ˜ธํ™”์‹œ ๋งŽ์ด ์“ฐ์ด๋Š” PBKDF2 ๋ฅผ ์ด์šฉํ•˜๋Š” ์ธ์ฝ”๋”๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ด๋ฆ„์ธ "PBKDF2WithHmacSHA1", ๋ฐ˜๋ณต ํšŒ์ˆ˜์™€ ํ‚ค ๊ธธ์ด๋Š” ์™ธ๋ถ€ ๋ณ€์ˆ˜๋กœ ์ฃผ์ž…๋ฐ›์•˜์Šต๋‹ˆ๋‹ค. ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ์ž์˜ ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋”ฐ๋ผ ๋ณ€์ˆ˜๋ฅผ ์ •ํ–ˆ๊ณ  ํŽ˜ํผ๋Š” ๋”ฐ๋กœ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

@Component
public class Pbkdf2Encoder implements PasswordEncoder {
    private final String encodeAlgorithm; // ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ด๋ฆ„
    private final int iterations; // ๋ฐ˜๋ณต ํšŸ์ˆ˜
    private final int keyLength; // ํ‚ค ๊ธธ์ด

    public Pbkdf2Encoder(
            @Value("${password.encode.algorithm}") String encodeAlgorithm,
            @Value("${password.encode.iterations}") int iterations,
            @Value("${password.encode.keyLength}") int keyLength) {
        this.encodeAlgorithm = encodeAlgorithm;
        this.iterations = iterations;
        this.keyLength = keyLength;
    }

    @Override
    public Password encrypt(String password) {
        byte[] saltBytes = generateSalt();
        return encrypt(password, saltBytes);
    }

    public Password encrypt(String password, byte[] saltBytes) {
        byte[] hashBytes;
        KeySpec spec = new PBEKeySpec(password.toCharArray(), saltBytes, iterations, keyLength);
        try {
            SecretKeyFactory factory = SecretKeyFactory.getInstance(encodeAlgorithm);
            hashBytes = factory.generateSecret(spec).getEncoded();
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new RuntimeException("Password encryption failed.", e);
        }
        String hash = new String(Base64.getEncoder().encode(hashBytes));
        return Password.of(hash, saltBytes);
    }
		
		// ๋žœ๋ค salt๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
    private byte[] getSalt() {
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[16];
        random.nextBytes(salt);
        return salt;
    }

}
  • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
    @Test
    @DisplayName("๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•”ํ˜ธํ™” ํ•  ์ˆ˜ ์žˆ๋‹ค.")
    void testPasswordEncoder() throws Exception{
        //given
        String password = "password1234";
        
        //when
        Password encrypt = passwordEncoder.encrypt(password);
        log.info(encrypt.toString());
    }

    @Test
    @DisplayName("๊ฐ™์€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๊ฐ™์€ salt๋กœ ์•”ํ˜ธํ™”ํ•˜๋ฉด ํ•ด์‹œ๊ฐ€ ๊ฐ™์•„์•ผ ํ•œ๋‹ค.")
    void testPassword() throws Exception{
        //given
        String inputPassword1 = "password1234";
        String inputPassword2 = "password1234";

        //when
        Password password1 = passwordEncoder.encrypt(inputPassword1);
        byte[] salt = password1.getSalt();
        Password password2 = passwordEncoder.encrypt(inputPassword2, salt);

        assertThat(password1.getHash()).isEqualTo(password2.getHash());
        assertThat(password1).isEqualTo(password2);
    }โ€‹

PasswordValidator

์•”ํ˜ธ๋ฅผ ์ฒดํฌํ•˜๋Š” ์—ญํ• ์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋”ฐ๋กœ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์—ญํ• ์ด ์ž‘๊ธฐ ๋•Œ๋ฌธ์— PasswordEncoder ์— ๊ฐ™์ด ์ฒดํฌ ๋ฉ”์„œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•ด๋„ ๋ฌด๋ฐฉํ•  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

@Component
@RequiredArgsConstructor
public class PasswordValidator {

    private final PasswordEncoder passwordEncoder;

    public boolean check(Member member, String inputPassword) {
        return passwordEncoder.encrypt(inputPassword, member.getSalt())
                .equals(member.getPassword());
    }
}

  • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
    @BeforeEach
    void init() {
        Password password = passwordEncoder.encrypt("myPassword");

        testMember = Member.builder()
                .id(1L)
                .email("test@gmail.com")
                .username("ํšŒ์›")
                .password(password)
                .build();
    }

    @Test
    @DisplayName("ํšŒ์› ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋งž๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.")
    void isCorrectPassword() {
        byte[] salt = testMember.getSalt();
        Password otherPassword = passwordEncoder.encrypt("myPassword", salt);

        assertThat(testMember.isCorrectPassword(otherPassword)).isTrue();
    }

    @Test
    @DisplayName("ํšŒ์› ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ‹€๋ฆฐ์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.")
    void isIncorrectPassword() {
        byte[] salt = testMember.getSalt();
        Password otherPassword = passwordEncoder.encrypt("wrongPassword", salt);

        assertThat(testMember.isCorrectPassword(otherPassword)).isFalse();
    }

๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

๊ตฌํ˜„ ํ•˜๋ฉด์„œ Spring Security๋Š” ์–ด๋–ป๊ฒŒ ํ•ด์‹œ์™€ ์†”ํŠธ๋ฅผ ์ €์žฅํ•˜๊ณ  ์žˆ๋Š”์ง€ ๊ถ๊ธˆํ–ˆ๋Š”๋ฐ์š”.

์ž์ฃผ ์“ฐ์ด๋Š” BCryptPasswordEncoder์˜ ๊ฒฝ์šฐ, ๋‚ด๋ถ€์ ์œผ๋กœ ์ž„์˜ ์†”ํŠธ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ํ•ด์‹œ ๋‚ด์— ์ €์žฅํ•˜๊ณ  ์žˆ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ‘‰ ์ฐธ๊ณ ๋งํฌ

$2a$10$ZLhnHxdpHETcxmtEStgpI./Ri1mksgJ9iDP36FmfMdYyVg9g0b2dq

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


Refs.