๐ ์ ํ๋ฆฌ์ผ์ด์ ๋ณด์ ์์ ์ ๋ณต: ์น๊ตฌ์ฒ๋ผ ์๋ ค์ฃผ๋ ์์ ํ ์ธ์ ๊ด๋ฆฌ ๊ธฐ๋ฒ (2025๋ ์ต์ ํ) ๐

์๋ ! ์ค๋์ 2025๋ 3์์ ๋ง์ ์น/์ฑ ๊ฐ๋ฐ์๋ผ๋ฉด ๊ผญ ์์์ผ ํ ์ธ์ ๊ด๋ฆฌ ๋ณด์ ๊ธฐ๋ฒ์ ๋ํด ์น๊ตฌ์ฒ๋ผ ์ฝ๊ฒ ์ค๋ช ํด์ค๊ฒ. ๐ค ์์ฆ ๊ฐ์ ๋์งํธ ์๋์ ๋ณด์์ ์ ํ์ด ์๋ ํ์์์. ํนํ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ค๋ฃจ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ด๋ผ๋ฉด ๋๋์ฑ!
์ด ๊ธ์ ํตํด ์ธ์ ๊ด๋ฆฌ์ ๊ธฐ๋ณธ๋ถํฐ ์ต์ ๋ณด์ ๊ธฐ๋ฒ๊น์ง ๋ฐฐ์ฐ๊ณ ๋๋ฉด, ๋์ ์ ํ๋ฆฌ์ผ์ด์ ์ ํจ์ฌ ๋ ์์ ํด์ง ๊ฑฐ์ผ. ์ฌ๋ฅ๋ท ๊ฐ์ ์ฌ์ฉ์ ์ ๋ณด์ ์ฌ๋ฅ์ ๊ฑฐ๋ํ๋ ํ๋ซํผ์์๋ ์ด๋ฐ ๋ณด์ ๊ธฐ์ ์ด ์ผ๋ง๋ ์ค์ํ์ง ์ ์ ์์ง! ์, ์ด์ ์์ํด๋ณผ๊น? ๐
๐ ๋ชฉ์ฐจ
- ์ธ์ ์ด ๋ญ์ผ? ๊ธฐ๋ณธ ๊ฐ๋ ์ดํดํ๊ธฐ
- ์ธ์ ๊ด๋ฆฌ๊ฐ ์ ์ค์ํ ๊น?
- ์ธ์ ๊ด๋ จ ์ทจ์ฝ์ ๊ณผ ๊ณต๊ฒฉ ์ ํ
- ์์ ํ ์ธ์ ID ์์ฑ ๋ฐฉ๋ฒ
- ์ธ์ ์๋ช ๊ด๋ฆฌํ๊ธฐ
- ์ฟ ํค ๋ณด์ ์ค์ ์ ๋ชจ๋ ๊ฒ
- HTTPS์ ์ธ์ ๋ณด์
- ๋ค์ค ์์ ์ธ์ฆ(MFA)๊ณผ ์ธ์
- JWT๋ฅผ ํ์ฉํ ํ๋์ ์ธ์ ๊ด๋ฆฌ
- OAuth 2.0๊ณผ OpenID Connect
- ๋ชจ๋ฐ์ผ ์ฑ์์์ ์ธ์ ๊ด๋ฆฌ
- 2025๋ ์ต์ ์ธ์ ๊ด๋ฆฌ ํธ๋ ๋
- ์ค์ ๊ตฌํ ์์ ์ ์ฝ๋
- ๋ง๋ฌด๋ฆฌ ๋ฐ ์ฒดํฌ๋ฆฌ์คํธ
1. ์ธ์ ์ด ๋ญ์ผ? ๊ธฐ๋ณธ ๊ฐ๋ ์ดํดํ๊ธฐ ๐ง
์ธ์ ์ด ๋ญ์ง ๊ฐ๋จํ ์ค๋ช ํด๋ณผ๊ฒ. ์น์ฌ์ดํธ๋ ์ฑ์ ์ฌ์ฉํ ๋, ๋์ ์๋ฒ ์ฌ์ด์ ์ผ์ข ์ '๋ํ'๊ฐ ์ด๋ฃจ์ด์ง๋๋ฐ, ์ด ๋ํ๋ฅผ ๊ธฐ์ตํ๋ ๋ฐฉ๋ฒ์ด ๋ฐ๋ก '์ธ์ '์ด์ผ. ๐ฃ๏ธ
์ธ์ (Session)์ด๋? ์ฌ์ฉ์๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ๊ณผ ์ํธ์์ฉํ๋ ๋์ ์ ์ง๋๋ ์ ๋ณด์ ๋จ์๋ก, ์ฌ์ฉ์๋ฅผ ์๋ณํ๊ณ ์ํ๋ฅผ ์ ์งํ๋ ๋ฉ์ปค๋์ฆ์ด์ผ.
HTTP๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ํ๊ฐ ์๋(Stateless) ํ๋กํ ์ฝ์ด์ผ. ์ฆ, ๊ฐ ์์ฒญ์ ๋ ๋ฆฝ์ ์ด๊ณ ์ด์ ์์ฒญ์ ๋ํ ์ ๋ณด๋ฅผ ๊ธฐ์ตํ์ง ์์. ๊ทผ๋ฐ ์ฐ๋ฆฌ๊ฐ ๋ก๊ทธ์ธํ๊ณ ์ฅ๋ฐ๊ตฌ๋์ ๋ฌผ๊ฑด์ ๋ด๋ ๋ฑ์ ์์ ์ ํ ๋๋ ์ํ ์ ์ง๊ฐ ํ์ํ์์? ์ด๋ ์ธ์ ์ด ๋ฑ์ฅํ๋ ๊ฑฐ์ง! ๐
์ธ์ ์๋ ๋ฐฉ์
์ธ์ ์ ๊ธฐ๋ณธ ์๋ ๊ณผ์ ์ ๋ค์๊ณผ ๊ฐ์:
- ์ธ์ ์์ฑ: ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธํ๋ฉด ์๋ฒ๋ ๊ณ ์ ํ ์ธ์ ID๋ฅผ ์์ฑํด.
- ์ธ์ ์ ์ฅ: ์๋ฒ๋ ์ด ์ธ์ ID์ ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ์๋ฒ ์ธก์ ์ ์ฅํด.
- ์ธ์ ID ์ ๋ฌ: ์๋ฒ๋ ์ธ์ ID๋ฅผ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๋ฌํด (๋ณดํต ์ฟ ํค๋ฅผ ํตํด).
- ํ์ ์์ฒญ: ํด๋ผ์ด์ธํธ๋ ์ดํ ๋ชจ๋ ์์ฒญ์ ์ด ์ธ์ ID๋ฅผ ํฌํจ์์ผ.
- ์ธ์ ํ์ธ: ์๋ฒ๋ ์์ฒญ์ ํฌํจ๋ ์ธ์ ID๋ฅผ ํ์ธํ๊ณ ํด๋น ์ฌ์ฉ์์ ๋ฐ์ดํฐ์ ์ ๊ทผํด.
์ธ์ vs ์ฟ ํค vs ํ ํฐ
์ธ์ , ์ฟ ํค, ํ ํฐ... ํท๊ฐ๋ฆฌ์ง? ๊ฐ๋จํ ๋น๊ตํด๋ณผ๊ฒ! ๐
๊ตฌ๋ถ | ์ ์ฅ ์์น | ๋ณด์ ์์ค | ํ์ฅ์ฑ | ์ฉ๋ |
---|---|---|---|---|
์ธ์ | ์๋ฒ | ๋์ | ๋ฎ์ | ์ฌ์ฉ์ ์ธ์ฆ, ์ํ ์ ์ง |
์ฟ ํค | ํด๋ผ์ด์ธํธ | ๋ฎ์ | ์ค๊ฐ | ์ฌ์ฉ์ ์ค์ , ์ถ์ |
ํ ํฐ(JWT) | ํด๋ผ์ด์ธํธ | ์ค๊ฐ~๋์ | ๋์ | API ์ธ์ฆ, ๋ง์ดํฌ๋ก์๋น์ค |
์ด์ ์ธ์ ์ด ๋ญ์ง ์์์ผ๋, ์ ์ด๊ฒ ์ค์ํ์ง ์์๋ณผ๊น? ๐ง
2. ์ธ์ ๊ด๋ฆฌ๊ฐ ์ ์ค์ํ ๊น? ๐
์ธ์ ๊ด๋ฆฌ๊ฐ ์ค์ํ ์ด์ ๋ ํ๋ง๋๋ก "๋ณด์๊ณผ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ท ํ" ๋๋ฌธ์ด์ผ. ์ ๋๋ก ๊ด๋ฆฌ๋์ง ์์ ์ธ์ ์ ํด์ปค๋ค์ ๋์ดํฐ๊ฐ ๋ ์ ์์ด! ๐ฑ
๊ฒฝ๊ณ ! ์ทจ์ฝํ ์ธ์ ๊ด๋ฆฌ๋ ๊ณ์ ํ์ทจ, ๋ฐ์ดํฐ ์ ์ถ, ์๋น์ค ๊ฑฐ๋ถ ๊ณต๊ฒฉ ๋ฑ ์ฌ๊ฐํ ๋ณด์ ๋ฌธ์ ๋ฅผ ์ผ์ผํฌ ์ ์์ด.
2024๋ OWASP(Open Web Application Security Project) Top 10์์๋ '์ทจ์ฝํ ์ธ์ฆ ๋ฐ ์ธ์ ๊ด๋ฆฌ'๋ ์ฌ์ ํ ์์๊ถ์ ๋ญํฌ๋์ด ์์ด. ํนํ ์ฌ๋ฅ๋ท๊ณผ ๊ฐ์ด ์ฌ์ฉ์ ๊ฐ ์ฌ๋ฅ๊ณผ ์๋น์ค๋ฅผ ๊ฑฐ๋ํ๋ ํ๋ซํผ์์๋ ๋์ฑ ์ค์ํ์ง. ๋ค๋ฅธ ์ฌ๋์ ๊ณ์ ์ ํ์ทจํ๋ค๋ฉด? ์๊ฐ๋ง ํด๋ ์์ฐํ์ง ์์? ๐ธ
์ธ์ ๊ด๋ฆฌ์ ์ค์์ฑ
์ธ์ ๊ด๋ฆฌ๊ฐ ์ค์ํ ๊ตฌ์ฒด์ ์ธ ์ด์ ๋ค์ ์ดํด๋ณผ๊ฒ:
- ์ฌ์ฉ์ ์ธ์ฆ ์ ์ง: ๋ก๊ทธ์ธ ์ํ๋ฅผ ์ ์งํด์ ๋งค๋ฒ ์ธ์ฆ ์ ๋ณด๋ฅผ ์ ๋ ฅํ์ง ์์๋ ๋ผ.
- ๊ฐ์ธํ๋ ๊ฒฝํ ์ ๊ณต: ์ฌ์ฉ์๋ณ ์ค์ , ์ ํธ๋, ์ฅ๋ฐ๊ตฌ๋ ๋ฑ์ ๊ธฐ์ตํ ์ ์์ด.
- ๋ฌด๋จ ์ ๊ทผ ๋ฐฉ์ง: ์ธ์ฆ๋ ์ฌ์ฉ์๋ง ํน์ ๋ฆฌ์์ค์ ์ ๊ทผํ ์ ์๋๋ก ์ ํํด.
- ์ธ์ ํ์ด์ฌํน ๋ฐฉ์ง: ์ ๋๋ก ๋ ์ธ์ ๊ด๋ฆฌ๋ ์ธ์ ํ์ทจ ๊ณต๊ฒฉ์ ๋ง์์ค.
- CSRF ๊ณต๊ฒฉ ๋ฐฉ์ด: ์ฌ์ดํธ ๊ฐ ์์ฒญ ์์กฐ ๊ณต๊ฒฉ์ผ๋ก๋ถํฐ ์ฌ์ฉ์๋ฅผ ๋ณดํธํด.
- ๊ท์ ์ค์: GDPR, CCPA ๊ฐ์ ๊ฐ์ธ์ ๋ณด ๋ณดํธ๋ฒ์ ์ค์ํ๋ ๋ฐ ๋์์ด ๋ผ.
๐ก ์๊ณ ์๋? 2024๋ ๊ธฐ์ค์ผ๋ก ์ธ์ ๊ด๋ จ ์ทจ์ฝ์ ์ ์ ์ฒด ์น ์ ํ๋ฆฌ์ผ์ด์ ๋ณด์ ์ฌ๊ณ ์ ์ฝ 30%๋ฅผ ์ฐจ์งํด. ๊ทธ๋งํผ ์ค์ํ๋ค๋ ๋ป์ด์ง!
์ด์ ์ธ์ ๊ด๋ฆฌ๊ฐ ์ผ๋ง๋ ์ค์ํ์ง ์์์ผ๋, ์ด๋ค ์ํ์ด ์๋์ง ์ดํด๋ณผ๊น? ๐ต๏ธโโ๏ธ
3. ์ธ์ ๊ด๋ จ ์ทจ์ฝ์ ๊ณผ ๊ณต๊ฒฉ ์ ํ ๐จ
์ธ์ ๊ด๋ฆฌ์ ๋ฌธ์ ๊ฐ ์์ผ๋ฉด ํด์ปค๋ค์ด ์ข์ํ๋ ๊ณต๊ฒฉ ๋์์ด ๋ผ. ์ฃผ์ ๊ณต๊ฒฉ ์ ํ๋ค์ ์์๋ณด์!
์ธ์ ํ์ด์ฌํน (Session Hijacking)
์ธ์ ํ์ด์ฌํน์ ๊ณต๊ฒฉ์๊ฐ ์ ํจํ ์ธ์ ID๋ฅผ ํ์ณ ์ฌ์ฉ์์ธ ๊ฒ์ฒ๋ผ ํ๋ํ๋ ๊ณต๊ฒฉ์ด์ผ. ๋ง์น ๋ค ์ด์ ๋ฅผ ํ์ณ์ ๋ค ์ง์ ๋ค์ด๊ฐ๋ ๊ฒ๊ณผ ๊ฐ์ง! ๐
์ธ์ ํ์ด์ฌํน์ ์ฃผ์ ๋ฐฉ๋ฒ๋ค:
- ํจํท ์ค๋ํ: ๋คํธ์ํฌ ํธ๋ํฝ์ ๊ฐ์ํด ์ธ์ ID๋ฅผ ํ์ทจํด.
- ์ค๊ฐ์ ๊ณต๊ฒฉ(MITM): ํด๋ผ์ด์ธํธ์ ์๋ฒ ์ฌ์ด์ ํต์ ์ ๊ฐ๋ก์ฑ.
- XSS(Cross-Site Scripting): ์ ์ฑ ์คํฌ๋ฆฝํธ๋ฅผ ์ฝ์ ํด ์ฟ ํค๋ฅผ ํ์ณ.
- ์ธ์ ๊ณ ์ ๊ณต๊ฒฉ: ๊ณต๊ฒฉ์๊ฐ ์์ ์ ์ธ์ ID๋ฅผ ํผํด์์๊ฒ ๊ฐ์ ๋ก ์ฌ์ฉํ๊ฒ ํจ.
CSRF (Cross-Site Request Forgery)
CSRF๋ ์ฌ์ฉ์๊ฐ ์์ ๋ ๋ชจ๋ฅด๊ฒ ๊ณต๊ฒฉ์๊ฐ ์๋ํ ํ๋์ ์ํํ๋๋ก ์์ด๋ ๊ณต๊ฒฉ์ด์ผ. ์๋ฅผ ๋ค์ด, ๋ค๊ฐ ๋ก๊ทธ์ธํ ์ํ์์ ์ ์ฑ ์ฌ์ดํธ๋ฅผ ๋ฐฉ๋ฌธํ๋ฉด, ๊ทธ ์ฌ์ดํธ๊ฐ ๋์ ๊ถํ์ผ๋ก ๋ค๋ฅธ ์์ ์ ์ํํ ์ ์์ด. ๐
CSRF ๊ณต๊ฒฉ ์์: ์จ๋ผ์ธ ๋ฑ ํน์ ๋ก๊ทธ์ธํ ์ํ์์ ์ ์ฑ ์ด๋ฉ์ผ์ ๋งํฌ๋ฅผ ํด๋ฆญํ๋ฉด, ๊ทธ ๋งํฌ๊ฐ ์๋์ผ๋ก ์ก๊ธ ์์ฒญ์ ๋ณด๋ผ ์ ์์ด!
์ธ์ ํฝ์ธ์ด์ (Session Fixation)
์ด ๊ณต๊ฒฉ์ ๊ณต๊ฒฉ์๊ฐ ์์ ์ด ์๊ณ ์๋ ์ธ์ ID๋ฅผ ์ฌ์ฉ์์๊ฒ ๊ฐ์ ๋ก ์ฌ์ฉํ๊ฒ ํ ๋ค์, ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธํ๋ฉด ๊ทธ ์ธ์ ์ ํ์ทจํ๋ ๋ฐฉ์์ด์ผ.
์ธ์ ๊ด๋ จ ๊ธฐํ ์ทจ์ฝ์
- ๐น ์ธ์ ํ์์์ ๋ถ์ฌ: ์ธ์ ์ด ๋๋ฌด ์ค๋ ์ ์ง๋๋ฉด ์ํํด.
- ๐น ์ฝํ ์ธ์ ID ์์ฑ: ์์ธก ๊ฐ๋ฅํ ์ธ์ ID๋ ์ฝ๊ฒ ์ถ์ธก๋ ์ ์์ด.
- ๐น ์์ ํ์ง ์์ ์ธ์ ์ ์ฅ: ์ธ์ ๋ฐ์ดํฐ๊ฐ ์์ ํ๊ฒ ์ ์ฅ๋์ง ์์ผ๋ฉด ์ ์ถ๋ ์ ์์ด.
- ๐น ์ธ์ ์ฌ์ฌ์ฉ: ๋ก๊ทธ์์ ํ์๋ ๊ฐ์ ์ธ์ ์ด ์ ํจํ๋ฉด ๋ณด์ ์ํ์ด ์ปค.
- ๐น ๋์ ์ธ์ ์ ์ด ๋ถ์ฌ: ์ฌ๋ฌ ๊ธฐ๊ธฐ์์ ๋์ ๋ก๊ทธ์ธ์ ์ ํํ์ง ์์ผ๋ฉด ํ์ทจ ์ํ์ด ๋์.
์ด๋ฐ ๊ณต๊ฒฉ๋ค์ด ๋ฌด์ญ์ง? ๊ทธ๋ผ ์ด์ ์ด๋ป๊ฒ ์์ ํ๊ฒ ์ธ์ ID๋ฅผ ์์ฑํ๋์ง ์์๋ณด์! ๐ก๏ธ
4. ์์ ํ ์ธ์ ID ์์ฑ ๋ฐฉ๋ฒ ๐
์ธ์ ๋ณด์์ ์ฒซ ๋จ๊ณ๋ ๊ฐ๋ ฅํ ์ธ์ ID๋ฅผ ์์ฑํ๋ ๊ฑฐ์ผ. ์ฝํ ์ธ์ ID๋ ๋ง์น ์ข ์ด๋ก ๋ง๋ ์๋ฌผ์ ๊ฐ์ ๊ฑฐ์ง! ๐ช
์์ ํ ์ธ์ ID์ ์กฐ๊ฑด: ๊ธธ์ด๊ฐ ์ถฉ๋ถํ๊ณ , ๋ฌด์์์ ์ด๋ฉฐ, ์์ธก ๋ถ๊ฐ๋ฅํด์ผ ํด. ์ต์ 128๋นํธ(16๋ฐ์ดํธ) ์ด์์ ์ํธ๋กํผ๋ฅผ ๊ฐ์ ธ์ผ ์์ ํ๋ค๊ณ ๋ณผ ์ ์์ด.
์์ ํ ์ธ์ ID ์์ฑ ์์น
- ์ถฉ๋ถํ ๊ธธ์ด: ์ต์ 16๋ฐ์ดํธ(128๋นํธ) ์ด์์ ๊ธธ์ด๋ฅผ ์ฌ์ฉํด.
- ์ํธํ์ ์ผ๋ก ์์ ํ ๋์ ์์ฑ๊ธฐ ์ฌ์ฉ: Math.random() ๊ฐ์ ์ผ๋ฐ ๋์ ํจ์๋ ํผํด์ผ ํด.
- ์์ธก ๋ถ๊ฐ๋ฅ์ฑ: ์๊ฐ, ์ฌ์ฉ์ ID ๋ฑ ์์ธก ๊ฐ๋ฅํ ์ ๋ณด๋ฅผ ํฌํจํ์ง ๋ง.
- ๊ณ ์ ์ฑ: ๊ฐ ์ธ์ ๋ง๋ค ๊ณ ์ ํ ID๋ฅผ ์์ฑํด์ผ ํด.
- ์ ๊ธฐ์ ์ธ ์ฌ์์ฑ: ์ฃผ์ ์ธ์ฆ ์ด๋ฒคํธ(๋ก๊ทธ์ธ, ๊ถํ ๋ณ๊ฒฝ ๋ฑ) ํ์๋ ์๋ก์ด ์ธ์ ID๋ฅผ ๋ฐ๊ธํด.
์ฃผ์ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด๋ณ ์์ ํ ์ธ์ ID ์์ฑ ๋ฐฉ๋ฒ
Node.js์์ ์์ ํ ์ธ์ ID ์์ฑ
const crypto = require('crypto');
function generateSecureSessionId(length = 32) {
return crypto.randomBytes(length).toString('hex');
}
// ์ฌ์ฉ ์
const sessionId = generateSecureSessionId();
console.log(sessionId); // ์: 3a1c5b8f7e2d9a6c4b8f7e2d9a6c4b8f7e2d9a6c4b8f7e2d9a6c4b8f
Python์์ ์์ ํ ์ธ์ ID ์์ฑ
import secrets
import base64
def generate_secure_session_id(length=32):
# ์ํธํ์ ์ผ๋ก ์์ ํ ๋์ ์์ฑ
random_bytes = secrets.token_bytes(length)
# base64๋ก ์ธ์ฝ๋ฉ (URL ์์ ๋ฒ์ )
return base64.urlsafe_b64encode(random_bytes).decode('utf-8')
# ์ฌ์ฉ ์
session_id = generate_secure_session_id()
print(session_id) # ์: X7lsGt_3fP1K8bNmHgQlLnYE5zUoq2Af8vM9pR6w
Java์์ ์์ ํ ์ธ์ ID ์์ฑ
import java.security.SecureRandom;
import java.util.Base64;
public class SessionIdGenerator {
public static String generateSecureSessionId(int length) {
SecureRandom secureRandom = new SecureRandom();
byte[] randomBytes = new byte[length];
secureRandom.nextBytes(randomBytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
}
public static void main(String[] args) {
// 24๋ฐ์ดํธ(192๋นํธ) ๊ธธ์ด์ ์ธ์
ID ์์ฑ
String sessionId = generateSecureSessionId(24);
System.out.println(sessionId);
}
}
PHP์์ ์์ ํ ์ธ์ ID ์์ฑ
function generateSecureSessionId($length = 32) {
return bin2hex(random_bytes($length));
}
// ์ฌ์ฉ ์
$sessionId = generateSecureSessionId();
echo $sessionId; // ์: 7a6f8d2c4b9e1a3f5d7c0b2e4a6f8d2c4b9e1a3f5d7c0b2e4a6f8d2c
์ธ์ ID ์์ฑ ์ ํผํด์ผ ํ ์ค์
โ ๏ธ ์ฃผ์! ๋ค์๊ณผ ๊ฐ์ ๋ฐฉ๋ฒ์ ์ ๋ ์ฌ์ฉํ์ง ๋ง์ธ์:
- โ
Math.random()
๊ฐ์ ๋น์ํธํ์ ๋์ ์์ฑ๊ธฐ ์ฌ์ฉ - โ ์ฌ์ฉ์ ID, ํ์์คํฌํ ๋ฑ ์์ธก ๊ฐ๋ฅํ ์ ๋ณด ํฌํจ
- โ MD5, SHA-1 ๊ฐ์ ์ทจ์ฝํ ํด์ ์๊ณ ๋ฆฌ์ฆ ์ฌ์ฉ
- โ ๋๋ฌด ์งง์ ์ธ์ ID ๊ธธ์ด (16๋ฐ์ดํธ ๋ฏธ๋ง)
- โ ์์ฐจ์ ์ผ๋ก ์ฆ๊ฐํ๋ ์ธ์ ID ์ฌ์ฉ
์์ ํ ์ธ์ ID๋ฅผ ์์ฑํ๋ค๋ฉด, ์ด์ ๊ทธ ์ธ์ ์ ์๋ช ์ ์ด๋ป๊ฒ ๊ด๋ฆฌํ ์ง ์์๋ณผ๊น? โฑ๏ธ
5. ์ธ์ ์๋ช ๊ด๋ฆฌํ๊ธฐ โณ
์ธ์ ์ ์์ํ ์ด์์์ผ๋ฉด ์ ๋ผ. ์ ์ ํ ์์ ์ ๋ง๋ฃ์ํค๊ณ ์ ๋ฆฌํด์ผ ํด. ์ธ์ ์๋ช ๊ด๋ฆฌ๋ ๋ณด์๊ณผ ์ฌ์ฉ์ ๊ฒฝํ ์ฌ์ด์ ๊ท ํ์ ๋ง์ถ๋ ์์ ์ด์ผ.
์ธ์ ์๋ช ์ฃผ๊ธฐ์ ์ฃผ์ ๋จ๊ณ
- ์ธ์ ์์ฑ: ์ฌ์ฉ์ ๋ก๊ทธ์ธ ๋๋ ์ฒซ ๋ฐฉ๋ฌธ ์ ์ธ์ ์์ฑ
- ์ธ์ ํ์ฑํ: ์ฌ์ฉ์๊ฐ ํ๋ํ๋ ๋์ ์ธ์ ์ ์ง
- ์ธ์ ๋นํ์ฑํ: ์ผ์ ์๊ฐ ๋์ ํ๋์ด ์์ ๋ ์ธ์ ๋นํ์ฑ ์ํ๋ก ์ ํ
- ์ธ์ ๋ง๋ฃ: ์ ๋ ๋ง๋ฃ ์๊ฐ ๋๋ฌ ๋๋ ๋นํ์ฑ ํ์์์ ํ ์ธ์ ์ข ๋ฃ
- ์ธ์ ์ญ์ : ๋ก๊ทธ์์ ์ ์ธ์ ์ฆ์ ์ญ์
ํจ๊ณผ์ ์ธ ์ธ์ ํ์์์ ์ค์
์ธ์ ํ์์์์ ๋ ๊ฐ์ง ์ ํ์ด ์์ด:
ํ์์์ ์ ํ | ์ค๋ช | ๊ถ์ฅ ์ค์ |
---|---|---|
์ ๋ ํ์์์ | ์ธ์ ์์ฑ ์์ ๋ถํฐ ๊ณ์ฐ๋ ์ต๋ ์๋ช |
- ์ผ๋ฐ ์น์ฌ์ดํธ: 24์๊ฐ - ๊ธ์ต/์๋ฃ: 15-30๋ถ - ๊ด๋ฆฌ์ ํ์ด์ง: 2-4์๊ฐ |
๋นํ์ฑ ํ์์์ | ๋ง์ง๋ง ํ๋ ์ดํ ๊ฒฝ๊ณผ ์๊ฐ |
- ์ผ๋ฐ ์น์ฌ์ดํธ: 30-60๋ถ - ๊ธ์ต/์๋ฃ: 5-10๋ถ - ๊ด๋ฆฌ์ ํ์ด์ง: 15-30๋ถ |
๐ก ํ: ์ฌ์ฉ์ ๊ฒฝํ๊ณผ ๋ณด์ ์ฌ์ด์ ๊ท ํ์ ๋ง์ถ๋ ค๋ฉด, ์ธ์ ์ด ๊ณง ๋ง๋ฃ๋ ๋ ์ฌ์ฉ์์๊ฒ ์๋ฆผ์ ์ฃผ๊ณ ์ฐ์ฅ ์ต์ ์ ์ ๊ณตํ๋ ๊ฒ์ด ์ข์!
์ฃผ์ ํ๋ ์์ํฌ์์์ ์ธ์ ์๋ช ๊ด๋ฆฌ
Express.js (Node.js)
const session = require('express-session');
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS์์๋ง ์ฟ ํค ์ ์ก
httpOnly: true, // JavaScript์์ ์ฟ ํค ์ ๊ทผ ๋ฐฉ์ง
maxAge: 24 * 60 * 60 * 1000, // ์ ๋ ํ์์์: 24์๊ฐ
sameSite: 'strict' // CSRF ๋ฐฉ์ง
},
rolling: true // ์์ฒญ๋ง๋ค ๋ง๋ฃ ์๊ฐ ๊ฐฑ์ (๋นํ์ฑ ํ์์์ ๊ตฌํ)
}));
Django (Python)
# settings.py
SESSION_COOKIE_SECURE = True # HTTPS์์๋ง ์ฟ ํค ์ ์ก
SESSION_COOKIE_HTTPONLY = True # JavaScript์์ ์ฟ ํค ์ ๊ทผ ๋ฐฉ์ง
SESSION_COOKIE_SAMESITE = 'Strict' # CSRF ๋ฐฉ์ง
SESSION_COOKIE_AGE = 86400 # ์ ๋ ํ์์์: 24์๊ฐ(์ด ๋จ์)
SESSION_EXPIRE_AT_BROWSER_CLOSE = False # ๋ธ๋ผ์ฐ์ ๋ซ์ ๋ ์ธ์
์ ์ง
SESSION_SAVE_EVERY_REQUEST = True # ์์ฒญ๋ง๋ค ๋ง๋ฃ ์๊ฐ ๊ฐฑ์ (๋นํ์ฑ ํ์์์)
Spring Boot (Java)
// application.properties
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.same-site=strict
server.servlet.session.timeout=24h // ์ ๋ ํ์์์: 24์๊ฐ
// SessionConfig.java
@Configuration
public class SessionConfig {
@Bean
public HttpSessionListener httpSessionListener() {
return new HttpSessionListener() {
@Override
public void sessionCreated(HttpSessionEvent se) {
System.out.println("์ธ์
์์ฑ: " + se.getSession().getId());
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
System.out.println("์ธ์
๋ง๋ฃ: " + se.getSession().getId());
}
};
}
}
์ธ์ ์ข ๋ฃ ์ฒ๋ฆฌ
์ธ์ ์ ์ข ๋ฃํ ๋๋ ๋ค์ ์์ ์ ์ํํด์ผ ํด:
- ์๋ฒ ์ธก ์ธ์ ๋ฐ์ดํฐ ์ญ์ : ๋ฉ๋ชจ๋ฆฌ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ธ์ ์ ๋ณด ์ ๊ฑฐ
- ํด๋ผ์ด์ธํธ ์ธก ์ธ์ ์ฟ ํค ๋ฌดํจํ: ๋ง๋ฃ ์๊ฐ์ ๊ณผ๊ฑฐ๋ก ์ค์
- ์ธ์ ์ข ๋ฃ ๋ก๊น : ๊ฐ์ฌ ๋ชฉ์ ์ผ๋ก ์ธ์ ์ข ๋ฃ ๊ธฐ๋ก
- ๊ด๋ จ ๋ฆฌ์์ค ์ ๋ฆฌ: ์ธ์ ๊ณผ ์ฐ๊ฒฐ๋ ์์ ํ์ผ์ด๋ ๋ฆฌ์์ค ์ ๋ฆฌ
โ ๏ธ ์ฃผ์! ๋ก๊ทธ์์ ๊ธฐ๋ฅ์ ๋ฐ๋์ POST ์์ฒญ์ผ๋ก ๊ตฌํํด์ผ CSRF ๊ณต๊ฒฉ์ ๋ฐฉ์งํ ์ ์์ด.
์ธ์ ์๋ช ์ ์ ๊ด๋ฆฌํ๋ ๊ฒ๋ ์ค์ํ์ง๋ง, ์ธ์ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๋ ์ฟ ํค์ ๋ณด์ ์ค์ ๋ ๋งค์ฐ ์ค์ํด. ๋ค์ ์น์ ์์ ์์๋ณด์! ๐ช
6. ์ฟ ํค ๋ณด์ ์ค์ ์ ๋ชจ๋ ๊ฒ ๐ช
์ธ์ ID๋ ์ฃผ๋ก ์ฟ ํค๋ฅผ ํตํด ํด๋ผ์ด์ธํธ์ ์๋ฒ ์ฌ์ด์์ ์ ๋ฌ๋ผ. ๊ทธ๋์ ์ฟ ํค ๋ณด์ ์ค์ ์ ์ธ์ ๋ณด์์ ํต์ฌ์ด์ผ! ๐
ํ์ ์ฟ ํค ๋ณด์ ์์ฑ
์์ฑ | ์ค๋ช | ๊ถ์ฅ ์ค์ |
---|---|---|
HttpOnly | JavaScript๋ฅผ ํตํ ์ฟ ํค ์ ๊ทผ์ ๋ฐฉ์ง | ํญ์ true๋ก ์ค์ (XSS ๊ณต๊ฒฉ ๋ฐฉ์ด) |
Secure | HTTPS ์ฐ๊ฒฐ์์๋ง ์ฟ ํค ์ ์ก | ํญ์ true๋ก ์ค์ (MITM ๊ณต๊ฒฉ ๋ฐฉ์ด) |
SameSite | ํฌ๋ก์ค ์ฌ์ดํธ ์์ฒญ์ ์ฟ ํค ์ ์ก ์ ํ |
- Strict: ๊ฐ์ฅ ์์ (๋์ผ ์ฌ์ดํธ๋ง) - Lax: ๊ท ํ์ (์ผ๋ถ ํฌ๋ก์ค ์ฌ์ดํธ ํ์ฉ) - None: ๋ชจ๋ ํฌ๋ก์ค ์ฌ์ดํธ ํ์ฉ (Secure ํ์) |
Domain | ์ฟ ํค๊ฐ ์ ์ก๋ ๋๋ฉ์ธ ์ง์ | ๊ฐ๋ฅํ ํ ์ ํ์ ์ผ๋ก ์ค์ (์๋ธ๋๋ฉ์ธ ํฌํจ ์ฌ๋ถ ์ฃผ์) |
Path | ์ฟ ํค๊ฐ ์ ์ก๋ ๊ฒฝ๋ก ์ง์ | ํ์ํ ๊ฒฝ๋ก๋ก ์ ํ (๊ธฐ๋ณธ๊ฐ '/'๋ ๋ชจ๋ ๊ฒฝ๋ก) |
Expires/Max-Age | ์ฟ ํค ๋ง๋ฃ ์๊ฐ ์ค์ | ์ธ์ ์๋ช ์ ๋ง๊ฒ ์ ์ ํ ์ค์ |
SameSite ์์ฑ ์ฌ์ธต ์ดํด
2020๋ ์ดํ Chrome์ ๋น๋กฏํ ์ฃผ์ ๋ธ๋ผ์ฐ์ ์์ SameSite ๊ธฐ๋ณธ๊ฐ์ด 'Lax'๋ก ๋ณ๊ฒฝ๋์์ด. ์ด ์์ฑ์ CSRF ๊ณต๊ฒฉ ๋ฐฉ์ด์ ๋งค์ฐ ์ค์ํด!
SameSite ๊ฐ | ๋์ ๋ฐฉ์ | ์ฌ์ฉ ์ฌ๋ก |
---|---|---|
Strict | ๋์ผ ์ฌ์ดํธ ์์ฒญ์๋ง ์ฟ ํค ์ ์ก | ๋์ ๋ณด์์ด ํ์ํ ์ธ์ (๊ด๋ฆฌ์ ํ์ด์ง, ๊ธ์ต ์๋น์ค) |
Lax | GET ์์ฒญ๊ณผ ๊ฐ์ ์์ ํ ์์ฒญ์๋ ํฌ๋ก์ค ์ฌ์ดํธ๋ ์ฟ ํค ์ ์ก | ๋๋ถ๋ถ์ ์น ์ ํ๋ฆฌ์ผ์ด์ (๊ท ํ์ ์ธ ๋ณด์) |
None | ๋ชจ๋ ํฌ๋ก์ค ์ฌ์ดํธ ์์ฒญ์ ์ฟ ํค ์ ์ก (Secure ํ์) | ์๋ํํฐ ํตํฉ์ด ํ์ํ ๊ฒฝ์ฐ (์์ ๋ก๊ทธ์ธ ๋ฑ) |
์ฃผ์ ํ๋ ์์ํฌ์์์ ์ฟ ํค ๋ณด์ ์ค์
Express.js (Node.js)
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
sameSite: 'strict',
domain: 'yourapp.com',
path: '/',
maxAge: 24 * 60 * 60 * 1000 // 24์๊ฐ
}
}));
Spring Boot (Java)
// application.properties
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.same-site=strict
server.servlet.session.cookie.domain=yourapp.com
server.servlet.session.cookie.path=/
server.servlet.session.cookie.max-age=86400 // 24์๊ฐ(์ด ๋จ์)
Django (Python)
# settings.py
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = 'Strict'
SESSION_COOKIE_DOMAIN = 'yourapp.com'
SESSION_COOKIE_PATH = '/'
SESSION_COOKIE_AGE = 86400 # 24์๊ฐ(์ด ๋จ์)
PHP
// php.ini ๋๋ .htaccess
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = "Strict"
session.cookie_domain = "yourapp.com"
session.cookie_path = "/"
session.cookie_lifetime = 86400 // 24์๊ฐ(์ด ๋จ์)
// ๋๋ PHP ์ฝ๋์์ ์ง์ ์ค์
session_set_cookie_params([
'lifetime' => 86400,
'path' => '/',
'domain' => 'yourapp.com',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
๐ก ์ฐธ๊ณ : 2025๋ ํ์ฌ ๋๋ถ๋ถ์ ์ต์ ๋ธ๋ผ์ฐ์ ๋ SameSite=None ์ฟ ํค์ ๋ํด ๋ฐ๋์ Secure ์์ฑ๋ ํจ๊ป ์ค์ ํ ๊ฒ์ ์๊ตฌํด. ๊ทธ๋ ์ง ์์ผ๋ฉด ์ฟ ํค๊ฐ ๊ฑฐ๋ถ๋ ์ ์์ด!
์ฟ ํค ๋ณด์์ ์ํ ์ถ๊ฐ ํ
- ํ์ํ ์ ๋ณด๋ง ์ ์ฅ: ์ธ์ ID ์ธ์ ๋ฏผ๊ฐํ ์ ๋ณด๋ ์ฟ ํค์ ์ ์ฅํ์ง ๋ง.
- ์ฟ ํค ํฌ๊ธฐ ์ต์ํ: ์ฟ ํค๋ ๋ชจ๋ ์์ฒญ์ ํฌํจ๋๋ฏ๋ก ํฌ๊ธฐ๋ฅผ ์ต์ํํด์ผ ์ฑ๋ฅ์ด ์ข์.
- __Host- ์ ๋์ฌ ์ฌ์ฉ: ๋ ๊ฐ๋ ฅํ ๋ณด์์ ์ํด ์ฟ ํค ์ด๋ฆ์ __Host- ์ ๋์ฌ๋ฅผ ์ฌ์ฉํ ์ ์์ด (Domain ์์ฑ ์์ด Secure, Path=/ ํ์).
- ์ ๊ธฐ์ ์ธ ์ฟ ํค ๊ต์ฒด: ์ฃผ์ ์ธ์ฆ ์ด๋ฒคํธ(๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ, ๊ถํ ๋ณ๊ฒฝ ๋ฑ) ํ์๋ ์ ์ฟ ํค ๋ฐ๊ธ.
- ์ฟ ํค ์ํธํ ๊ณ ๋ ค: ํ์ํ ๊ฒฝ์ฐ ์ฟ ํค ๊ฐ์ ์ํธํํ์ฌ ์ถ๊ฐ ๋ณดํธ์ธต ์ ๊ณต.
์ฟ ํค ๋ณด์ ์ค์ ์ ์ ๋๋ก ํ๋ค๋ฉด, ์ด์ HTTPS๋ฅผ ํตํ ์ธ์ ๋ณด์์ ๊ฐํํด๋ณด์! ๐
7. HTTPS์ ์ธ์ ๋ณด์ ๐
HTTPS๋ ์ธ์ ๋ณด์์ ๊ธฐ๋ณธ ์ค์ ๊ธฐ๋ณธ์ด์ผ. HTTP ๋์ HTTPS๋ฅผ ์ฌ์ฉํ๋ฉด ์ธ์ ID๋ฅผ ํฌํจํ ๋ชจ๋ ํต์ ์ด ์ํธํ๋์ด ๋์ฒญ์ด๋ ์ค๊ฐ์ ๊ณต๊ฒฉ์ ๋ฐฉ์งํ ์ ์์ด! ๐ก๏ธ
HTTPS๊ฐ ์ธ์ ๋ณด์์ ์ค์ํ ์ด์
- ๋ฐ์ดํฐ ์ํธํ: ์ธ์ ID๋ฅผ ํฌํจํ ๋ชจ๋ ํต์ ๋ด์ฉ์ ์ํธํํ์ฌ ๋์ฒญ ๋ฐฉ์ง
- ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ: ์ ์ก ์ค ๋ฐ์ดํฐ ๋ณ์กฐ ๋ฐฉ์ง
- ์๋ฒ ์ธ์ฆ: ํด๋ผ์ด์ธํธ๊ฐ ์ ์ํ ์๋ฒ๊ฐ ์ง์ง ์๋ฒ์์ ์ธ์ฆ
- Secure ์ฟ ํค ํ์ฑํ: HTTPS์์๋ง ์๋ํ๋ Secure ์ฟ ํค ์์ฑ ์ฌ์ฉ ๊ฐ๋ฅ
- ์ต์ ๋ณด์ ๊ธฐ๋ฅ ์ง์: HTTP/2, HSTS ๋ฑ ์ต์ ๋ณด์ ๊ธฐ๋ฅ ํ์ฉ ๊ฐ๋ฅ
HTTPS ๊ตฌํ ๋ชจ๋ฒ ์ฌ๋ก
๋ชจ๋ฒ ์ฌ๋ก | ์ค๋ช |
---|---|
์ ์ฒด ์ฌ์ดํธ HTTPS ์ ์ฉ | ๋ก๊ทธ์ธ ํ์ด์ง๋ฟ๋ง ์๋๋ผ ์ ์ฒด ์ฌ์ดํธ์ HTTPS ์ ์ฉ (ํผํฉ ์ฝํ ์ธ ๋ฌธ์ ๋ฐฉ์ง) |
HSTS(HTTP Strict Transport Security) ํ์ฑํ | ๋ธ๋ผ์ฐ์ ๊ฐ ํญ์ HTTPS๋ก๋ง ์ฌ์ดํธ์ ์ ์ํ๋๋ก ๊ฐ์ |
์ต์ TLS ๋ฒ์ ์ฌ์ฉ | 2025๋ ๊ธฐ์ค TLS 1.3 ์ฌ์ฉ ๊ถ์ฅ (TLS 1.0/1.1์ ๋ ์ด์ ์์ ํ์ง ์์) |
๊ฐ๋ ฅํ ์ํธํ ์ค์ํธ ์ค์ | ์์ ํ ์ํธํ ์๊ณ ๋ฆฌ์ฆ๋ง ํ์ฉํ๋๋ก ์๋ฒ ๊ตฌ์ฑ |
์ธ์ฆ์ ์๋ ๊ฐฑ์ | Let's Encrypt์ ๊ฐ์ ์๋น์ค๋ฅผ ํ์ฉํ ์ธ์ฆ์ ์๋ ๊ฐฑ์ ์ค์ |
HTTP์์ HTTPS๋ก ๋ฆฌ๋ค์ด๋ ํธ | HTTP ์์ฒญ์ ์๋์ผ๋ก HTTPS๋ก ๋ฆฌ๋ค์ด๋ ํธํ์ฌ ํญ์ ์ํธํ๋ ์ฐ๊ฒฐ ์ฌ์ฉ |
HSTS ์ค์ ๋ฐฉ๋ฒ
HSTS(HTTP Strict Transport Security)๋ ์น์ฌ์ดํธ๊ฐ HTTPS๋ก๋ง ์ ์๋๋๋ก ๊ฐ์ ํ๋ ๋ณด์ ๊ธฐ๋ฅ์ด์ผ. ์ด๋ฅผ ํตํด SSL Stripping ๊ฐ์ ๋ค์ด๊ทธ๋ ์ด๋ ๊ณต๊ฒฉ์ ๋ฐฉ์งํ ์ ์์ด.
Nginx์์ HSTS ์ค์
server {
listen 443 ssl;
server_name yourapp.com;
# SSL ์ธ์ฆ์ ์ค์
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# HSTS ์ค์ (max-age๋ ์ด ๋จ์, 1๋
= 31536000์ด)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# ๊ธฐํ ์ค์ ...
}
Apache์์ HSTS ์ค์
<virtualhost>
ServerName yourapp.com
# SSL ์ธ์ฆ์ ์ค์
SSLEngine on
SSLCertificateFile /path/to/cert.pem
SSLCertificateKeyFile /path/to/key.pem
# HSTS ์ค์
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# ๊ธฐํ ์ค์ ...
</virtualhost>
Express.js์์ HSTS ์ค์
const express = require('express');
const helmet = require('helmet');
const app = express();
// Helmet์ ์ฌ์ฉํ HSTS ์ค์
app.use(helmet.hsts({
maxAge: 31536000, // 1๋
includeSubDomains: true,
preload: true
}));
๐ก ํ: HSTS Preload List(https://hstspreload.org/)์ ๋๋ฉ์ธ์ ๋ฑ๋กํ๋ฉด ์ฌ์ฉ์๊ฐ ์ฒ์ ๋ฐฉ๋ฌธํ ๋๋ถํฐ HTTPS๊ฐ ๊ฐ์ ๋์ด ๋ ์์ ํด!
์ธ์ฆ์ ๊ด๋ฆฌ ์๋ํ
SSL/TLS ์ธ์ฆ์๋ ์ ๊ธฐ์ ์ผ๋ก ๊ฐฑ์ ํด์ผ ํด. Let's Encrypt์ ๊ฐ์ ๋ฌด๋ฃ ์ธ์ฆ ๊ธฐ๊ด๊ณผ ์๋ํ ๋๊ตฌ๋ฅผ ํ์ฉํ๋ฉด ์ธ์ฆ์ ๊ด๋ฆฌ๋ฅผ ์ฝ๊ฒ ํ ์ ์์ด.
Certbot์ ์ด์ฉํ Let's Encrypt ์ธ์ฆ์ ์๋ ๊ฐฑ์
# Certbot ์ค์น (Ubuntu/Debian)
sudo apt-get update
sudo apt-get install certbot
# Nginx์ฉ ์ธ์ฆ์ ๋ฐ๊ธ
sudo certbot --nginx -d yourapp.com -d www.yourapp.com
# ์๋ ๊ฐฑ์ ํฌ๋ก ์์
์ค์
echo "0 3 * * * /usr/bin/certbot renew --quiet" | sudo tee -a /etc/crontab
HTTPS๋ ๊ธฐ๋ณธ์ด์ง๋ง, ๋ ๊ฐ๋ ฅํ ๋ณด์์ ์ํด ๋ค์ค ์์ ์ธ์ฆ(MFA)์ ์ถ๊ฐํ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ. ๋ค์ ์น์ ์์ ์์๋ณด์! ๐
8. ๋ค์ค ์์ ์ธ์ฆ(MFA)๊ณผ ์ธ์ ๐
๋ค์ค ์์ ์ธ์ฆ(MFA)์ ๋น๋ฐ๋ฒํธ ์ธ์ ์ถ๊ฐ์ ์ธ ์ธ์ฆ ์์๋ฅผ ์๊ตฌํ์ฌ ๋ณด์์ ๊ฐํํ๋ ๋ฐฉ๋ฒ์ด์ผ. ํนํ ์ค์ํ ์์ ์ด๋ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ์ ์ ๊ทผํ ๋ ์ธ์ ๋ณด์์ ํ์ธต ๋ ๊ฐํํ ์ ์์ด! ๐ก๏ธ
MFA ์ ํ๊ณผ ์ธ์ ๊ด๋ฆฌ
MFA ์ ํ | ์ค๋ช | ์ธ์ ๊ด๋ฆฌ ๋ฐฉ์ |
---|---|---|
SMS/์ด๋ฉ์ผ OTP | SMS๋ ์ด๋ฉ์ผ๋ก ์ผํ์ฉ ์ฝ๋ ์ ์ก |
- ์ธ์
์ MFA ์๋ฃ ์ํ ์ ์ฅ - ์ฝ๋ ์ ํจ ์๊ฐ ์ ํ (๋ณดํต 5-10๋ถ) |
TOTP ์ฑ | Google Authenticator ๊ฐ์ ์ฑ์์ ์์ฑ๋๋ ์๊ฐ ๊ธฐ๋ฐ ์ฝ๋ |
- ์ธ์
์ MFA ์๋ฃ ์ํ ์ ์ฅ - ์ฝ๋๋ ๋ณดํต 30์ด๋ง๋ค ๋ณ๊ฒฝ |
ํธ์ ์๋ฆผ | ๋ชจ๋ฐ์ผ ์ฑ์ผ๋ก ์น์ธ ์์ฒญ ํธ์ ์๋ฆผ ์ ์ก |
- ์ธ์
ID์ ํธ์ ์์ฒญ ์ฐ๊ฒฐ - ์๋ต ๋๊ธฐ ์ํ ๊ด๋ฆฌ ํ์ |
์์ฒด ์ธ์ | ์ง๋ฌธ, ์ผ๊ตด ์ธ์ ๋ฑ ์์ฒด ์ ๋ณด ํ์ฉ |
- WebAuthn/FIDO2 ํ์ค ํ์ฉ - ์ธ์ ์ ์ธ์ฆ ์๋ฃ ์ํ ์ ์ฅ |
ํ๋์จ์ด ํค | YubiKey ๊ฐ์ ๋ฌผ๋ฆฌ์ ๋ณด์ ํค ์ฌ์ฉ |
- WebAuthn/FIDO2 ํ์ค ํ์ฉ - ์ธ์ ์ ํค ์ธ์ฆ ์๋ฃ ์ํ ์ ์ฅ |
MFA์ ์ธ์ ํตํฉ ๊ตฌํ
MFA๋ฅผ ์ธ์ ๊ด๋ฆฌ์ ํตํฉํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์:
- MFA ์ธ์ฆ ์ํ ์ ์ฅ: ์ธ์ ์ MFA ์๋ฃ ์ฌ๋ถ๋ฅผ ์ ์ฅํ์ฌ ๋ณดํธ๋ ๋ฆฌ์์ค ์ ๊ทผ ์ ํ์ธ
- ๋จ๊ณ๋ณ ์ธ์ฆ ๊ตฌํ: ์ผ๋ฐ ํ์ด์ง๋ ๊ธฐ๋ณธ ์ธ์ฆ๋ง์ผ๋ก, ์ค์ ์์ ์ ์ถ๊ฐ MFA ์๊ตฌ
- MFA ์ฌ์ธ์ฆ ์ ์ฑ : ์ค์ ์์ , ์ผ์ ์๊ฐ ๊ฒฝ๊ณผ, IP ๋ณ๊ฒฝ ๋ฑ์ ์กฐ๊ฑด์์ MFA ์ฌ์ธ์ฆ ์๊ตฌ
- ์ ๋ขฐํ ์ ์๋ ๊ธฐ๊ธฐ ๊ด๋ฆฌ: ์ฌ์ฉ์๊ฐ ์ ๋ขฐํ๋ ๊ธฐ๊ธฐ์์๋ MFA ๋น๋ ๊ฐ์ ์ต์ ์ ๊ณต
Node.js์์ MFA ์ธ์ ๊ด๋ฆฌ ์์
// MFA ์ํ๋ฅผ ์ธ์
์ ์ ์ฅํ๋ ์์
app.post('/verify-mfa', (req, res) => {
const { code } = req.body;
const user = getUserFromSession(req);
// TOTP ์ฝ๋ ๊ฒ์ฆ
if (verifyTOTP(user.mfaSecret, code)) {
// ์ธ์
์ MFA ์๋ฃ ์ํ ์ ์ฅ
req.session.mfaVerified = true;
req.session.mfaVerifiedAt = Date.now();
res.redirect('/dashboard');
} else {
res.render('mfa', { error: '์๋ชป๋ ์ฝ๋์
๋๋ค. ๋ค์ ์๋ํด์ฃผ์ธ์.' });
}
});
// ๋ณดํธ๋ ๋ฆฌ์์ค์ ์ ๊ทผํ ๋ MFA ํ์ธ ๋ฏธ๋ค์จ์ด
function requireMFA(req, res, next) {
if (!req.session.mfaVerified) {
// MFA๊ฐ ์๋ฃ๋์ง ์์ ๊ฒฝ์ฐ MFA ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ
return res.redirect('/mfa');
}
// MFA ์๋ฃ ํ ์ผ์ ์๊ฐ์ด ์ง๋ฌ๋์ง ํ์ธ (์: 4์๊ฐ)
const mfaExpiry = 4 * 60 * 60 * 1000; // 4์๊ฐ(๋ฐ๋ฆฌ์ด)
if (Date.now() - req.session.mfaVerifiedAt > mfaExpiry) {
// MFA ๋ง๋ฃ๋ ๊ฒฝ์ฐ ์ฌ์ธ์ฆ ์๊ตฌ
req.session.mfaVerified = false;
return res.redirect('/mfa');
}
// MFA ์ ํจํ ๊ฒฝ์ฐ ๋ค์ ๋ฏธ๋ค์จ์ด๋ก ์งํ
next();
}
MFA ์ฐํ ๋ฐฉ์ง๋ฅผ ์ํ ์ธ์ ๋ณด์ ๊ฐํ
โ ๏ธ ์ฃผ์! MFA๋ฅผ ๊ตฌํํด๋ ์ธ์ ๊ด๋ฆฌ์ ์ทจ์ฝ์ ์ด ์์ผ๋ฉด ์ฐํ๋ ์ ์์ด. ๋ค์ ์ฌํญ์ ๋ฐ๋์ ํ์ธํ์ธ์:
- ์ธ์ฆ ๋จ๊ณ ๋ถ๋ฆฌ: MFA ์ธ์ฆ ์ /ํ ์ธ์ ์ ๋ช ํํ ๊ตฌ๋ถํ๊ณ ๊ถํ ์ฒดํฌ ์ฒ ์ ํ ์ํ
- MFA ์ํ ๋ณดํธ: ์ธ์ ์ ์ ์ฅ๋ MFA ์๋ฃ ์ํ๊ฐ ์กฐ์๋์ง ์๋๋ก ๋ณดํธ
- ์ธ์ ๊ณ ์ ๊ณต๊ฒฉ ๋ฐฉ์ง: MFA ์๋ฃ ํ ์ธ์ ID ์ฌ์์ฑ ๊ณ ๋ ค
- MFA ์ฐํ ์๋ ๋ชจ๋ํฐ๋ง: ๋น์ ์์ ์ธ ์ธ์ฆ ์๋ ๊ฐ์ง ๋ฐ ์ฐจ๋จ
- ๋ฐฑ์๋ ๊ฒ์ฆ ์ฒ ์ : ํด๋ผ์ด์ธํธ ์ธก ๊ฒ์ฆ์๋ง ์์กดํ์ง ๋ง๊ณ ํญ์ ์๋ฒ์์ ์ฌ๊ฒ์ฆ
๐ก ํ: 2025๋ ํ์ฌ WebAuthn/FIDO2๋ ๊ฐ์ฅ ์์ ํ MFA ํ์ค์ผ๋ก ์ธ์ ๋ฐ๊ณ ์์ด. ๊ฐ๋ฅํ๋ค๋ฉด ์ด ํ์ค์ ์ง์ํ๋ ๊ฒ์ด ์ข์!
MFA๋ฅผ ํตํด ์ธ์ ๋ณด์์ ๊ฐํํ๋ค๋ฉด, ์ด์ ํ๋์ ์ธ ์ธ์ ๊ด๋ฆฌ ๋ฐฉ์์ธ JWT์ ๋ํด ์์๋ณด์! ๐
9. JWT๋ฅผ ํ์ฉํ ํ๋์ ์ธ์ ๊ด๋ฆฌ ๐
JWT(JSON Web Token)๋ ์ ํต์ ์ธ ์๋ฒ ๊ธฐ๋ฐ ์ธ์ ๊ณผ๋ ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ์ธ์ฆ๊ณผ ๊ถํ ๋ถ์ฌ๋ฅผ ์ฒ๋ฆฌํด. ํนํ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ๋ SPA(Single Page Application)์์ ๋ง์ด ์ฌ์ฉ๋๋ ๋ฐฉ์์ด์ง! ๐
JWT์ ๊ธฐ๋ณธ ๊ตฌ์กฐ
JWT๋ ์ธ ๋ถ๋ถ์ผ๋ก ๊ตฌ์ฑ๋ ๋ฌธ์์ด์ด์ผ:
- ํค๋(Header): ํ ํฐ ์ ํ๊ณผ ์ฌ์ฉ๋ ์ํธํ ์๊ณ ๋ฆฌ์ฆ ์ ๋ณด
- ํ์ด๋ก๋(Payload): ์ฌ์ฉ์ ID, ๊ถํ, ๋ง๋ฃ ์๊ฐ ๋ฑ ํด๋ ์(claim) ์ ๋ณด
- ์๋ช (Signature): ํ ํฐ์ ๋ฌด๊ฒฐ์ฑ์ ๊ฒ์ฆํ๊ธฐ ์ํ ์๋ช
JWT ์์ ๋ถ์
// JWT ํ ํฐ ์์
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
// ๋์ฝ๋ฉ๋ ํค๋
{
"alg": "HS256", // ์๋ช
์๊ณ ๋ฆฌ์ฆ
"typ": "JWT" // ํ ํฐ ํ์
}
// ๋์ฝ๋ฉ๋ ํ์ด๋ก๋
{
"sub": "0", // ์ฃผ์ (์ฌ์ฉ์ ID)
"name": "John Doe", // ์ฌ์ฉ์ ์ด๋ฆ
"iat": 1516239022 // ๋ฐ๊ธ ์๊ฐ(Issued At)
}
JWT vs ์ ํต์ ์ธ์ ๊ด๋ฆฌ
ํน์ฑ | ์ ํต์ ์ธ์ | JWT |
---|---|---|
์ํ ๊ด๋ฆฌ | Stateful (์๋ฒ์ ์ํ ์ ์ฅ) | Stateless (ํ ํฐ์ ๋ชจ๋ ์ ๋ณด ํฌํจ) |
์ ์ฅ ์์น | ์๋ฒ ๋ฉ๋ชจ๋ฆฌ, ๋ฐ์ดํฐ๋ฒ ์ด์ค, ์บ์ | ํด๋ผ์ด์ธํธ (ํ ํฐ ์์ฒด์ ์ ๋ณด ํฌํจ) |
ํ์ฅ์ฑ | ์ธ์ ๊ณต์ ๋ฉ์ปค๋์ฆ ํ์ | ์ฝ๊ฒ ํ์ฅ ๊ฐ๋ฅ (์๋ฒ ๊ฐ ์ํ ๊ณต์ ๋ถํ์) |
ํ ํฐ ํฌ๊ธฐ | ์์ (์ธ์ ID๋ง ์ ์ก) | ์๋์ ์ผ๋ก ํผ (๋ชจ๋ ์ ๋ณด ํฌํจ) |
๋ง๋ฃ ์ฒ๋ฆฌ | ์๋ฒ์์ ์ธ์ ์ญ์ ๋ก ์ฆ์ ๋ฌดํจํ ๊ฐ๋ฅ | ๋ง๋ฃ ์๊ฐ๊น์ง ์ ํจ (๋ธ๋๋ฆฌ์คํธ ํ์) |
์ ํฉํ ํ๊ฒฝ | ๋ชจ๋๋ฆฌ์ ์ ํ๋ฆฌ์ผ์ด์ , ์ฆ์ ์ธ์ ๋ฌดํจํ ํ์ ์ | ๋ง์ดํฌ๋ก์๋น์ค, SPA, ๋ชจ๋ฐ์ผ ์ฑ |
JWT ๊ธฐ๋ฐ ์ธ์ ๊ด๋ฆฌ ๊ตฌํ
Node.js์์ JWT ๊ตฌํ ์์
const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();
// ๋น๋ฐ ํค (์ค์ ๋ก๋ ํ๊ฒฝ ๋ณ์ ๋ฑ์ผ๋ก ์์ ํ๊ฒ ๊ด๋ฆฌ)
const JWT_SECRET = 'your-secret-key';
// ๋ก๊ทธ์ธ ๋ฐ JWT ๋ฐ๊ธ
app.post('/login', (req, res) => {
const { username, password } = req.body;
// ์ฌ์ฉ์ ์ธ์ฆ (์ค์ ๋ก๋ DB ์กฐํ ๋ฑ์ผ๋ก ๊ตฌํ)
if (authenticateUser(username, password)) {
// ์ฌ์ฉ์ ์ ๋ณด
const user = {
id: 123,
username: username,
role: 'user'
};
// JWT ํ ํฐ ์์ฑ
const token = jwt.sign(
{ userId: user.id, role: user.role }, // ํ์ด๋ก๋
JWT_SECRET, // ๋น๋ฐ ํค
{
expiresIn: '1h', // ๋ง๋ฃ ์๊ฐ
issuer: 'yourapp.com' // ๋ฐ๊ธ์
}
);
// ์๋ต์ผ๋ก ํ ํฐ ๋ฐํ
res.json({ token });
} else {
res.status(401).json({ error: '์ธ์ฆ ์คํจ' });
}
});
// JWT ๊ฒ์ฆ ๋ฏธ๋ค์จ์ด
function authenticateJWT(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: '์ธ์ฆ ํ ํฐ์ด ์์ต๋๋ค' });
}
// Bearer ํ ํฐ์์ JWT ์ถ์ถ
const token = authHeader.split(' ')[1];
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: '์ ํจํ์ง ์์ ํ ํฐ์
๋๋ค' });
}
// ์์ฒญ ๊ฐ์ฒด์ ์ฌ์ฉ์ ์ ๋ณด ์ถ๊ฐ
req.user = user;
next();
});
}
// ๋ณดํธ๋ ๋ผ์ฐํธ
app.get('/protected', authenticateJWT, (req, res) => {
res.json({ message: '๋ณดํธ๋ ๋ฐ์ดํฐ์ ์ ๊ทผํ์ต๋๋ค', user: req.user });
});
JWT ๋ณด์ ๋ชจ๋ฒ ์ฌ๋ก
JWT๋ ํธ๋ฆฌํ์ง๋ง, ์๋ชป ์ฌ์ฉํ๋ฉด ๋ณด์ ์ํ์ด ์์ด. ๋ค์ ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ๋ฐ๋ผ์ผ ํด:
- ์ ์ ํ ๋ง๋ฃ ์๊ฐ ์ค์ : ๋๋ฌด ๊ธธ๋ฉด ํ์ทจ ์ํ์ด ์ปค์ง๊ณ , ๋๋ฌด ์งง์ผ๋ฉด ์ฌ์ฉ์ ๊ฒฝํ์ด ๋๋น ์ ธ.
- ๋ฆฌํ๋ ์ ํ ํฐ ์ฌ์ฉ: ์ก์ธ์ค ํ ํฐ์ ์งง๊ฒ, ๋ฆฌํ๋ ์ ํ ํฐ์ ๊ธธ๊ฒ ์ค์ ํ์ฌ ๋ณด์๊ณผ ์ฌ์ฉ์ฑ ๊ท ํ ์ ์ง.
- ์์ ํ ์๊ณ ๋ฆฌ์ฆ ์ ํ: HS256(HMAC + SHA-256) ์ด์ ๋๋ RS256(RSA + SHA-256) ์ฌ์ฉ.
- ๋ฏผ๊ฐํ ์ ๋ณด ์ ์ธ: ํ์ด๋ก๋์ ๋น๋ฐ๋ฒํธ ๋ฑ ๋ฏผ๊ฐ ์ ๋ณด๋ฅผ ์ ๋ ํฌํจํ์ง ๋ง.
- ํ ํฐ ์ ์ฅ ์์น: HttpOnly, Secure ์ฟ ํค์ ์ ์ฅํ๋ ๊ฒ์ด localStorage๋ณด๋ค ์์ .
- ํ ํฐ ๋ฌดํจํ ๋ฉ์ปค๋์ฆ: ํ์์ ํ ํฐ์ ๋ฌดํจํํ ์ ์๋ ๋ธ๋๋ฆฌ์คํธ ๊ตฌํ.
โ ๏ธ ์ฃผ์! JWT๋ ์๋ช ๋์์ ๋ฟ ์ํธํ๋์ง ์์๋ค๋ ์ ์ ๊ธฐ์ตํด! ํ์ด๋ก๋๋ ๋๊ตฌ๋ ๋์ฝ๋ฉํ ์ ์์ผ๋ฏ๋ก ๋ฏผ๊ฐํ ์ ๋ณด๋ฅผ ํฌํจํ์ง ๋ง์ธ์.
๋ฆฌํ๋ ์ ํ ํฐ ํจํด ๊ตฌํ
๋ฆฌํ๋ ์ ํ ํฐ ํจํด์ ์งง์ ์๋ช ์ ์ก์ธ์ค ํ ํฐ๊ณผ ๊ธด ์๋ช ์ ๋ฆฌํ๋ ์ ํ ํฐ์ ํจ๊ป ์ฌ์ฉํ๋ ๋ฐฉ์์ด์ผ. ์ด๋ ๊ฒ ํ๋ฉด ๋ณด์์ ๊ฐํํ๋ฉด์๋ ์ฌ์ฉ์ ๊ฒฝํ์ ํด์น์ง ์์ ์ ์์ด.
๋ฆฌํ๋ ์ ํ ํฐ ํจํด ๊ตฌํ ์์
// ๋ฆฌํ๋ ์ ํ ํฐ ์ ์ฅ์ (์ค์ ๋ก๋ DB ์ฌ์ฉ)
const refreshTokens = {};
// ๋ก๊ทธ์ธ ๋ฐ ํ ํฐ ๋ฐ๊ธ
app.post('/login', (req, res) => {
// ์ฌ์ฉ์ ์ธ์ฆ (์๋ต)
// ์ก์ธ์ค ํ ํฐ ์์ฑ (์งง์ ์๋ช
)
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
JWT_SECRET,
{ expiresIn: '15m' } // 15๋ถ
);
// ๋ฆฌํ๋ ์ ํ ํฐ ์์ฑ (๊ธด ์๋ช
)
const refreshToken = crypto.randomBytes(40).toString('hex');
// ๋ฆฌํ๋ ์ ํ ํฐ ์ ์ฅ
refreshTokens[refreshToken] = {
userId: user.id,
expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000) // 7์ผ
};
// ์๋ต์ผ๋ก ๋ ํ ํฐ ๋ชจ๋ ๋ฐํ
res.json({ accessToken, refreshToken });
});
// ๋ฆฌํ๋ ์ ํ ํฐ์ผ๋ก ์ ์ก์ธ์ค ํ ํฐ ๋ฐ๊ธ
app.post('/refresh-token', (req, res) => {
const { refreshToken } = req.body;
// ๋ฆฌํ๋ ์ ํ ํฐ ๊ฒ์ฆ
if (!refreshToken || !refreshTokens[refreshToken]) {
return res.status(401).json({ error: '์ ํจํ์ง ์์ ๋ฆฌํ๋ ์ ํ ํฐ' });
}
const tokenData = refreshTokens[refreshToken];
// ๋ง๋ฃ ํ์ธ
if (tokenData.expiresAt < Date.now()) {
delete refreshTokens[refreshToken];
return res.status(401).json({ error: '๋ฆฌํ๋ ์ ํ ํฐ์ด ๋ง๋ฃ๋จ' });
}
// ์ ์ก์ธ์ค ํ ํฐ ๋ฐ๊ธ
const accessToken = jwt.sign(
{ userId: tokenData.userId, role: user.role },
JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken });
});
// ๋ก๊ทธ์์ (๋ฆฌํ๋ ์ ํ ํฐ ๋ฌดํจํ)
app.post('/logout', (req, res) => {
const { refreshToken } = req.body;
// ๋ฆฌํ๋ ์ ํ ํฐ ์ญ์
delete refreshTokens[refreshToken];
res.json({ message: '๋ก๊ทธ์์ ์ฑ๊ณต' });
});
๐ก ํ: ๋ฆฌํ๋ ์ ํ ํฐ์ ์ก์ธ์ค ํ ํฐ๊ณผ ๋ฌ๋ฆฌ ์๋ฒ์ ์ ์ฅํด์ผ ํด. ์ด๋ ๊ฒ ํ๋ฉด ํ์ํ ๋ ๋ฆฌํ๋ ์ ํ ํฐ์ ๋ฌดํจํํ ์ ์์ด!
JWT๋ ๊ฐ๋ ฅํ์ง๋ง, ๋ ๋ณต์กํ ์ธ์ฆ ์๋๋ฆฌ์ค์์๋ OAuth 2.0๊ณผ OpenID Connect๊ฐ ํ์ํ ์ ์์ด. ๋ค์ ์น์ ์์ ์์๋ณด์! ๐
10. OAuth 2.0๊ณผ OpenID Connect ๐
๋ณต์กํ ์ธ์ฆ ์๋๋ฆฌ์ค, ํนํ ์๋ํํฐ ์๋น์ค ํตํฉ์ด ํ์ํ ๊ฒฝ์ฐ OAuth 2.0๊ณผ OpenID Connect๋ ๊ฐ๋ ฅํ ์๋ฃจ์ ์ ์ ๊ณตํด. ์ฌ๋ฅ๋ท๊ณผ ๊ฐ์ ํ๋ซํผ์์ ์์ ๋ก๊ทธ์ธ์ ๊ตฌํํ ๋ ํนํ ์ ์ฉํ์ง! ๐
OAuth 2.0๊ณผ OpenID Connect ์ดํดํ๊ธฐ
ํ๋กํ ์ฝ | ์ฃผ์ ๋ชฉ์ | ์ฌ์ฉ ์ฌ๋ก |
---|---|---|
OAuth 2.0 | ๊ถํ ๋ถ์ฌ (Authorization) |
- ์๋ํํฐ ์ฑ์ ์ ํ๋ ์ ๊ทผ ๊ถํ ๋ถ์ฌ - API ์ ๊ทผ ๊ถํ ๊ด๋ฆฌ - ์๋น์ค ๊ฐ ํตํฉ |
OpenID Connect | ์ธ์ฆ (Authentication) |
- ์์
๋ก๊ทธ์ธ - SSO(Single Sign-On) - ์ฌ์ฉ์ ์ ์ ํ์ธ |
OpenID Connect๋ OAuth 2.0 ์์ ๊ตฌ์ถ๋ ์ธ์ฆ ๋ ์ด์ด์ผ. OAuth 2.0์ด "์ด ์ฑ์ด ๋ด ๋ฐ์ดํฐ์ ์ ๊ทผํ ์ ์๋์?"๋ฅผ ๋ค๋ฃฌ๋ค๋ฉด, OpenID Connect๋ "์ด ์ฌ์ฉ์๊ฐ ๋๊ตฌ์ธ๊ฐ์?"๋ฅผ ๋ค๋ค.
OAuth 2.0 ์ฃผ์ ์ฉ์ด
- ๋ฆฌ์์ค ์์ ์: ๋ณดํธ๋ ๋ฆฌ์์ค์ ์ ๊ทผ ๊ถํ์ ๋ถ์ฌํ๋ ์ฌ์ฉ์
- ํด๋ผ์ด์ธํธ: ์ฌ์ฉ์๋ฅผ ๋์ ํ์ฌ ๋ณดํธ๋ ๋ฆฌ์์ค์ ์ ๊ทผํ๋ ค๋ ์ ํ๋ฆฌ์ผ์ด์
- ์ธ์ฆ ์๋ฒ: ์ฌ์ฉ์๋ฅผ ์ธ์ฆํ๊ณ ํด๋ผ์ด์ธํธ์๊ฒ ํ ํฐ์ ๋ฐ๊ธํ๋ ์๋ฒ
- ๋ฆฌ์์ค ์๋ฒ: ๋ณดํธ๋ ๋ฆฌ์์ค๋ฅผ ํธ์คํ ํ๋ ์๋ฒ
- ์ก์ธ์ค ํ ํฐ: ๋ณดํธ๋ ๋ฆฌ์์ค์ ์ ๊ทผํ๊ธฐ ์ํ ์๊ฒฉ ์ฆ๋ช
- ๋ฆฌํ๋ ์ ํ ํฐ: ์ ์ก์ธ์ค ํ ํฐ์ ์ป๊ธฐ ์ํ ์๊ฒฉ ์ฆ๋ช
- ์ค์ฝํ: ํ ํฐ์ผ๋ก ์ ๊ทผํ ์ ์๋ ๊ถํ์ ๋ฒ์
OAuth 2.0 ๊ถํ ๋ถ์ฌ ์ ํ
๊ถํ ๋ถ์ฌ ์ ํ | ์ค๋ช | ์ ํฉํ ์ํฉ |
---|---|---|
์ธ์ฆ ์ฝ๋ (Authorization Code) | ์ฌ์ฉ์ ์ธ์ฆ ํ ์ฝ๋๋ฅผ ๋ฐ๊ธ๋ฐ๊ณ , ์ด ์ฝ๋๋ก ํ ํฐ ๊ตํ | ์๋ฒ ์ฌ์ด๋ ์น ์ฑ, ๋ชจ๋ฐ์ผ ์ฑ (PKCE์ ํจ๊ป) |
์์์ (Implicit) | ์ธ์ฆ ํ ๋ฐ๋ก ์ก์ธ์ค ํ ํฐ ๋ฐ๊ธ (๋ ์ด์ ๊ถ์ฅ๋์ง ์์) | ๋ ๊ฑฐ์ SPA (ํ์ฌ๋ ์ธ์ฆ ์ฝ๋ + PKCE ๊ถ์ฅ) |
๋ฆฌ์์ค ์์ ์ ๋น๋ฐ๋ฒํธ ์๊ฒฉ ์ฆ๋ช (Password) | ์ฌ์ฉ์ ์ด๋ฆ๊ณผ ๋น๋ฐ๋ฒํธ๋ก ์ง์ ํ ํฐ ์์ฒญ | ์์ฌ ์ ํ๋ฆฌ์ผ์ด์ , ๋์ ์ ๋ขฐ๋๊ฐ ํ์ํ ๊ฒฝ์ฐ |
ํด๋ผ์ด์ธํธ ์๊ฒฉ ์ฆ๋ช (Client Credentials) | ํด๋ผ์ด์ธํธ ID์ ์ํฌ๋ฆฟ์ผ๋ก ํ ํฐ ์์ฒญ (์ฌ์ฉ์ ์์) | ์๋ฒ ๊ฐ ํต์ , ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ |
โ ๏ธ ์ฃผ์! 2025๋ ํ์ฌ, ๋ชจ๋ฐ์ผ ์ฑ๊ณผ SPA์๋ ์ธ์ฆ ์ฝ๋ ํ๋ฆ + PKCE(Proof Key for Code Exchange)๊ฐ ๊ฐ์ฅ ์์ ํ ๋ฐฉ์์ผ๋ก ๊ถ์ฅ๋ผ. ์์์ ํ๋ฆ์ ๋ ์ด์ ๊ถ์ฅ๋์ง ์์!
OpenID Connect ์ถ๊ฐ ๊ธฐ๋ฅ
OpenID Connect๋ OAuth 2.0์ ๋ค์๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ์ถ๊ฐํด:
- ID ํ ํฐ: ์ฌ์ฉ์ ์ ์ ์ ๋ณด๋ฅผ ํฌํจํ๋ JWT
- UserInfo ์๋ํฌ์ธํธ: ์ฌ์ฉ์์ ๋ํ ์ถ๊ฐ ์ ๋ณด๋ฅผ ์ป๋ ํ์คํ๋ API
- ํ์ค ํด๋ ์: ์ด๋ฆ, ์ด๋ฉ์ผ ๋ฑ ์ฌ์ฉ์ ์ ๋ณด์ ๋ํ ํ์คํ๋ ํ๋
- ๊ฒ์ ๊ฐ๋ฅํ ๋ฉํ๋ฐ์ดํฐ: ์๋น์ค ๊ตฌ์ฑ์ ์๋์ผ๋ก ๋ฐ๊ฒฌํ ์ ์๋ ๊ธฐ๋ฅ
OAuth 2.0 + OpenID Connect ๊ตฌํ ์์
Node.js์์ OAuth 2.0 ํด๋ผ์ด์ธํธ ๊ตฌํ (Google ๋ก๊ทธ์ธ)
const express = require('express');
const axios = require('axios');
const app = express();
// OAuth ์ค์
const config = {
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
redirectUri: 'http://localhost:3000/auth/callback',
authUrl: 'https://accounts.google.com/o/oauth2/auth',
tokenUrl: 'https://oauth2.googleapis.com/token',
userInfoUrl: 'https://www.googleapis.com/oauth2/v3/userinfo',
scope: 'openid profile email'
};
// ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ
app.get('/auth/login', (req, res) => {
// PKCE์ฉ ์ฝ๋ ๊ฒ์ฆ๊ธฐ ์์ฑ (์ค์ ๊ตฌํ์์๋ ์ํธํ์ ์ผ๋ก ์์ ํ ๋ฐฉ์ ์ฌ์ฉ)
const codeVerifier = generateRandomString(64);
// ์ฝ๋ ๊ฒ์ฆ๊ธฐ๋ฅผ ํด์ํ์ฌ ์ฝ๋ ์ฑ๋ฆฐ์ง ์์ฑ
const codeChallenge = base64UrlEncode(sha256(codeVerifier));
// ์ธ์
์ ์ฝ๋ ๊ฒ์ฆ๊ธฐ ์ ์ฅ (์ค์ ๊ตฌํ์์๋ ์ธ์
์ฌ์ฉ)
req.session.codeVerifier = codeVerifier;
// ์ธ์ฆ URL ์์ฑ
const authUrl = new URL(config.authUrl);
authUrl.searchParams.append('client_id', config.clientId);
authUrl.searchParams.append('redirect_uri', config.redirectUri);
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('scope', config.scope);
authUrl.searchParams.append('code_challenge', codeChallenge);
authUrl.searchParams.append('code_challenge_method', 'S256');
authUrl.searchParams.append('state', generateRandomString(16)); // CSRF ๋ฐฉ์ง
// ์ธ์ฆ ์๋ฒ๋ก ๋ฆฌ๋ค์ด๋ ํธ
res.redirect(authUrl.toString());
});
// ์ฝ๋ฐฑ ์ฒ๋ฆฌ
app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query;
// ์ค๋ฅ ์ฒ๋ฆฌ
if (!code) {
return res.status(400).send('์ธ์ฆ ์ฝ๋๊ฐ ์์ต๋๋ค');
}
try {
// ์ฝ๋๋ฅผ ํ ํฐ์ผ๋ก ๊ตํ
const tokenResponse = await axios.post(config.tokenUrl, {
client_id: config.clientId,
client_secret: config.clientSecret,
code,
code_verifier: req.session.codeVerifier, // ์ธ์
์์ ์ฝ๋ ๊ฒ์ฆ๊ธฐ ๊ฐ์ ธ์ค๊ธฐ
redirect_uri: config.redirectUri,
grant_type: 'authorization_code'
});
const { access_token, id_token, refresh_token } = tokenResponse.data;
// ID ํ ํฐ ๊ฒ์ฆ (์ค์ ๊ตฌํ์์๋ ์๋ช
๊ฒ์ฆ ํ์)
const decodedIdToken = decodeJwt(id_token);
// ์ก์ธ์ค ํ ํฐ์ผ๋ก ์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
const userInfoResponse = await axios.get(config.userInfoUrl, {
headers: { Authorization: `Bearer ${access_token}` }
});
const userInfo = userInfoResponse.data;
// ์ฌ์ฉ์ ์ ๋ณด๋ก ๋ก๊ทธ์ธ ์ฒ๋ฆฌ
// ...
// ํ ํฐ์ ์์ ํ๊ฒ ์ ์ฅ (HttpOnly ์ฟ ํค ๋ฑ)
// ...
res.redirect('/dashboard');
} catch (error) {
console.error('OAuth ์ค๋ฅ:', error);
res.status(500).send('์ธ์ฆ ์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค');
}
});
OAuth 2.0 ๋ณด์ ๋ชจ๋ฒ ์ฌ๋ก
- PKCE ์ฌ์ฉ: ๋ชจ๋ ๊ณต๊ฐ ํด๋ผ์ด์ธํธ(๋ชจ๋ฐ์ผ ์ฑ, SPA)์ PKCE ์ ์ฉ
- ์ํ ํ๋ผ๋ฏธํฐ ๊ฒ์ฆ: CSRF ๊ณต๊ฒฉ ๋ฐฉ์ง๋ฅผ ์ํด state ํ๋ผ๋ฏธํฐ ์ฌ์ฉ ๋ฐ ๊ฒ์ฆ
- ๋ฆฌ๋ค์ด๋ ํธ URI ๊ฒ์ฆ: ๋ฑ๋ก๋ ๋ฆฌ๋ค์ด๋ ํธ URI๋ง ํ์ฉ
- ์ต์ ๊ถํ ์์น: ํ์ํ ์ค์ฝํ๋ง ์์ฒญ
- ID ํ ํฐ ์๋ช ๊ฒ์ฆ: OpenID Connect ID ํ ํฐ์ ์๋ช ๋ฐ๋์ ๊ฒ์ฆ
- ํ ํฐ ์์ ํ๊ฒ ์ ์ฅ: ์ก์ธ์ค ํ ํฐ๊ณผ ๋ฆฌํ๋ ์ ํ ํฐ์ HttpOnly, Secure ์ฟ ํค์ ์ ์ฅ
- ํด๋ผ์ด์ธํธ ์ํฌ๋ฆฟ ๋ณดํธ: ํด๋ผ์ด์ธํธ ์ํฌ๋ฆฟ์ ์ ๋ ํด๋ผ์ด์ธํธ ์ธก ์ฝ๋์ ํฌํจํ์ง ์๊ธฐ
๐ก ํ: ์ง์ OAuth 2.0/OpenID Connect ์๋ฒ๋ฅผ ๊ตฌํํ๋ ๊ฒ์ ๋ณต์กํ๊ณ ๋ณด์ ์ํ์ด ์์ด. Auth0, Okta, Keycloak ๊ฐ์ ๊ฒ์ฆ๋ ์๋ฃจ์ ์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์!
OAuth์ OpenID Connect๋ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ํฉํ์ง๋ง, ๋ชจ๋ฐ์ผ ์ฑ์์๋ ์ถ๊ฐ์ ์ธ ๊ณ ๋ ค์ฌํญ์ด ์์ด. ๋ค์ ์น์ ์์ ์์๋ณด์! ๐ฑ
11. ๋ชจ๋ฐ์ผ ์ฑ์์์ ์ธ์ ๊ด๋ฆฌ ๐ฑ
๋ชจ๋ฐ์ผ ์ฑ์์์ ์ธ์ ๊ด๋ฆฌ๋ ์น๊ณผ๋ ๋ค๋ฅธ ์ ๊ทผ ๋ฐฉ์์ด ํ์ํด. ๋ค์ดํฐ๋ธ ์ ์ฅ์, ์์ฒด ์ธ์ฆ, ์ฑ ์๋ช ์ฃผ๊ธฐ ๋ฑ ๋ชจ๋ฐ์ผ ํ๊ฒฝ์ ํน์ฑ์ ๊ณ ๋ คํด์ผ ํด! ๐
๋ชจ๋ฐ์ผ ์ฑ ์ธ์ ๊ด๋ฆฌ์ ํน์ง
๋ชจ๋ฐ์ผ ์ฑ ํ๊ฒฝ์ ์น๊ณผ ๋ค๋ฅธ ํน์ฑ์ ๊ฐ์ง๊ณ ์์ด:
- ์ฅ๊ธฐ ์ธ์ : ๋ชจ๋ฐ์ผ ์ฑ์ ์ผ๋ฐ์ ์ผ๋ก ๋ ๊ธด ์ธ์ ์๋ช ์ ๊ฐ์ง
- ๋ค์ดํฐ๋ธ ์ ์ฅ์: ์ฟ ํค ๋์ ํ๋ซํผ๋ณ ๋ณด์ ์ ์ฅ์ ์ฌ์ฉ
- ์ฑ ์๋ช ์ฃผ๊ธฐ: ๋ฐฑ๊ทธ๋ผ์ด๋ ์ ํ, ์ข ๋ฃ, ์ฌ์์ ๋ฑ ์ฑ ์ํ ๋ณํ ์ฒ๋ฆฌ ํ์
- ์คํ๋ผ์ธ ๋ชจ๋: ๋คํธ์ํฌ ์ฐ๊ฒฐ ์์ด๋ ์๋ํด์ผ ํ๋ ๊ฒฝ์ฐ ๊ณ ๋ ค
- ๊ธฐ๊ธฐ๋ณ ๋ณด์ ๊ธฐ๋ฅ: ์์ฒด ์ธ์ฆ, ํ๋์จ์ด ๋ณด์ ๋ชจ๋ ๋ฑ ํ์ฉ ๊ฐ๋ฅ
ํ ํฐ ์ ์ฅ ๋ฐฉ์ ๋น๊ต
์ ์ฅ ๋ฐฉ์ | iOS | Android | ๋ณด์ ์์ค |
---|---|---|---|
๋ณด์ ์ ์ฅ์ | Keychain | Keystore / StrongBox | ๋์ (ํ๋์จ์ด ์ง์ ๊ฐ๋ฅ) |
์ํธํ๋ ์ค์ | EncryptedUserDefaults | EncryptedSharedPreferences | ์ค๊ฐ~๋์ (๊ตฌํ์ ๋ฐ๋ผ ๋ค๋ฆ) |
๋ณด์ ํ์ผ | Data Protection API | File Encryption | ์ค๊ฐ~๋์ |
์ผ๋ฐ ์ค์ | UserDefaults | SharedPreferences | ๋ฎ์ (์ํธํ ์์) |
์ธ๋ฉ๋ชจ๋ฆฌ | ๋ณ์์ ์ ์ฅ | ๋ณ์์ ์ ์ฅ | ์ฑ ์คํ ์ค์๋ง ์์ |
โ ๏ธ ์ฃผ์! ์ ๋ ๋ฏผ๊ฐํ ํ ํฐ์ ์ํธํ ์์ด SharedPreferences, UserDefaults, ๋ก์ปฌ ์คํ ๋ฆฌ์ง์ ์ ์ฅํ์ง ๋ง์ธ์. ๋ฃจํ /ํ์ฅ๋ ๊ธฐ๊ธฐ์์๋ ์ฝ๊ฒ ์ถ์ถ๋ ์ ์์ต๋๋ค!
๋ชจ๋ฐ์ผ ์ฑ ์ธ์ ๊ด๋ฆฌ ๋ชจ๋ฒ ์ฌ๋ก
- ๋ฆฌํ๋ ์ ํ ํฐ ํจํด ์ฌ์ฉ: ์งง์ ์๋ช ์ ์ก์ธ์ค ํ ํฐ + ๊ธด ์๋ช ์ ๋ฆฌํ๋ ์ ํ ํฐ
- ๋ณด์ ์ ์ฅ์ ํ์ฉ: ํ ํฐ์ Keychain(iOS) ๋๋ Keystore(Android)์ ์ ์ฅ
- ํ ํฐ ์ํธํ: ์ ์ฅ ์ ์ถ๊ฐ ์ํธํ ๋ ์ด์ด ์ ์ฉ
- ์์ฒด ์ธ์ฆ ํตํฉ: ์ค์ ์์ ์ Touch ID/Face ID ๋๋ ์ง๋ฌธ ์ธ์ฆ ์๊ตฌ
- ์ฑ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ฒ๋ฆฌ: ์ฑ์ด ๋ฐฑ๊ทธ๋ผ์ด๋๋ก ์ ํ๋ ๋ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ๋ณดํธ ๋๋ ์ธ์ ์ ๊ธ
- ์ธ์ฆ์ ํผ๋: ์๋ฒ ์ธ์ฆ์ ๊ฒ์ฆ์ผ๋ก MITM ๊ณต๊ฒฉ ๋ฐฉ์ง
- ๋ฃจํธ/ํ์ฅ ํ์ง: ๋ฃจํ /ํ์ฅ๋ ๊ธฐ๊ธฐ์์ ์ถ๊ฐ ๋ณด์ ์กฐ์น ์ ์ฉ
- ๋คํธ์ํฌ ๋ณด์: ํญ์ HTTPS ์ฌ์ฉ, ๋คํธ์ํฌ ๋ณด์ ๊ตฌ์ฑ ์ ์ฉ
iOS์์์ ์์ ํ ํ ํฐ ์ ์ฅ
Swift์์ Keychain ์ฌ์ฉ ์์
import Security
class KeychainManager {
static func save(token: String, service: String, account: String) -> Bool {
// ๊ธฐ์กด ํญ๋ชฉ ์ญ์
_ = delete(service: service, account: account)
let tokenData = token.data(using: .utf8)!
// Keychain ์ฟผ๋ฆฌ ์์ฑ
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecValueData as String: tokenData,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
// Keychain์ ์ ์ฅ
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
static func load(service: String, account: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let data = result as? Data,
let token = String(data: data, encoding: .utf8) else {
return nil
}
return token
}
static func delete(service: String, account: String) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account
]
let status = SecItemDelete(query as CFDictionary)
return status == errSecSuccess || status == errSecItemNotFound
}
}
// ์ฌ์ฉ ์
func saveTokens(accessToken: String, refreshToken: String) {
_ = KeychainManager.save(token: accessToken, service: "com.yourapp.tokens", account: "accessToken")
_ = KeychainManager.save(token: refreshToken, service: "com.yourapp.tokens", account: "refreshToken")
}
func getAccessToken() -> String? {
return KeychainManager.load(service: "com.yourapp.tokens", account: "accessToken")
}
Android์์์ ์์ ํ ํ ํฐ ์ ์ฅ
Kotlin์์ EncryptedSharedPreferences ์ฌ์ฉ ์์
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
class SecureTokenManager(context: Context) {
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
private val sharedPreferences = EncryptedSharedPreferences.create(
context,
"secure_tokens",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun saveAccessToken(token: String) {
sharedPreferences.edit().putString("access_token", token).apply()
}
fun saveRefreshToken(token: String) {
sharedPreferences.edit().putString("refresh_token", token).apply()
}
fun getAccessToken(): String? {
return sharedPreferences.getString("access_token", null)
}
fun getRefreshToken(): String? {
return sharedPreferences.getString("refresh_token", null)
}
fun clearTokens() {
sharedPreferences.edit().clear().apply()
}
}
// ์ฌ์ฉ ์
val tokenManager = SecureTokenManager(context)
tokenManager.saveAccessToken("eyJhbGciOiJIUzI1...")
val token = tokenManager.getAccessToken()
์์ฒด ์ธ์ฆ ํตํฉ
์์ฒด ์ธ์ฆ์ ์ธ์ ๊ด๋ฆฌ์ ํตํฉํ๋ฉด ๋ณด์์ ํฌ๊ฒ ๊ฐํํ ์ ์์ด:
iOS์์ ์์ฒด ์ธ์ฆ ์์ (Swift)
import LocalAuthentication
func authenticateWithBiometrics(completion: @escaping (Bool, Error?) -> Void) {
let context = LAContext()
var error: NSError?
// ์์ฒด ์ธ์ฆ ๊ฐ๋ฅ ์ฌ๋ถ ํ์ธ
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
let reason = "๋ฏผ๊ฐํ ๋ฐ์ดํฐ์ ์ ๊ทผํ๊ธฐ ์ํด ์ธ์ฆ์ด ํ์ํฉ๋๋ค"
// ์์ฒด ์ธ์ฆ ์์ฒญ
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, error in
DispatchQueue.main.async {
completion(success, error)
}
}
} else {
completion(false, error)
}
}
// ์ฌ์ฉ ์
func accessSecureFeature() {
authenticateWithBiometrics { success, error in
if success {
// ์ธ์ฆ ์ฑ๊ณต, ๋ณดํธ๋ ๊ธฐ๋ฅ ์ ๊ทผ ํ์ฉ
loadProtectedData()
} else {
// ์ธ์ฆ ์คํจ
showAuthenticationError(error)
}
}
}
Android์์ ์์ฒด ์ธ์ฆ ์์ (Kotlin)
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
fun showBiometricPrompt(activity: FragmentActivity, onSuccess: () -> Unit) {
val executor = ContextCompat.getMainExecutor(activity)
val biometricPrompt = BiometricPrompt(activity, executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
// ์ธ์ฆ ์ฑ๊ณต
onSuccess()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
// ์ธ์ฆ ์ค๋ฅ
Toast.makeText(activity, "์ธ์ฆ ์ค๋ฅ: $errString", Toast.LENGTH_SHORT).show()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
// ์ธ์ฆ ์คํจ
Toast.makeText(activity, "์ธ์ฆ์ ์คํจํ์ต๋๋ค", Toast.LENGTH_SHORT).show()
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("์์ฒด ์ธ์ฆ")
.setSubtitle("๊ณ์ํ๋ ค๋ฉด ์ง๋ฌธ์ ์ฌ์ฉํ์ฌ ์ธ์ฆํ์ธ์")
.setNegativeButtonText("์ทจ์")
.build()
biometricPrompt.authenticate(promptInfo)
}
// ์ฌ์ฉ ์
fun accessProtectedFeature() {
showBiometricPrompt(this) {
// ์ธ์ฆ ์ฑ๊ณต ํ ์คํํ ์ฝ๋
loadSecureData()
}
}
์ฑ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ฒ๋ฆฌ
์ฑ์ด ๋ฐฑ๊ทธ๋ผ์ด๋๋ก ์ ํ๋ ๋ ์ธ์ ๋ณด์์ ๊ฐํํ๋ ๋ฐฉ๋ฒ:
iOS์์ ์ฑ ์ํ ๋ณํ ์ฒ๋ฆฌ (Swift)
import UIKit
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// ์ฑ ์์ ์ ์ค์
setupAppStateNotifications()
return true
}
func setupAppStateNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(appDidEnterBackground),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(appWillEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
}
@objc func appDidEnterBackground() {
// ์ฑ์ด ๋ฐฑ๊ทธ๋ผ์ด๋๋ก ์ ํ๋ ๋
// 1. ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ๋ฉ๋ชจ๋ฆฌ์์ ์ ๊ฑฐ
clearSensitiveDataFromMemory()
// 2. ์ธ์
์ ๊ธ ์ค์
SessionManager.shared.lockSession()
// 3. ์คํฌ๋ฆฐ์ท ๋ฐฉ์ง (ํ์์)
preventScreenshots()
}
@objc func appWillEnterForeground() {
// ์ฑ์ด ํฌ๊ทธ๋ผ์ด๋๋ก ๋์์ฌ ๋
// ์ธ์
์ํ ํ์ธ ๋ฐ ํ์์ ์ฌ์ธ์ฆ ์๊ตฌ
SessionManager.shared.checkSessionStatus()
}
func clearSensitiveDataFromMemory() {
// ๋ฉ๋ชจ๋ฆฌ์์ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ์ ๊ฑฐ
// ...
}
func preventScreenshots() {
// ์คํฌ๋ฆฐ์ท ๋ฐฉ์ง ๋ก์ง
// ...
}
}
Android์์ ์ฑ ์ํ ๋ณํ ์ฒ๋ฆฌ (Kotlin)
import android.app.Application
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
class MyApplication : Application(), LifecycleObserver {
override fun onCreate() {
super.onCreate()
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onAppBackgrounded() {
// ์ฑ์ด ๋ฐฑ๊ทธ๋ผ์ด๋๋ก ์ ํ๋ ๋
// 1. ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ๋ฉ๋ชจ๋ฆฌ์์ ์ ๊ฑฐ
clearSensitiveDataFromMemory()
// 2. ์ธ์
์ ๊ธ ์ค์
SessionManager.getInstance(this).lockSession()
// 3. ์คํฌ๋ฆฐ์ท ๋ฐฉ์ง (ํ์์)
preventScreenshots()
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onAppForegrounded() {
// ์ฑ์ด ํฌ๊ทธ๋ผ์ด๋๋ก ๋์์ฌ ๋
// ์ธ์
์ํ ํ์ธ ๋ฐ ํ์์ ์ฌ์ธ์ฆ ์๊ตฌ
SessionManager.getInstance(this).checkSessionStatus()
}
private fun clearSensitiveDataFromMemory() {
// ๋ฉ๋ชจ๋ฆฌ์์ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ์ ๊ฑฐ
// ...
}
private fun preventScreenshots() {
// ์คํฌ๋ฆฐ์ท ๋ฐฉ์ง ๋ก์ง
// ...
}
}
๐ก ํ: ๋ชจ๋ฐ์ผ ์ฑ์์๋ ์ธ์ ํ์์์ ์ธ์๋ ์ฑ ๋นํ์ฑ ์๊ฐ์ ๊ธฐ์ค์ผ๋ก ์๋ ๋ก๊ทธ์์์ด๋ ์ฌ์ธ์ฆ์ ์๊ตฌํ๋ ๊ฒ์ด ์ข์. ์๋ฅผ ๋ค์ด, ์ฑ์ด 5๋ถ ์ด์ ๋ฐฑ๊ทธ๋ผ์ด๋์ ์์๋ค๋ฉด ๋ค์ ์ธ์ฆ์ ์๊ตฌํ ์ ์์ด!
๋ชจ๋ฐ์ผ ์ฑ์์์ ์ธ์ ๊ด๋ฆฌ๋ฅผ ๋ง์คํฐํ๋ค๋ฉด, ์ด์ 2025๋ ์ต์ ์ธ์ ๊ด๋ฆฌ ํธ๋ ๋๋ฅผ ์ดํด๋ณด์! ๐
12. 2025๋ ์ต์ ์ธ์ ๊ด๋ฆฌ ํธ๋ ๋ ๐
2025๋ ํ์ฌ, ์ธ์ ๊ด๋ฆฌ ๊ธฐ์ ์ ๊ณ์ ๋ฐ์ ํ๊ณ ์์ด. ์ต์ ํธ๋ ๋์ ๊ธฐ์ ์ ์์๋ณด๊ณ ์์๊ฐ๋ ๊ฐ๋ฐ์๊ฐ ๋์! ๐ฎ
1. ํจ์คํค(Passkeys) ๋์ ํ๋
ํจ์คํค๋ FIDO2 ํ์ค์ ๊ธฐ๋ฐ์ผ๋ก ํ ๋น๋ฐ๋ฒํธ ์๋ ์ธ์ฆ ๋ฐฉ์์ผ๋ก, 2025๋ ์๋ ์ฃผ๋ฅ ์ธ์ฆ ๋ฐฉ์์ผ๋ก ์๋ฆฌ์ก๊ณ ์์ด.
ํจ์คํค(Passkeys)๋? ์์ฒด ์ธ์์ด๋ ๊ธฐ๊ธฐ PIN์ ์ฌ์ฉํ์ฌ ๊ณต๊ฐํค ์ํธํ ๊ธฐ๋ฐ์ผ๋ก ์ธ์ฆํ๋ ๋ฐฉ์์ผ๋ก, ํผ์ฑ์ ๊ฐํ๊ณ ์ฌ์ฉ์ ๊ฒฝํ์ด ์ฐ์ํด. Apple, Google, Microsoft ๋ฑ ์ฃผ์ ๊ธฐ์ ๋ค์ด ๋ชจ๋ ์ง์ํ๊ณ ์์ด.
ํจ์คํค์ ์ฃผ์ ์ฅ์ :
- ํผ์ฑ ๋ฐฉ์ง: ๋๋ฉ์ธ์ ๋ฐ์ธ๋ฉ๋์ด ํผ์ฑ ์ฌ์ดํธ์์ ์ฌ์ฉ ๋ถ๊ฐ
- ๋น๋ฐ๋ฒํธ ์ ๊ฑฐ: ๊ธฐ์ตํด์ผ ํ ๋น๋ฐ๋ฒํธ ์์
- ๊ฐ๋ ฅํ ๋ณด์: ๊ณต๊ฐํค ์ํธํ ๊ธฐ๋ฐ์ผ๋ก ๋งค์ฐ ์์
- ํธ๋ฆฌํ ์ฌ์ฉ์ฑ: ์ง๋ฌธ, ์ผ๊ตด ์ธ์ ๋ฑ ๊ฐํธํ ์ธ์ฆ
- ํฌ๋ก์ค ํ๋ซํผ: ๋ค์ํ ๊ธฐ๊ธฐ ๊ฐ ๋๊ธฐํ ์ง์
WebAuthn์ ์ด์ฉํ ํจ์คํค ๊ตฌํ ์์
// ์๋ฒ ์ธก ์ฝ๋ (Node.js)
const { generateRegistrationOptions, verifyRegistrationResponse } = require('@simplewebauthn/server');
// ๋ฑ๋ก ์ต์
์์ฑ
app.get('/api/passkey/register/options', async (req, res) => {
const userId = req.user.id;
const username = req.user.username;
const options = await generateRegistrationOptions({
rpName: '์ฌ๋ฅ๋ท',
rpID: 'jaenung.net',
userID: userId,
userName: username,
attestationType: 'none',
authenticatorSelection: {
userVerification: 'preferred',
residentKey: 'required',
}
});
// ์ธ์
์ ์ฑ๋ฆฐ์ง ์ ์ฅ
req.session.currentChallenge = options.challenge;
res.json(options);
});
// ๋ฑ๋ก ์๋ต ๊ฒ์ฆ
app.post('/api/passkey/register/verify', async (req, res) => {
const { attestationResponse } = req.body;
try {
const verification = await verifyRegistrationResponse({
response: attestationResponse,
expectedChallenge: req.session.currentChallenge,
expectedOrigin: 'https://jaenung.net',
expectedRPID: 'jaenung.net'
});
if (verification.verified) {
// ๊ฒ์ฆ ์ฑ๊ณต, ์ฌ์ฉ์์ ํจ์คํค ์ ์ฅ
const { credentialID, credentialPublicKey } = verification.registrationInfo;
// ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํจ์คํค ์ ๋ณด ์ ์ฅ
await savePasskeyToDatabase(req.user.id, credentialID, credentialPublicKey);
res.json({ success: true });
} else {
res.status(400).json({ success: false, error: 'ํจ์คํค ๋ฑ๋ก ์คํจ' });
}
} catch (error) {
res.status(400).json({ success: false, error: error.message });
}
});
// ํด๋ผ์ด์ธํธ ์ธก ์ฝ๋ (JavaScript)
async function registerPasskey() {
try {
// ์๋ฒ์์ ๋ฑ๋ก ์ต์
๊ฐ์ ธ์ค๊ธฐ
const optionsResponse = await fetch('/api/passkey/register/options');
const options = await optionsResponse.json();
// ๋ธ๋ผ์ฐ์ ์ WebAuthn API ํธ์ถ
const attestation = await navigator.credentials.create({
publicKey: {
challenge: base64URLToBuffer(options.challenge),
rp: {
name: options.rp.name,
id: options.rp.id
},
user: {
id: base64URLToBuffer(options.user.id),
name: options.user.name,
displayName: options.user.displayName
},
pubKeyCredParams: options.pubKeyCredParams,
authenticatorSelection: options.authenticatorSelection,
timeout: options.timeout
}
});
// ์๋ต์ ์๋ฒ๋ก ์ ์ก
const response = await fetch('/api/passkey/register/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
attestationResponse: {
id: attestation.id,
rawId: bufferToBase64URL(attestation.rawId),
response: {
clientDataJSON: bufferToBase64URL(attestation.response.clientDataJSON),
attestationObject: bufferToBase64URL(attestation.response.attestationObject)
},
type: attestation.type
}
})
});
const result = await response.json();
if (result.success) {
showSuccess('ํจ์คํค๊ฐ ์ฑ๊ณต์ ์ผ๋ก ๋ฑ๋ก๋์์ต๋๋ค!');
} else {
showError('ํจ์คํค ๋ฑ๋ก ์คํจ: ' + result.error);
}
} catch (error) {
showError('ํจ์คํค ๋ฑ๋ก ์ค ์ค๋ฅ ๋ฐ์: ' + error.message);
}
}
2. ์ ๋ก ํธ๋ฌ์คํธ ์ํคํ ์ฒ(Zero Trust Architecture)
์ ๋ก ํธ๋ฌ์คํธ๋ "์ ๋ขฐํ์ง ๋ง๊ณ ํญ์ ๊ฒ์ฆํ๋ผ"๋ ๋ณด์ ์์น์ผ๋ก, 2025๋ ์๋ ์ธ์ ๊ด๋ฆฌ์๋ ๊น์ด ํตํฉ๋๊ณ ์์ด.
์ ๋ก ํธ๋ฌ์คํธ ์ธ์ ๊ด๋ฆฌ์ ํน์ง:
- ์ง์์ ์ธ ๊ฒ์ฆ: ์ธ์ ์ค์๋ ์ฃผ๊ธฐ์ ์ผ๋ก ์ฌ์ฉ์์ ๊ธฐ๊ธฐ ์ฌ๊ฒ์ฆ
- ์ต์ ๊ถํ ์์น: ํ์ํ ์ต์ํ์ ์ ๊ทผ ๊ถํ๋ง ๋ถ์ฌ
- ์ปจํ ์คํธ ๊ธฐ๋ฐ ์ ๊ทผ ์ ์ด: ์ฌ์ฉ์ ์์น, ๊ธฐ๊ธฐ ์ํ, ํ๋ ํจํด ๋ฑ ๊ณ ๋ ค
- ๋ง์ดํฌ๋ก ์ธ๊ทธ๋ฉํ ์ด์ : ๋ฆฌ์์ค๋ฅผ ์ธ๋ถํํ์ฌ ์ ๊ทผ ์ ์ด
- ์ํธํ๋ ํต์ : ๋ชจ๋ ํต์ ์ ํญ์ ์ํธํ
์ ๋ก ํธ๋ฌ์คํธ ์ธ์ ๊ด๋ฆฌ ๊ตฌํ ์์
// ์ง์์ ์ธ ์ธ์
๊ฒ์ฆ ๋ฏธ๋ค์จ์ด
function continuousSessionVerification(req, res, next) {
// 1. ๊ธฐ๋ณธ ํ ํฐ ๊ฒ์ฆ
const token = extractTokenFromRequest(req);
if (!isTokenValid(token)) {
return res.status(401).json({ error: '์ ํจํ์ง ์์ ์ธ์
' });
}
// 2. ์ปจํ
์คํธ ๊ธฐ๋ฐ ์ํ ํ๊ฐ
const riskScore = assessRisk(req);
// 3. ์ํ ์์ค์ ๋ฐ๋ฅธ ์กฐ์น
if (riskScore > 80) {
// ๋์ ์ํ: ์ธ์
์ข
๋ฃ ๋ฐ ์ฌ์ธ์ฆ ์๊ตฌ
invalidateSession(token);
return res.status(403).json({
error: '๋ณด์์์ ์ด์ ๋ก ์ฌ์ธ์ฆ์ด ํ์ํฉ๋๋ค',
action: 'REAUTHENTICATE'
});
} else if (riskScore > 50) {
// ์ค๊ฐ ์ํ: ์ถ๊ฐ ์ธ์ฆ ์๊ตฌ
req.requireStepUpAuth = true;
next();
} else {
// ๋ฎ์ ์ํ: ์ ์ ์งํ
next();
}
}
// ์ํ ํ๊ฐ ํจ์
function assessRisk(req) {
let riskScore = 0;
// 1. IP ์ฃผ์ ๋ณ๊ฒฝ ํ์ธ
if (hasIpChanged(req)) {
riskScore += 30;
}
// 2. ๋น์ ์์ ์ธ ์์ฒญ ํจํด ํ์ธ
if (isAbnormalRequestPattern(req)) {
riskScore += 25;
}
// 3. ๋ฏผ๊ฐํ ์์
ํ์ธ
if (isSensitiveOperation(req.path)) {
riskScore += 20;
}
// 4. ๊ธฐ๊ธฐ ์ํ ํ์ธ
const deviceHealth = getDeviceHealthFromHeader(req);
if (deviceHealth !== 'healthy') {
riskScore += 15;
}
// 5. ์ธ์
๋์ด ํ์ธ
const sessionAge = getSessionAge(req);
if (sessionAge > 4 * 60 * 60 * 1000) { // 4์๊ฐ
riskScore += 10;
}
return riskScore;
}
// ์ถ๊ฐ ์ธ์ฆ ๋ฏธ๋ค์จ์ด
function stepUpAuthMiddleware(req, res, next) {
if (req.requireStepUpAuth) {
// ์ถ๊ฐ ์ธ์ฆ ํ์
return res.status(403).json({
error: '์ด ์์
์ ์ํด ์ถ๊ฐ ์ธ์ฆ์ด ํ์ํฉ๋๋ค',
action: 'STEP_UP_AUTH'
});
}
next();
}
3. ๋ถ์ฐ ID(Decentralized Identity, DID)
๋ธ๋ก์ฒด์ธ ๊ธฐ์ ์ ํ์ฉํ ๋ถ์ฐ ID๋ ์ฌ์ฉ์๊ฐ ์์ ์ ์ ์ ์ ๋ณด๋ฅผ ์ง์ ์ ์ดํ ์ ์๊ฒ ํด์ฃผ๋ ํ์ ์ ์ธ ์ ๊ทผ ๋ฐฉ์์ด์ผ.
๋ถ์ฐ ID์ ์ฃผ์ ํน์ง:
- ์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์ฌ๋ฅ๋ท์ ์ต์ AI ๊ธฐ์ ๊ณผ ๋ฒ๋ฅ ์ ๊ธฐ๋ฐํ์ฌ ์์ฌ์ ์ง์ ์ฌ์ฐ๊ถ์ ์ ๊ทน์ ์ผ๋ก ๋ณดํธํ๋ฉฐ,
๋ฌด๋จ ์ฌ์ฉ ๋ฐ ์นจํด ํ์์ ๋ํด ๋ฒ์ ๋์์ ํ ๊ถ๋ฆฌ๋ฅผ ๋ณด์ ํฉ๋๋ค.
ยฉ 2025 ์ฌ๋ฅ๋ท | All rights reserved.
๋๊ธ 0๊ฐ