Ver3.0 ๐ ์์ ๋ก๊ทธ์ธ ํตํฉ๊ณผ OAuth๋ฅผ ํ์ฉํ ์ฌ์ฉ์ ์ธ์ฆ ๊ฐ์ํ: ์ฑ ๊ฐ๋ฐ์๋ฅผ ์ํ ์๋ฒฝ ๊ฐ์ด๋

๐ ์์ ๋ก๊ทธ์ธ ํตํฉ๊ณผ OAuth๋ฅผ ํ์ฉํ ์ฌ์ฉ์ ์ธ์ฆ ๊ฐ์ํ: ์ฑ ๊ฐ๋ฐ์๋ฅผ ์ํ ์๋ฒฝ ๊ฐ์ด๋
๋ณต์กํ ํ์๊ฐ์ ์ ์ด์ ๊ทธ๋ง! ํด๋ฆญ ํ ๋ฒ์ผ๋ก ๋๋๋ ๋ง๋ฒ ๊ฐ์ ์ธ์ฆ ์์คํ ๊ตฌ์ถํ๊ธฐ โจ
์๋
, ์น๊ตฌ! ๐ ํน์ ์๋ก์ด ์ฑ์ ๋ค์ด๋ฐ์๋๋ฐ ํ์๊ฐ์
์์์ด ๋๋ฌด ๊ธธ์ด์ ๊ทธ๋ฅ ์ญ์ ํด๋ฒ๋ฆฐ ๊ฒฝํ ์์ด? ์๋๋ฉด ๋น๋ฐ๋ฒํธ๋ฅผ ๋ ์์ด๋ฒ๋ ค์ '๋น๋ฐ๋ฒํธ ์ฐพ๊ธฐ'๋ฅผ ์์ญ ๋ฒ ํด๋ฆญํ ์ ์? ์ฐ๋ฆฌ ๋ชจ๋ ๊ทธ๋ฐ ๊ฒฝํ์ด ์์ ๊ฑฐ์ผ. ๐
๋ฐ๋ก ์ด๋ฐ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ฑ์ฅํ ๊ฒ ์์
๋ก๊ทธ์ธ์ด์ผ! ๊ตฌ๊ธ, ํ์ด์ค๋ถ, ์นด์นด์ค, ๋ค์ด๋ฒ ๊ฐ์ ํ๋ซํผ์ ๊ณ์ ์ผ๋ก ๋จ ๋ช ์ด ๋ง์ ๋ก๊ทธ์ธํ ์ ์๋ ์ด ๊ธฐ์ ์ ์ด์ ๊ฑฐ์ ๋ชจ๋ ์ฑ์์ ํ์๊ฐ ๋์์ง.
์ค๋์ ์ด ์์
๋ก๊ทธ์ธ์ ํต์ฌ ๊ธฐ์ ์ธ OAuth์ ๋ํด ๊น์ด ํํค์ณ๋ณด๊ณ , ์ค์ ๋ก ์ด๋ป๊ฒ ๊ตฌํํ๋์ง ์น๊ตฌ์๊ฒ ์ค๋ช
ํ๋ฏ ์ฝ๊ณ ์ฌ๋ฏธ์๊ฒ ์๋ ค์ค๊ฒ! ๐
๐ค ์ ์์ ๋ก๊ทธ์ธ์ด ํ์ํ ๊น?
๋จผ์ ์์
๋ก๊ทธ์ธ์ด ์ ์ด๋ ๊ฒ ์ธ๊ธฐ๋ฅผ ๋๊ฒ ๋๋์ง๋ถํฐ ์ด์ผ๊ธฐํด๋ณผ๊น?
์ฌ์ฉ์ ์
์ฅ์์๋:
โข ์๋ก์ด ๊ณ์ ์ ๋ง๋ค ํ์๊ฐ ์์ด ๐
โข ๋ณต์กํ ๋น๋ฐ๋ฒํธ๋ฅผ ๋ ๋ง๋ค๊ณ ๊ธฐ์ตํ ํ์๊ฐ ์์ด
โข ํด๋ฆญ 2~3๋ฒ์ด๋ฉด ๋ก๊ทธ์ธ ์๋ฃ
โข ์ด๋ฉ์ผ ์ธ์ฆ ๊ฐ์ ๊ท์ฐฎ์ ์ ์ฐจ ์๋ต
โข ์ ๋ขฐํ ์ ์๋ ๋ํ ํ๋ซํผ์ ๋ณด์ ์์คํ
ํ์ฉ
๊ฐ๋ฐ์ ์
์ฅ์์๋:
โข ํ์๊ฐ์
์ ํ์จ์ด ๋ํญ ์์น (ํ๊ท 20~40% ์ฆ๊ฐ!) ๐
โข ์ฌ์ฉ์ ์ธ์ฆ ์์คํ
๊ตฌ์ถ ๋น์ฉ ์ ๊ฐ
โข ๋น๋ฐ๋ฒํธ ๊ด๋ฆฌ, ์ด๋ฉ์ผ ์ธ์ฆ ๋ฑ์ ๋ถ๋ด ๊ฐ์
โข ์ฌ์ฉ์์ ๊ธฐ๋ณธ ํ๋กํ ์ ๋ณด๋ฅผ ์ฝ๊ฒ ๊ฐ์ ธ์ฌ ์ ์์
โข ๋ณด์ ์ทจ์ฝ์ ๋ฆฌ์คํฌ ๊ฐ์
์ค์ ๋ก ํ ์ฐ๊ตฌ์ ๋ฐ๋ฅด๋ฉด, ์์
๋ก๊ทธ์ธ์ ๋์
ํ ์ฑ์ ๊ทธ๋ ์ง ์์ ์ฑ์ ๋นํด ์ฌ์ฉ์ ๊ฐ์
๋ฅ ์ด ํ๊ท 35% ์ฆ๊ฐํ๋ค๊ณ ํด. ์์ฒญ๋ ์ฐจ์ด์ง? ๐ช
๐ก ์ฌ๋ฅ๋ท ํ! ์ฌ๋ฅ๋ท(https://www.jaenung.net)์์๋ ๋ค์ํ ๊ฐ๋ฐ์๋ค์ด ์์ ๋ก๊ทธ์ธ ๊ตฌํ ์๋น์ค๋ฅผ ์ ๊ณตํ๊ณ ์์ด. ์ง์ ๊ตฌํํ๊ธฐ ์ด๋ ต๋ค๋ฉด ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ!
๐ OAuth๋ ๋ฌด์์ธ๊ฐ?
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก OAuth์ ๋ํด ์์๋ณผ ์๊ฐ์ด์ผ!
OAuth๋ Open Authorization์ ์ฝ์๋ก, ์ฌ์ฉ์๊ฐ ๋น๋ฐ๋ฒํธ๋ฅผ ์ ๊ณตํ์ง ์๊ณ ๋ ๋ค๋ฅธ ์น์ฌ์ดํธ๋ ์ ํ๋ฆฌ์ผ์ด์
์ ์์ ์ ์ ๋ณด์ ๋ํ ์ ๊ทผ ๊ถํ์ ๋ถ์ฌํ ์ ์๊ฒ ํด์ฃผ๋ ๊ฐ๋ฐฉํ ํ์ค ํ๋กํ ์ฝ์ด์ผ. ๐ก๏ธ
์ฝ๊ฒ ๋งํ๋ฉด, "๋ด ๊ตฌ๊ธ ๊ณ์ ์ผ๋ก ์ด ์ฑ์ ๋ก๊ทธ์ธํ ๊ฒ. ๊ทผ๋ฐ ๋ด ๋น๋ฐ๋ฒํธ๋ ์ ๋ ์๋ ค์ฃผ์ง ์์ ๊ฑฐ์ผ!"๋ผ๊ณ ๋งํ๋ ๊ฑฐ์ง.
OAuth์ ํต์ฌ ๊ฐ๋
:
1. Resource Owner (์์ ์์ ์): ๋ฐ๋ก ์ฌ์ฉ์, ์ฆ ๋! ๐ค
2. Client (ํด๋ผ์ด์ธํธ): ์ฌ์ฉ์์ ์ ๋ณด์ ์ ๊ทผํ๋ ค๋ ์ ํ๋ฆฌ์ผ์ด์
(๋ค๊ฐ ๋ง๋ ์ฑ)
3. Authorization Server (์ธ์ฆ ์๋ฒ): ์ฌ์ฉ์์ ๋์๋ฅผ ๋ฐ๊ณ ํ ํฐ์ ๋ฐ๊ธํ๋ ์๋ฒ
4. Resource Server (์์ ์๋ฒ): ์ค์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ง๊ณ ์๋ ์๋ฒ
ํ์ฌ ๊ฐ์ฅ ๋๋ฆฌ ์ฌ์ฉ๋๋ ๋ฒ์ ์ OAuth 2.0์ด์ผ. OAuth 1.0์ ๋นํด ํจ์ฌ ๊ฐ๋จํ๊ณ ๋ชจ๋ฐ์ผ ํ๊ฒฝ์ ์ต์ ํ๋์ด ์์ง. ๐ฑ
๐ก ์์๋๋ฉด ์ข์์! OAuth 2.0์ 2012๋ ์ ๊ณต์ ํ์ค์ผ๋ก ์ฑํ๋์์ด. ๊ทธ ์ดํ๋ก ๊ตฌ๊ธ, ํ์ด์ค๋ถ, ํธ์ํฐ ๋ฑ ๊ฑฐ์ ๋ชจ๋ ์ฃผ์ ํ๋ซํผ์ด OAuth 2.0์ ๊ธฐ๋ฐ์ผ๋ก API๋ฅผ ์ ๊ณตํ๊ณ ์์ง!
OAuth 2.0์ ์ฃผ์ ์ธ์ฆ ๋ฐฉ์ (Grant Types)
OAuth 2.0์ ๋ค์ํ ์ํฉ์ ๋ง๋ ์ฌ๋ฌ ๊ฐ์ง ์ธ์ฆ ๋ฐฉ์์ ์ ๊ณตํด. ๊ฐ๊ฐ์ ํน์ง์ ์ดํด๋ณผ๊น? ๐
| ์ธ์ฆ ๋ฐฉ์ | ์ค๋ช | ์ฃผ์ ์ฌ์ฉ์ฒ |
|---|---|---|
| Authorization Code | ๊ฐ์ฅ ์์ ํ๊ณ ์ผ๋ฐ์ ์ธ ๋ฐฉ์. ์ธ์ฆ ์ฝ๋๋ฅผ ๋จผ์ ๋ฐ๊ณ ํ ํฐ์ผ๋ก ๊ตํ | ์น ์ ํ๋ฆฌ์ผ์ด์ , ๋ชจ๋ฐ์ผ ์ฑ |
| Implicit | ํ ํฐ์ ๋ฐ๋ก ๋ฐ๋ ๊ฐ์ํ๋ ๋ฐฉ์ (๋ณด์์ ๊ถ์ฅ๋์ง ์์) | ๋จ์ํ SPA (ํ์ฌ๋ ๊ฑฐ์ ์ฌ์ฉ ์ ํจ) |
| Resource Owner Password | ์ฌ์ฉ์ ID/PW๋ฅผ ์ง์ ์ ๋ ฅ๋ฐ๋ ๋ฐฉ์ | ์ ๋ขฐํ ์ ์๋ ์์ฌ ์ฑ |
| Client Credentials | ์ฌ์ฉ์ ์์ด ์ฑ ์์ฒด๊ฐ ์ธ์ฆ | ์๋ฒ ๊ฐ ํต์ , API ํธ์ถ |
| PKCE | Authorization Code์ ๋ณด์ ๊ฐํ ๋ฒ์ | ๋ชจ๋ฐ์ผ ์ฑ, ๋ค์ดํฐ๋ธ ์ฑ |
๋ชจ๋ฐ์ผ ์ฑ ๊ฐ๋ฐ์๋ผ๋ฉด Authorization Code + PKCE ๋ฐฉ์์ ์ฌ์ฉํ๋ ๊ฒ ๊ฐ์ฅ ์ข์. ์ด๊ฒ ํ์ฌ ์ ๊ณ ํ์ค์ด์ ๊ฐ์ฅ ์์ ํ ๋ฐฉ๋ฒ์ด๊ฑฐ๋ ! โ
๐ฏ OAuth ์ธ์ฆ ํ๋ฆ ์๋ฒฝ ์ดํดํ๊ธฐ
์, ์ด์ OAuth๊ฐ ์ค์ ๋ก ์ด๋ป๊ฒ ์๋ํ๋์ง ๋จ๊ณ๋ณ๋ก ์ดํด๋ณด์. ์นดํ์์ ์ปคํผ๋ฅผ ์ฃผ๋ฌธํ๋ ๊ฒ์ฒ๋ผ ์ฝ๊ฒ ์ค๋ช ํด์ค๊ฒ! โ
Step 1: ์ฌ์ฉ์๊ฐ "๊ตฌ๊ธ๋ก ๋ก๊ทธ์ธ" ๋ฒํผ ํด๋ฆญ ๐
์ฌ์ฉ์๊ฐ ๋ค ์ฑ์์ ์์
๋ก๊ทธ์ธ ๋ฒํผ์ ๋๋ฅด๋ฉด, ์ฑ์ ์ฌ์ฉ์๋ฅผ ๊ตฌ๊ธ์ ์ธ์ฆ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ์์ผ. ์ด๋ ๋ค์๊ณผ ๊ฐ์ ์ ๋ณด๋ค์ด ํจ๊ป ์ ๋ฌ๋ผ:
โข client_id: ๋ค ์ฑ์ ์๋ณํ๋ ๊ณ ์ ID
โข redirect_uri: ์ธ์ฆ ํ ๋์์ฌ ์ฃผ์
โข scope: ์์ฒญํ๋ ๊ถํ ๋ฒ์ (์ด๋ฉ์ผ, ํ๋กํ ๋ฑ)
โข state: CSRF ๊ณต๊ฒฉ ๋ฐฉ์ง์ฉ ๋๋ค ๊ฐ
โข code_challenge: PKCE ์ฌ์ฉ ์ ๋ณด์ ์ฝ๋
Step 2: ์ฌ์ฉ์๊ฐ ๊ตฌ๊ธ์ ๋ก๊ทธ์ธํ๊ณ ๊ถํ ์น์ธ โ๏ธ
๊ตฌ๊ธ ๋ก๊ทธ์ธ ํ์ด์ง์์ ์ฌ์ฉ์๊ฐ ์์ ์ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธํ๋ฉด, ๊ตฌ๊ธ์ "์ด ์ฑ์ด ๋น์ ์ ์ด๋ฉ์ผ๊ณผ ํ๋กํ ์ ๋ณด์ ์ ๊ทผํ๋ ค๊ณ ํด์. ํ์ฉํ์๊ฒ ์ด์?"๋ผ๊ณ ๋ฌผ์ด๋ด.
์ฌ์ฉ์๊ฐ "ํ์ฉ"์ ๋๋ฅด๋ฉด ๋ค์ ๋จ๊ณ๋ก ๋์ด๊ฐ๋ ๊ฑฐ์ง!
Step 3: ์ธ์ฆ ์ฝ๋ ๋ฐ๊ธ ๐ซ
์ฌ์ฉ์๊ฐ ์น์ธํ๋ฉด, ๊ตฌ๊ธ์ ์ฌ์ฉ์๋ฅผ ๋ค๊ฐ ์ง์ ํ redirect_uri๋ก ๋ค์ ๋ณด๋ด. ์ด๋ URL์ ์ธ์ฆ ์ฝ๋(authorization code)๋ฅผ ๋ถ์ฌ์ ๋ณด๋ด์ค.
์์: https://yourapp.com/callback?code=ABC123XYZ&state=random_value
์ด ์ฝ๋๋ ์ผํ์ฉ์ด๊ณ , ๋ณดํต 10๋ถ ์ ๋๋ง ์ ํจํด. ๋ง์น ์ํ๊ด ์
์ฅ๊ถ ๊ฐ์ ๊ฑฐ์ง!
Step 4: ์ก์ธ์ค ํ ํฐ์ผ๋ก ๊ตํ ๐
๋ค ์ฑ์ ์๋ฒ๋ ๋ฐ์ ์ธ์ฆ ์ฝ๋๋ฅผ ๊ฐ์ง๊ณ ๊ตฌ๊ธ์ ํ ํฐ ์๋ํฌ์ธํธ์ ์์ฒญ์ ๋ณด๋ด. ์ด๋ client_secret(์ฑ์ ๋น๋ฐ ํค)๋ ํจ๊ป ๋ณด๋ด์ "๋ ์ง์ง ์ ์ ์ฑ ๋ง์!"๋ผ๊ณ ์ฆ๋ช
ํ๋ ๊ฑฐ์ผ.
๊ตฌ๊ธ์ ๋ชจ๋ ๊ฒ ์ ์์ด๋ฉด ์ก์ธ์ค ํ ํฐ(access token)๊ณผ ๋ฆฌํ๋ ์ ํ ํฐ(refresh token)์ ๋ฐ๊ธํด์ค.
Step 5: ์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ ๐ฅ
์ด์ ์ก์ธ์ค ํ ํฐ์ ๊ฐ์ง๊ณ ๊ตฌ๊ธ์ API๋ฅผ ํธ์ถํ๋ฉด ์ฌ์ฉ์์ ์ด๋ฉ์ผ, ์ด๋ฆ, ํ๋กํ ์ฌ์ง ๋ฑ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ด!
์ด ์ ๋ณด๋ก ๋ค ์ฑ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฌ์ฉ์ ๊ณ์ ์ ๋ง๋ค๊ฑฐ๋ ๊ธฐ์กด ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ์ํค๋ฉด ๋! ๐
โ ๏ธ ๋ณด์ ์ฃผ์์ฌํญ! client_secret์ ์ ๋๋ก ํด๋ผ์ด์ธํธ ์ธก ์ฝ๋์ ๋ ธ์ถ๋๋ฉด ์ ๋ผ! ๋ฐ๋์ ์๋ฒ์์๋ง ์ฌ์ฉํด์ผ ํด. ๋ชจ๋ฐ์ผ ์ฑ์ ๊ฒฝ์ฐ PKCE๋ฅผ ์ฌ์ฉํด์ client_secret ์์ด๋ ์์ ํ๊ฒ ์ธ์ฆํ ์ ์์ด.
๐ ๏ธ ์ฃผ์ ์์ ํ๋ซํผ๋ณ OAuth ๊ตฌํ ๊ฐ์ด๋
์ด์ ์ค์ ์ด์ผ! ๊ฐ ํ๋ซํผ๋ณ๋ก OAuth๋ฅผ ์ด๋ป๊ฒ ๊ตฌํํ๋์ง ์์๋ณด์. ๐ป
1. ๊ตฌ๊ธ ๋ก๊ทธ์ธ (Google Sign-In)
๊ตฌ๊ธ์ ๊ฐ์ฅ ๋๋ฆฌ ์ฌ์ฉ๋๋ ์์
๋ก๊ทธ์ธ ์ค ํ๋์ผ. ๊ตฌํ๋ ๋น๊ต์ ๊ฐ๋จํ ํธ์ด์ง!
์ค๋น ๋จ๊ณ:
1. Google Cloud Console์ ์ ์ (console.cloud.google.com)
2. ์ ํ๋ก์ ํธ ์์ฑ
3. "API ๋ฐ ์๋น์ค" โ "์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด"๋ก ์ด๋
4. OAuth 2.0 ํด๋ผ์ด์ธํธ ID ์์ฑ
5. ์น์ธ๋ ๋ฆฌ๋๋ ์
URI ๋ฑ๋ก
Android ์ฑ ์์ ์ฝ๋:
// build.gradle์ ์์กด์ฑ ์ถ๊ฐ
implementation 'com.google.android.gms:play-services-auth:20.7.0'
// GoogleSignInOptions ์ค์
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build();
// GoogleSignInClient ์์ฑ
GoogleSignInClient mGoogleSignInClient = GoogleSignIn.getClient(this, gso);
// ๋ก๊ทธ์ธ ์ธํ
ํธ ์์
Intent signInIntent = mGoogleSignInClient.getSignInIntent();
startActivityForResult(signInIntent, RC_SIGN_IN);
// ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == RC_SIGN_IN) {
Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data);
try {
GoogleSignInAccount account = task.getResult(ApiException.class);
// ์๋ฒ๋ก ID ํ ํฐ ์ ์ก
String idToken = account.getIdToken();
sendTokenToServer(idToken);
} catch (ApiException e) {
Log.w(TAG, "Google sign in failed", e);
}
}
}
iOS ์ฑ ์์ ์ฝ๋ (Swift):
// GoogleSignIn SDK ์ค์น (CocoaPods)
// pod 'GoogleSignIn'
import GoogleSignIn
// ๋ก๊ทธ์ธ ๋ฒํผ ์ก์
@IBAction func signInWithGoogle(_ sender: Any) {
guard let clientID = FirebaseApp.app()?.options.clientID else { return }
let config = GIDConfiguration(clientID: clientID)
GIDSignIn.sharedInstance.configuration = config
GIDSignIn.sharedInstance.signIn(withPresenting: self) { result, error in
guard error == nil else { return }
guard let user = result?.user,
let idToken = user.idToken?.tokenString else { return }
// ์๋ฒ๋ก ID ํ ํฐ ์ ์ก
self.sendTokenToServer(idToken)
}
}
๐ก Pro Tip! ๊ตฌ๊ธ์ ID Token์ JWT(JSON Web Token) ํ์์ผ๋ก ์ ๊ณตํด. ์ด ํ ํฐ์ ์๋ฒ์์ ๊ฒ์ฆํ๋ฉด ์ฌ์ฉ์์ ์ด๋ฉ์ผ, ์ด๋ฆ ๋ฑ์ ์ ๋ณด๋ฅผ ์์ ํ๊ฒ ํ์ธํ ์ ์์ด!
2. ์นด์นด์ค ๋ก๊ทธ์ธ (Kakao Login)
ํ๊ตญ์์ ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉ๋๋ ์์
๋ก๊ทธ์ธ์ด์ง! ์นด์นด์คํก ์ฌ์ฉ์๋ผ๋ฉด ๋๊ตฌ๋ ์ฝ๊ฒ ๋ก๊ทธ์ธํ ์ ์์ด. ๐ฌ
์ค๋น ๋จ๊ณ:
1. Kakao Developers์์ ์ฑ ๋ฑ๋ก (developers.kakao.com)
2. ํ๋ซํผ ์ค์ (Android ํจํค์ง๋ช
, iOS Bundle ID)
3. ์นด์นด์ค ๋ก๊ทธ์ธ ํ์ฑํ
4. ๋์ ํญ๋ชฉ ์ค์ (ํ์/์ ํ ์ ๋ณด)
Android ์ฑ ์์:
// build.gradle
implementation "com.kakao.sdk:v2-user:2.19.0"
// ์ด๊ธฐํ (Application ํด๋์ค)
KakaoSdk.init(this, "YOUR_NATIVE_APP_KEY")
// ๋ก๊ทธ์ธ ์คํ
if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
// ์นด์นด์คํก์ผ๋ก ๋ก๊ทธ์ธ
UserApiClient.instance.loginWithKakaoTalk(context) { token, error ->
if (error != null) {
Log.e(TAG, "๋ก๊ทธ์ธ ์คํจ", error)
} else if (token != null) {
Log.i(TAG, "๋ก๊ทธ์ธ ์ฑ๊ณต ${token.accessToken}")
getUserInfo()
}
}
} else {
// ์นด์นด์ค ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ
UserApiClient.instance.loginWithKakaoAccount(context) { token, error ->
if (error != null) {
Log.e(TAG, "๋ก๊ทธ์ธ ์คํจ", error)
} else if (token != null) {
Log.i(TAG, "๋ก๊ทธ์ธ ์ฑ๊ณต ${token.accessToken}")
getUserInfo()
}
}
}
// ์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
fun getUserInfo() {
UserApiClient.instance.me { user, error ->
if (error != null) {
Log.e(TAG, "์ฌ์ฉ์ ์ ๋ณด ์์ฒญ ์คํจ", error)
} else if (user != null) {
Log.i(TAG, "์ฌ์ฉ์ ์ ๋ณด: ${user.kakaoAccount?.email}")
// ์๋ฒ๋ก ์ ๋ณด ์ ์ก
}
}
}
iOS ์ฑ ์์ (Swift):
// CocoaPods
// pod 'KakaoSDKUser'
import KakaoSDKUser
import KakaoSDKAuth
// ๋ก๊ทธ์ธ
if (UserApi.isKakaoTalkLoginAvailable()) {
// ์นด์นด์คํก ์ฑ์ผ๋ก ๋ก๊ทธ์ธ
UserApi.shared.loginWithKakaoTalk { (oauthToken, error) in
if let error = error {
print(error)
} else {
print("loginWithKakaoTalk() success.")
self.getUserInfo()
}
}
} else {
// ์นด์นด์ค ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ
UserApi.shared.loginWithKakaoAccount { (oauthToken, error) in
if let error = error {
print(error)
} else {
print("loginWithKakaoAccount() success.")
self.getUserInfo()
}
}
}
// ์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
func getUserInfo() {
UserApi.shared.me { (user, error) in
if let error = error {
print(error)
} else {
print("me() success.")
if let email = user?.kakaoAccount?.email {
print("Email: \(email)")
}
}
}
}
๐ฏ ์นด์นด์ค ๋ก๊ทธ์ธ ํน์ง! ์นด์นด์ค๋ ์นด์นด์คํก ์ฑ์ด ์ค์น๋์ด ์์ผ๋ฉด ์ฑ ๊ฐ ์ ํ์ผ๋ก ๋ ๋น ๋ฅด๊ณ ํธ๋ฆฌํ๊ฒ ๋ก๊ทธ์ธํ ์ ์์ด. ์ฑ์ด ์์ผ๋ฉด ์๋์ผ๋ก ์น ๋ก๊ทธ์ธ์ผ๋ก ์ ํ๋์ง!
3. ๋ค์ด๋ฒ ๋ก๊ทธ์ธ (Naver Login)
๋ค์ด๋ฒ๋ ํ๊ตญ์์ ๋ง์ด ์ฌ์ฉ๋๋ ํ๋ซํผ์ด์ผ. ํนํ ์ผํ๋ชฐ์ด๋ ๋ธ๋ก๊ทธ ๊ด๋ จ ์ฑ์์ ์ ์ฉํ์ง! ๐๏ธ
์ค๋น ๋จ๊ณ:
1. ๋ค์ด๋ฒ ๊ฐ๋ฐ์ ์ผํฐ์์ ์ ํ๋ฆฌ์ผ์ด์
๋ฑ๋ก (developers.naver.com)
2. ์ฌ์ฉ API๋ฅผ "๋ค์ด๋ฒ ๋ก๊ทธ์ธ"์ผ๋ก ์ ํ
3. ์ ๊ณต ์ ๋ณด ์ ํ (์ด๋ฉ์ผ, ๋๋ค์, ํ๋กํ ์ด๋ฏธ์ง ๋ฑ)
4. ํ๊ฒฝ ์ถ๊ฐ (Android, iOS)
Android ์ฑ ์์:
// build.gradle
implementation 'com.navercorp.nid:oauth:5.7.0'
// ์ด๊ธฐํ
NaverIdLoginSDK.initialize(
context = this,
clientId = "YOUR_CLIENT_ID",
clientSecret = "YOUR_CLIENT_SECRET",
clientName = "YOUR_APP_NAME"
)
// ๋ก๊ทธ์ธ
val oauthLoginCallback = object : OAuthLoginCallback {
override fun onSuccess() {
// ๋ก๊ทธ์ธ ์ฑ๊ณต
val accessToken = NaverIdLoginSDK.getAccessToken()
getUserProfile(accessToken)
}
override fun onFailure(httpStatus: Int, message: String) {
Log.e(TAG, "๋ก๊ทธ์ธ ์คํจ: $message")
}
override fun onError(errorCode: Int, message: String) {
Log.e(TAG, "๋ก๊ทธ์ธ ์๋ฌ: $message")
}
}
NaverIdLoginSDK.authenticate(context, oauthLoginCallback)
// ํ๋กํ ๊ฐ์ ธ์ค๊ธฐ
fun getUserProfile(accessToken: String?) {
NidOAuthLogin().callProfileApi(object : NidProfileCallback<NidProfileResponse> {
override fun onSuccess(response: NidProfileResponse) {
val email = response.profile?.email
val name = response.profile?.name
Log.i(TAG, "Email: $email, Name: $name")
}
override fun onFailure(httpStatus: Int, message: String) {
Log.e(TAG, "ํ๋กํ ๊ฐ์ ธ์ค๊ธฐ ์คํจ: $message")
}
override fun onError(errorCode: Int, message: String) {
Log.e(TAG, "ํ๋กํ ์๋ฌ: $message")
}
})
}
4. ํ์ด์ค๋ถ ๋ก๊ทธ์ธ (Facebook Login)
๊ธ๋ก๋ฒ ์๋น์ค๋ผ๋ฉด ํ์ด์ค๋ถ ๋ก๊ทธ์ธ๋ ํ์! ์ ์ธ๊ณ 30์ต ๋ช
์ ์ฌ์ฉ์๋ฅผ ๋ณด์ ํ๊ณ ์์ด. ๐
์ค๋น ๋จ๊ณ:
1. Facebook for Developers์์ ์ฑ ์์ฑ (developers.facebook.com)
2. Facebook Login ์ ํ ์ถ๊ฐ
3. ํ๋ซํผ ์ค์ (Android, iOS)
4. ๊ถํ ์์ฒญ ํญ๋ชฉ ์ค์
Android ์ฑ ์์:
// build.gradle
implementation 'com.facebook.android:facebook-login:16.2.0'
// ์ด๊ธฐํ (Application ํด๋์ค)
FacebookSdk.sdkInitialize(applicationContext)
// ๋ก๊ทธ์ธ ๋ฒํผ ์ค์
callbackManager = CallbackManager.Factory.create()
loginButton.setPermissions("email", "public_profile")
loginButton.registerCallback(callbackManager, object : FacebookCallback<LoginResult> {
override fun onSuccess(loginResult: LoginResult) {
Log.d(TAG, "facebook:onSuccess:$loginResult")
handleFacebookAccessToken(loginResult.accessToken)
}
override fun onCancel() {
Log.d(TAG, "facebook:onCancel")
}
override fun onError(error: FacebookException) {
Log.d(TAG, "facebook:onError", error)
}
})
// ์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
fun handleFacebookAccessToken(token: AccessToken) {
val request = GraphRequest.newMeRequest(token) { jsonObject, response ->
val email = jsonObject?.getString("email")
val name = jsonObject?.getString("name")
Log.i(TAG, "Email: $email, Name: $name")
}
val parameters = Bundle()
parameters.putString("fields", "id,name,email,picture.type(large)")
request.parameters = parameters
request.executeAsync()
}
| ํ๋ซํผ | ์ฅ์ | ์ฃผ์ ์ฌ์ฉ ์ง์ญ | ํน์ด์ฌํญ |
|---|---|---|---|
| ๊ตฌ๊ธ | ๊ธ๋ก๋ฒ 1์, ์์ ์ , ๋ฌธ์ํ ์ฐ์ | ์ ์ธ๊ณ | Android ๊ธฐ๋ณธ ํตํฉ |
| ์นด์นด์ค | ํ๊ตญ ์ ์ ์จ 1์, ๋น ๋ฅธ ์ธ์ฆ | ํ๊ตญ | ์นด์นด์คํก ์ฑ ์ฐ๋ |
| ๋ค์ด๋ฒ | ํ๊ตญ ์ฌ์ฉ์ ๋ง์, ์ผํ ์ฐ๋ | ํ๊ตญ | ๋ค์ด๋ฒ ์๋น์ค ์ฐ๋ |
| ํ์ด์ค๋ถ | ๊ธ๋ก๋ฒ ์ฌ์ฉ์ ์ต๋ค | ์ ์ธ๊ณ (ํนํ ๋ฏธ๊ตญ, ์ ๋ฝ) | ์์ ๊ทธ๋ํ ํ์ฉ ๊ฐ๋ฅ |
| ์ ํ | iOS ํ์, ํ๋ผ์ด๋ฒ์ ๊ฐํ | iOS ์ฌ์ฉ์ | ์ด๋ฉ์ผ ์จ๊น ๊ธฐ๋ฅ |
๐ ๋ณด์ Best Practices
์์ ๋ก๊ทธ์ธ์ ๊ตฌํํ ๋ ๋ณด์์ ์ ๋ง ์ค์ํด! ๋ค์ ์ฌํญ๋ค์ ๊ผญ ์ง์ผ์ผ ํด. ๐ก๏ธ
1. PKCE (Proof Key for Code Exchange) ์ฌ์ฉํ๊ธฐ
๋ชจ๋ฐ์ผ ์ฑ์์๋ PKCE๋ฅผ ๋ฐ๋์ ์ฌ์ฉํด์ผ ํด. ์ด๊ฒ ๋ญ๋๋ฉด:
โข ์ฑ์ด ๋๋คํ code_verifier๋ฅผ ์์ฑ
โข ์ด๋ฅผ ํด์ํ code_challenge๋ฅผ ์ธ์ฆ ์์ฒญ ์ ์ ์ก
โข ํ ํฐ ๊ตํ ์ ์๋ณธ code_verifier๋ฅผ ํจ๊ป ๋ณด๋
โข ์๋ฒ๊ฐ ๊ฒ์ฆํด์ ์ค๊ฐ์ ๊ณต๊ฒฉ ๋ฐฉ์ง
์ด๋ ๊ฒ ํ๋ฉด client_secret ์์ด๋ ์์ ํ๊ฒ ์ธ์ฆํ ์ ์์ด!
// PKCE ๊ตฌํ ์์ (Kotlin)
import java.security.MessageDigest
import java.security.SecureRandom
import android.util.Base64
fun generateCodeVerifier(): String {
val secureRandom = SecureRandom()
val code = ByteArray(32)
secureRandom.nextBytes(code)
return Base64.encodeToString(code, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
}
fun generateCodeChallenge(verifier: String): String {
val bytes = verifier.toByteArray(Charsets.US_ASCII)
val messageDigest = MessageDigest.getInstance("SHA-256")
messageDigest.update(bytes, 0, bytes.size)
val digest = messageDigest.digest()
return Base64.encodeToString(digest, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
}
// ์ฌ์ฉ
val codeVerifier = generateCodeVerifier()
val codeChallenge = generateCodeChallenge(codeVerifier)
// ์ธ์ฆ ์์ฒญ ์ code_challenge ํฌํจ
// ํ ํฐ ๊ตํ ์ code_verifier ํฌํจ
2. State ํ๋ผ๋ฏธํฐ๋ก CSRF ๊ณต๊ฒฉ ๋ฐฉ์ง
state ํ๋ผ๋ฏธํฐ๋ CSRF(Cross-Site Request Forgery) ๊ณต๊ฒฉ์ ๋ง์์ค:
โข ์ธ์ฆ ์์ฒญ ์ ๋๋คํ state ๊ฐ ์์ฑ
โข ์ธ์
์ด๋ ๋ก์ปฌ ์คํ ๋ฆฌ์ง์ ์ ์ฅ
โข ์ฝ๋ฐฑ์์ ๋ฐ์ state์ ๋น๊ต
โข ์ผ์นํ์ง ์์ผ๋ฉด ์์ฒญ ๊ฑฐ๋ถ
// State ์์ฑ ๋ฐ ๊ฒ์ฆ ์์
import java.util.UUID
// ์ธ์ฆ ์์ฒญ ์
val state = UUID.randomUUID().toString()
sharedPreferences.edit().putString("oauth_state", state).apply()
// ์ฝ๋ฐฑ ์ฒ๋ฆฌ ์
val receivedState = intent.getStringExtra("state")
val savedState = sharedPreferences.getString("oauth_state", null)
if (receivedState != savedState) {
// CSRF ๊ณต๊ฒฉ ๊ฐ๋ฅ์ฑ!
Log.e(TAG, "State mismatch - possible CSRF attack")
return
}
// ๊ฒ์ฆ ์ฑ๊ณต ํ state ์ญ์
sharedPreferences.edit().remove("oauth_state").apply()
3. ํ ํฐ ์์ ํ๊ฒ ์ ์ฅํ๊ธฐ
์ก์ธ์ค ํ ํฐ๊ณผ ๋ฆฌํ๋ ์ ํ ํฐ์ ๋งค์ฐ ๋ฏผ๊ฐํ ์ ๋ณด์ผ. ์์ ํ๊ฒ ์ ์ฅํด์ผ ํด! ๐
Android์์์ ์์ ํ ์ ์ฅ:
โข EncryptedSharedPreferences ์ฌ์ฉ (Android Jetpack Security)
โข KeyStore๋ฅผ ํ์ฉํ ์ํธํ
โข ์ ๋ ์ผ๋ฐ SharedPreferences์ ํ๋ฌธ์ผ๋ก ์ ์ฅํ์ง ๋ง ๊ฒ!
iOS์์์ ์์ ํ ์ ์ฅ:
โข Keychain ์ฌ์ฉ
โข UserDefaults์ ์ ์ฅ ๊ธ์ง
โข Face ID/Touch ID์ ์ฐ๋ ๊ฐ๋ฅ
// Android - EncryptedSharedPreferences ์ฌ์ฉ
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val encryptedPrefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// ํ ํฐ ์ ์ฅ
encryptedPrefs.edit()
.putString("access_token", accessToken)
.putString("refresh_token", refreshToken)
.apply()
// iOS - Keychain ์ฌ์ฉ (Swift)
import Security
func saveToKeychain(key: String, value: String) {
let data = value.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data
]
SecItemDelete(query as CFDictionary)
SecItemAdd(query as CFDictionary, nil)
}
4. ํ ํฐ ๋ง๋ฃ ๋ฐ ๊ฐฑ์ ์ฒ๋ฆฌ
์ก์ธ์ค ํ ํฐ์ ๋ณดํต 1์๊ฐ ์ ๋๋ง ์ ํจํด. ๋ง๋ฃ๋๋ฉด ๋ฆฌํ๋ ์ ํ ํฐ์ผ๋ก ์๋ก์ด ์ก์ธ์ค ํ ํฐ์ ๋ฐ์์ผ ํด!
// ํ ํฐ ๊ฐฑ์ ๋ก์ง ์์
suspend fun refreshAccessToken(): String? {
val refreshToken = getRefreshToken() ?: return null
return try {
val response = apiService.refreshToken(
grantType = "refresh_token",
refreshToken = refreshToken,
clientId = CLIENT_ID
)
// ์ ํ ํฐ ์ ์ฅ
saveAccessToken(response.accessToken)
if (response.refreshToken != null) {
saveRefreshToken(response.refreshToken)
}
response.accessToken
} catch (e: Exception) {
Log.e(TAG, "Token refresh failed", e)
// ๋ฆฌํ๋ ์ ์คํจ ์ ์ฌ๋ก๊ทธ์ธ ํ์
null
}
}
// API ํธ์ถ ์ ์๋ ๊ฐฑ์
suspend fun <T> callApiWithTokenRefresh(apiCall: suspend () -> T): T? {
return try {
apiCall()
} catch (e: UnauthorizedException) {
// 401 ์๋ฌ ์ ํ ํฐ ๊ฐฑ์ ์๋
val newToken = refreshAccessToken()
if (newToken != null) {
// ๊ฐฑ์ ์ฑ๊ณต ์ ์ฌ์๋
apiCall()
} else {
// ๊ฐฑ์ ์คํจ ์ ๋ก๊ทธ์์
logout()
null
}
}
}
5. SSL/TLS ์ธ์ฆ์ ๊ฒ์ฆ
๋ชจ๋ OAuth ํต์ ์ ๋ฐ๋์ HTTPS๋ฅผ ํตํด ์ด๋ฃจ์ด์ ธ์ผ ํด. ๊ทธ๋ฆฌ๊ณ ์ธ์ฆ์ ๊ฒ์ฆ์ ์ฐํํ๋ฉด ์ ๋ ์ ๋ผ! โ ๏ธ
๐จ ์ ๋ ํ์ง ๋ง์์ผ ํ ๊ฒ๋ค!
โ ์ธ์ฆ์ ๊ฒ์ฆ ๋นํ์ฑํ
โ ํ ํฐ์ URL ํ๋ผ๋ฏธํฐ๋ก ์ ์ก
โ ํ ํฐ์ ๋ก๊ทธ์ ์ถ๋ ฅ
โ HTTP๋ก OAuth ํต์
โ client_secret์ ์ฑ์ ํ๋์ฝ๋ฉ
โ ํ ํฐ์ ์ธ๋ถ ์ ์ฅ์์ ์ ์ฅ
๐จ ์ฌ์ฉ์ ๊ฒฝํ(UX) ์ต์ ํ ํ
๊ธฐ์ ์ ์ผ๋ก ์๋ฒฝํด๋ ์ฌ์ฉ์ ๊ฒฝํ์ด ๋์๋ฉด ์์ฉ์์ด! ๋ค์ ํ๋ค๋ก UX๋ฅผ ๊ฐ์ ํด๋ณด์. โจ
1. ๋ช ํํ ๋ฒํผ ๋์์ธ
๊ฐ ํ๋ซํผ์ ๋ธ๋๋ ๊ฐ์ด๋๋ผ์ธ์ ๋ฐ๋ผ์ผ ํด:
โข ๊ตฌ๊ธ: "Sign in with Google" (ํ๋์/ํฐ์ ๋ฒํผ)
โข ์นด์นด์ค: "์นด์นด์ค ๋ก๊ทธ์ธ" (๋
ธ๋์ ๋ฒํผ)
โข ๋ค์ด๋ฒ: "๋ค์ด๋ฒ๋ก ๋ก๊ทธ์ธ" (์ด๋ก์ ๋ฒํผ)
โข ํ์ด์ค๋ถ: "Continue with Facebook" (ํ๋์ ๋ฒํผ)
โข ์ ํ: "Sign in with Apple" (๊ฒ์์/ํฐ์ ๋ฒํผ)
๊ฐ ํ๋ซํผ์ ๊ณต์ ๋์์ธ ๊ฐ์ด๋์ ๋ฒํผ ๋ฆฌ์์ค๋ฅผ ์ ๊ณตํ๋๊น ๊ผญ ํ์ฉํ์!
2. ๋ก๋ฉ ์ํ ํ์
OAuth ์ธ์ฆ์ ๋คํธ์ํฌ ํต์ ์ด ํ์ํด์ ์๊ฐ์ด ๊ฑธ๋ฆด ์ ์์ด. ์ฌ์ฉ์์๊ฒ ์งํ ์ํฉ์ ๋ณด์ฌ์ฃผ์:
// ๋ก๋ฉ UI ์์ (Kotlin + Jetpack Compose)
@Composable
fun SocialLoginButton(
platform: String,
onClick: () -> Unit,
isLoading: Boolean
) {
Button(
onClick = onClick,
enabled = !isLoading,
modifier = Modifier.fillMaxWidth()
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = Color.White
)
Spacer(modifier = Modifier.width(8.dp))
Text("๋ก๊ทธ์ธ ์ค...")
} else {
Icon(painter = painterResource(getPlatformIcon(platform)))
Spacer(modifier = Modifier.width(8.dp))
Text("$platform ๋ก๊ทธ์ธ")
}
}
}
3. ์๋ฌ ์ฒ๋ฆฌ ๋ฐ ์ฌ์ฉ์ ํผ๋๋ฐฑ
๋ก๊ทธ์ธ ์คํจ ์ ๋ช ํํ ๋ฉ์์ง๋ฅผ ๋ณด์ฌ์ค์ผ ํด:
์ข์ ์๋ฌ ๋ฉ์์ง ์์:
โ
"๋ก๊ทธ์ธ์ด ์ทจ์๋์์ต๋๋ค. ๋ค์ ์๋ํด์ฃผ์ธ์."
โ
"๋คํธ์ํฌ ์ฐ๊ฒฐ์ ํ์ธํด์ฃผ์ธ์."
โ
"์นด์นด์คํก ์ฑ์ด ์ค์น๋์ด ์์ง ์์ต๋๋ค. ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธํ์๊ฒ ์ต๋๊น?"
๋์ ์๋ฌ ๋ฉ์์ง ์์:
โ "Error code: 401"
โ "๋ก๊ทธ์ธ ์คํจ"
โ "์ ์ ์๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."
4. ๊ณ์ ์ฐ๊ฒฐ ๋ฐ ํด์ ๊ธฐ๋ฅ
์ฌ์ฉ์๊ฐ ์ฌ๋ฌ ์์ ๊ณ์ ์ ์ฐ๊ฒฐํ๊ฑฐ๋ ํด์ ํ ์ ์๊ฒ ํด์ฃผ์:
// ๊ณ์ ์ฐ๊ฒฐ ๊ด๋ฆฌ UI ์์
data class ConnectedAccount(
val platform: String,
val email: String,
val connectedAt: Long,
val isPrimary: Boolean
)
@Composable
fun AccountManagementScreen(accounts: List<ConnectedAccount>) {
LazyColumn {
items(accounts) { account ->
AccountItem(
account = account,
onDisconnect = {
if (!account.isPrimary) {
disconnectAccount(account)
}
}
)
}
item {
Button(onClick = { showAddAccountDialog() }) {
Text("๋ค๋ฅธ ๊ณ์ ์ฐ๊ฒฐํ๊ธฐ")
}
}
}
}
5. ์๋ ๋ก๊ทธ์ธ ๋ฐ ์ธ์ ๊ด๋ฆฌ
์ฌ์ฉ์๊ฐ ์ฑ์ ๋ค์ ์ด์์ ๋ ์๋์ผ๋ก ๋ก๊ทธ์ธ๋๊ฒ ํ์:
// ์๋ ๋ก๊ทธ์ธ ์ฒดํฌ
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
val isLoggedIn = checkLoginStatus()
if (isLoggedIn) {
// ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฆ
val isValid = validateToken()
if (isValid) {
navigateToMain()
} else {
// ํ ํฐ ๊ฐฑ์ ์๋
val refreshed = refreshAccessToken()
if (refreshed) {
navigateToMain()
} else {
navigateToLogin()
}
}
} else {
navigateToLogin()
}
}
}
}
๐ ํํ ๋ฌธ์ ์ ํด๊ฒฐ ๋ฐฉ๋ฒ
์ค์ ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ์ฌ๋ฌ ๋ฌธ์ ์ ๋ถ๋ชํ๊ฒ ๋ผ. ์์ฃผ ๋ฐ์ํ๋ ์ด์๋ค๊ณผ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์ ๋ฆฌํด๋ดค์ด! ๐ง
๋ฌธ์ 1: "Redirect URI mismatch" ์๋ฌ
์ฆ์:
๋ก๊ทธ์ธ ์๋ ์ "redirect_uri_mismatch" ๋๋ "The redirect URI provided does not match" ์๋ฌ ๋ฐ์
์์ธ:
์ฑ์์ ์ฌ์ฉํ๋ redirect URI๊ฐ ๊ฐ๋ฐ์ ์ฝ์์ ๋ฑ๋ก๋ URI์ ์ ํํ ์ผ์นํ์ง ์์
ํด๊ฒฐ ๋ฐฉ๋ฒ:
1. ๊ฐ๋ฐ์ ์ฝ์์์ ๋ฑ๋ก๋ redirect URI ํ์ธ
2. ์ฝ๋์์ ์ฌ์ฉํ๋ URI์ ์์ ํ ๋์ผํ์ง ์ฒดํฌ (๋์๋ฌธ์, ์ฌ๋์ ํฌํจ)
3. ํ๋กํ ์ฝ(http/https), ํฌํธ ๋ฒํธ๊น์ง ์ ํํ ์ผ์นํด์ผ ํจ
4. ์ฑ์ ๊ฒฝ์ฐ ์ปค์คํ
URL ์คํด์ด ์ ํํ ๋ฑ๋ก๋์๋์ง ํ์ธ
๋ฌธ์ 2: ํ ํฐ ๊ฒ์ฆ ์คํจ
์ฆ์:
ID ํ ํฐ ๊ฒ์ฆ ์ "Invalid token" ์๋ฌ ๋ฐ์
์์ธ:
โข ํ ํฐ์ด ๋ง๋ฃ๋จ
โข ์๋ชป๋ audience(aud) ํด๋ ์
โข ์๊ฐ ๋๊ธฐํ ๋ฌธ์
ํด๊ฒฐ ๋ฐฉ๋ฒ:
1. ํ ํฐ์ exp(๋ง๋ฃ ์๊ฐ) ํด๋ ์ ํ์ธ
2. aud ํด๋ ์์ด ์์ ์ client_id์ ์ผ์นํ๋์ง ํ์ธ
3. ์๋ฒ ์๊ฐ์ด ์ ํํ์ง ํ์ธ (NTP ๋๊ธฐํ)
4. ํ ํฐ ๊ฒ์ฆ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ ๊ถ์ฅ
// JWT ํ ํฐ ๊ฒ์ฆ ์์ (์๋ฒ ์ธก - Node.js)
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'https://www.googleapis.com/oauth2/v3/certs'
});
function getKey(header, callback) {
client.getSigningKey(header.kid, function(err, key) {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
function verifyGoogleToken(token) {
return new Promise((resolve, reject) => {
jwt.verify(token, getKey, {
audience: YOUR_CLIENT_ID,
issuer: 'https://accounts.google.com',
algorithms: ['RS256']
}, (err, decoded) => {
if (err) {
reject(err);
} else {
resolve(decoded);
}
});
});
}
๋ฌธ์ 3: ์นด์นด์คํก ์ฑ ๋ก๊ทธ์ธ ์คํจ
์ฆ์:
์นด์นด์คํก ์ฑ์ผ๋ก ๋ก๊ทธ์ธ ์๋ ์ ์คํจํ๊ฑฐ๋ ์น ๋ก๊ทธ์ธ์ผ๋ก ๋์ด๊ฐ
์์ธ:
โข ์ฑ์ ํค ํด์๊ฐ ๋ฑ๋ก๋์ง ์์
โข ์นด์นด์คํก ์ฑ์ด ์ค์น๋์ง ์์
โข ์นด์นด์คํก ๋ฒ์ ์ด ๋๋ฌด ๋ฎ์
ํด๊ฒฐ ๋ฐฉ๋ฒ:
1. ํค ํด์ ํ์ธ ๋ฐ ๋ฑ๋ก
// Android ํค ํด์ ํ์ธ ์ฝ๋
fun getKeyHash(context: Context): String {
try {
val info = context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SIGNATURES
)
for (signature in info.signatures) {
val md = MessageDigest.getInstance("SHA")
md.update(signature.toByteArray())
val keyHash = Base64.encodeToString(md.digest(), Base64.NO_WRAP)
Log.d("KeyHash", keyHash)
return keyHash
}
} catch (e: Exception) {
Log.e("KeyHash", "Error", e)
}
return ""
}
// ์ด ํค ํด์๋ฅผ ์นด์นด์ค ๊ฐ๋ฐ์ ์ฝ์์ ๋ฑ๋กํด์ผ ํจ!
๋ฌธ์ 4: iOS์์ ์ ํ ๋ก๊ทธ์ธ ์คํจ
์ฆ์:
"Sign in with Apple" ๋ฒํผ์ด ์๋ํ์ง ์๊ฑฐ๋ ์๋ฌ ๋ฐ์
์์ธ:
โข Capability๊ฐ ์ถ๊ฐ๋์ง ์์
โข Bundle ID๊ฐ ์๋ชป ์ค์ ๋จ
โข ํ๋ก๋น์ ๋ ํ๋กํ์ผ ๋ฌธ์
ํด๊ฒฐ ๋ฐฉ๋ฒ:
1. Xcode์์ Signing & Capabilities โ "Sign in with Apple" ์ถ๊ฐ
2. Apple Developer์์ App ID์ Sign in with Apple ํ์ฑํ
3. ํ๋ก๋น์ ๋ ํ๋กํ์ผ ์ฌ์์ฑ
4. iOS 13 ์ด์์์๋ง ์๋ํจ์ ํ์ธ
๋ฌธ์ 5: ํ ํฐ ๊ฐฑ์ ๋ฌดํ ๋ฃจํ
์ฆ์:
ํ ํฐ ๊ฐฑ์ ์ด ๊ณ์ ์คํจํ๊ณ ๋ฌดํ ๋ฃจํ์ ๋น ์ง
์์ธ:
โข ๋ฆฌํ๋ ์ ํ ํฐ๋ ๋ง๋ฃ๋จ
โข ํ ํฐ ๊ฐฑ์ ๋ก์ง์ ๋ฒ๊ทธ
โข ๋์์ ์ฌ๋ฌ ์์ฒญ์ด ํ ํฐ ๊ฐฑ์ ์๋
ํด๊ฒฐ ๋ฐฉ๋ฒ:
ํ ํฐ ๊ฐฑ์ ์ ๋๊ธฐํํ๊ณ ์ฌ์๋ ํ์ ์ ํ
// ํ ํฐ ๊ฐฑ์ ๋๊ธฐํ ์์
class TokenManager {
private var refreshJob: Job? = null
private val mutex = Mutex()
private var retryCount = 0
private val maxRetries = 3
suspend fun getValidToken(): String? {
val currentToken = getAccessToken()
if (isTokenValid(currentToken)) {
return currentToken
}
// ์ด๋ฏธ ๊ฐฑ์ ์ค์ด๋ฉด ๋๊ธฐ
mutex.withLock {
// ๋ค์ ํ์ธ (๋ค๋ฅธ ์์ฒญ์ด ์ด๋ฏธ ๊ฐฑ์ ํ์ ์ ์์)
val token = getAccessToken()
if (isTokenValid(token)) {
return token
}
// ์ฌ์๋ ํ์ ์ฒดํฌ
if (retryCount >= maxRetries) {
retryCount = 0
logout()
return null
}
retryCount++
return try {
val newToken = refreshAccessToken()
retryCount = 0 // ์ฑ๊ณต ์ ๋ฆฌ์
newToken
} catch (e: Exception) {
if (retryCount >= maxRetries) {
logout()
}
null
}
}
}
}
๐ ์ฑ๋ฅ ์ต์ ํ ๋ฐ ๋ชจ๋ํฐ๋ง
์์ ๋ก๊ทธ์ธ์ ์ฑ๋ฅ์ ์ต์ ํํ๊ณ ๋ชจ๋ํฐ๋งํ๋ ๊ฒ๋ ์ค์ํด! โก
1. ๋ก๊ทธ์ธ ์๋ ์ต์ ํ
์ฌ์ฉ์๋ ๋น ๋ฅธ ๋ก๊ทธ์ธ์ ์ํด. ๋ค์ ๋ฐฉ๋ฒ๋ค๋ก ์๋๋ฅผ ๊ฐ์ ํ ์ ์์ด:
์ต์ ํ ์ ๋ต:
โข SDK ์ง์ฐ ๋ก๋ฉ: ์ฑ ์์ ์๊ฐ ์๋ ํ์ํ ๋ ์ด๊ธฐํ
โข ํ ํฐ ์บ์ฑ: ์ ํจํ ํ ํฐ์ ๋ฉ๋ชจ๋ฆฌ์ ์บ์
โข ๋ณ๋ ฌ ์ฒ๋ฆฌ: ์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ์ ๋ค๋ฅธ ์์
๋์ ์งํ
โข ๋คํธ์ํฌ ์ต์ ํ: HTTP/2, ์ฐ๊ฒฐ ์ฌ์ฌ์ฉ
โข ํ๋ฆฌํ์นญ: ๋ก๊ทธ์ธ ํ๋ฉด ์ง์
์ SDK ๋ฏธ๋ฆฌ ์ค๋น
// SDK ์ง์ฐ ์ด๊ธฐํ ์์
class SocialLoginManager {
private var googleClient: GoogleSignInClient? = null
private var kakaoInitialized = false
fun initializeGoogle(context: Context) {
if (googleClient == null) {
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(context.getString(R.string.default_web_client_id))
.requestEmail()
.build()
googleClient = GoogleSignIn.getClient(context, gso)
}
}
fun initializeKakao(context: Context) {
if (!kakaoInitialized) {
KakaoSdk.init(context, "YOUR_APP_KEY")
kakaoInitialized = true
}
}
// ๋ก๊ทธ์ธ ํ๋ฉด ์ง์
์ ๋ฏธ๋ฆฌ ์ด๊ธฐํ
fun prepareForLogin(context: Context) {
lifecycleScope.launch {
initializeGoogle(context)
initializeKakao(context)
}
}
}
2. ๋ถ์ ๋ฐ ๋ชจ๋ํฐ๋ง
๋ก๊ทธ์ธ ์ฑ๊ณต๋ฅ , ์คํจ ์์ธ, ์์ ์๊ฐ ๋ฑ์ ์ถ์ ํด์ผ ํด:
// ๋ก๊ทธ์ธ ์ด๋ฒคํธ ์ถ์ ์์
class LoginAnalytics {
fun trackLoginAttempt(platform: String) {
Firebase.analytics.logEvent("login_attempt") {
param("platform", platform)
param("timestamp", System.currentTimeMillis())
}
}
fun trackLoginSuccess(platform: String, duration: Long) {
Firebase.analytics.logEvent("login_success") {
param("platform", platform)
param("duration_ms", duration)
}
}
fun trackLoginFailure(platform: String, errorCode: String, errorMessage: String) {
Firebase.analytics.logEvent("login_failure") {
param("platform", platform)
param("error_code", errorCode)
param("error_message", errorMessage)
}
// Crashlytics์๋ ๊ธฐ๋ก
FirebaseCrashlytics.getInstance().apply {
setCustomKey("login_platform", platform)
setCustomKey("error_code", errorCode)
recordException(Exception("Login failed: $errorMessage"))
}
}
}
3. A/B ํ ์คํ
์ด๋ค ์์ ๋ก๊ทธ์ธ ๋ฒํผ ๋ฐฐ์น๊ฐ ๊ฐ์ฅ ํจ๊ณผ์ ์ธ์ง ํ ์คํธํด๋ณด์:
| ํ ์คํธ ํญ๋ชฉ | ๋ณํ A | ๋ณํ B | ์ธก์ ์งํ |
|---|---|---|---|
| ๋ฒํผ ์์ | ๊ตฌ๊ธ โ ์นด์นด์ค โ ๋ค์ด๋ฒ | ์นด์นด์ค โ ๊ตฌ๊ธ โ ๋ค์ด๋ฒ | ํด๋ฆญ๋ฅ , ์ ํ์จ |
| ๋ฒํผ ํฌ๊ธฐ | ์์ ๋ฒํผ (40dp) | ํฐ ๋ฒํผ (56dp) | ๊ฐ์์ฑ, ํด๋ฆญ๋ฅ |
| ๋ฌธ๊ตฌ | "๋ก๊ทธ์ธ" | "์์ํ๊ธฐ" | ์ ํ์จ |
| ๋ฐฐ์น | ์ธ๋ก ๋์ด | ๊ฐ๋ก ๋์ด | ์ฌ์ฉ์ ์ ํธ๋ |
๐ ๋ฉํฐ ํ๋ซํผ ์ ๋ต
์น, Android, iOS ๋ชจ๋์์ ์ผ๊ด๋ ๊ฒฝํ์ ์ ๊ณตํ๋ ๊ฒ ์ค์ํด! ๐ฑ๐ป
ํฌ๋ก์ค ํ๋ซํผ ํ๋ ์์ํฌ ํ์ฉ
Flutter๋ React Native๋ฅผ ์ฌ์ฉํ๋ฉด ํ ๋ฒ์ ๊ตฌํ์ผ๋ก ์ฌ๋ฌ ํ๋ซํผ์ ์ง์ํ ์ ์์ด:
// Flutter์์ Google ๋ก๊ทธ์ธ ์์
import 'package:google_sign_in/google_sign_in.dart';
class GoogleSignInService {
final GoogleSignIn _googleSignIn = GoogleSignIn(
scopes: ['email', 'profile'],
);
Future<GoogleSignInAccount?> signIn() async {
try {
final account = await _googleSignIn.signIn();
if (account != null) {
final auth = await account.authentication;
final idToken = auth.idToken;
final accessToken = auth.accessToken;
// ์๋ฒ๋ก ํ ํฐ ์ ์ก
await sendTokenToServer(idToken, accessToken);
return account;
}
} catch (error) {
print('Google Sign In Error: $error');
}
return null;
}
Future<void> signOut() async {
await _googleSignIn.signOut();
}
}
// React Native์์ Kakao ๋ก๊ทธ์ธ ์์
import {login, getProfile} from '@react-native-seoul/kakao-login';
const kakaoLogin = async () => {
try {
const token = await login();
console.log('Access Token:', token.accessToken);
const profile = await getProfile();
console.log('Profile:', profile);
// ์๋ฒ๋ก ์ ๋ณด ์ ์ก
await sendProfileToServer(profile);
} catch (error) {
console.error('Kakao Login Error:', error);
}
};
๋ฐฑ์๋ ํตํฉ ์ ๋ต
์๋ฒ์์ ์ฌ๋ฌ ์์ ํ๋ซํผ์ ํตํฉ ๊ด๋ฆฌํ๋ ๊ตฌ์กฐ:
// Node.js + Express ๋ฐฑ์๋ ์์
const express = require('express');
const router = express.Router();
// ํตํฉ ์์
๋ก๊ทธ์ธ ์๋ํฌ์ธํธ
router.post('/auth/social', async (req, res) => {
const {platform, token} = req.body;
try {
let userInfo;
switch(platform) {
case 'google':
userInfo = await verifyGoogleToken(token);
break;
case 'kakao':
userInfo = await getKakaoUserInfo(token);
break;
case 'naver':
userInfo = await getNaverUserInfo(token);
break;
case 'facebook':
userInfo = await getFacebookUserInfo(token);
break;
default:
return res.status(400).json({error: 'Unsupported platform'});
}
// ์ฌ์ฉ์ ์ฐพ๊ธฐ ๋๋ ์์ฑ
let user = await User.findOne({
socialId: userInfo.id,
socialPlatform: platform
});
if (!user) {
user = await User.create({
socialId: userInfo.id,
socialPlatform: platform,
email: userInfo.email,
name: userInfo.name,
profileImage: userInfo.picture
});
}
// JWT ํ ํฐ ๋ฐ๊ธ
const jwtToken = generateJWT(user);
res.json({
success: true,
token: jwtToken,
user: {
id: user._id,
email: user.email,
name: user.name
}
});
} catch (error) {
console.error('Social login error:', error);
res.status(500).json({error: 'Authentication failed'});
}
});
module.exports = router;
๐ ์ค์ ํ๋ก์ ํธ: ์์ ํ ์์ ๋ก๊ทธ์ธ ์์คํ ๊ตฌ์ถ
์, ์ด์ ๋ฐฐ์ด ๋ด์ฉ์ ๋ชจ๋ ํ์ฉํด์ ์์ ํ ์์ ๋ก๊ทธ์ธ ์์คํ ์ ๋ง๋ค์ด๋ณด์! ๐
ํ๋ก์ ํธ ๊ตฌ์กฐ
MyApp/
โโโ app/
โ โโโ src/
โ โ โโโ main/
โ โ โ โโโ java/com/myapp/
โ โ โ โ โโโ auth/
โ โ โ โ โ โโโ SocialLoginManager.kt
โ โ โ โ โ โโโ GoogleLoginHandler.kt
โ โ โ โ โ โโโ KakaoLoginHandler.kt
โ โ โ โ โ โโโ NaverLoginHandler.kt
โ โ โ โ โ โโโ TokenManager.kt
โ โ โ โ โโโ ui/
โ โ โ โ โ โโโ LoginActivity.kt
โ โ โ โ โ โโโ MainActivity.kt
โ โ โ โ โโโ network/
โ โ โ โ โ โโโ ApiService.kt
โ โ โ โ โ โโโ AuthInterceptor.kt
โ โ โ โ โโโ data/
โ โ โ โ โโโ User.kt
โ โ โ โ โโโ UserRepository.kt
โ โ โ โโโ res/
โ โ โ โโโ layout/
โ โ โ โ โโโ activity_login.xml
โ โ โ โ โโโ activity_main.xml
โ โ โ โโโ values/
โ โ โ โโโ strings.xml
โ โโโ build.gradle
โโโ backend/
โโโ src/
โ โโโ routes/
โ โ โโโ auth.js
โ โโโ models/
โ โ โโโ User.js
โ โโโ middleware/
โ โ โโโ authMiddleware.js
โ โโโ utils/
โ โโโ tokenVerifier.js
โโโ package.json
ํต์ฌ ์ฝ๋ ๊ตฌํ
1. SocialLoginManager (ํตํฉ ๊ด๋ฆฌ์)
class SocialLoginManager(private val context: Context) {
private val googleHandler = GoogleLoginHandler(context)
private val kakaoHandler = KakaoLoginHandler(context)
private val naverHandler = NaverLoginHandler(context)
private val tokenManager = TokenManager(context)
suspend fun login(platform: SocialPlatform): Result<User> {
return withContext(Dispatchers.IO) {
try {
val socialToken = when(platform) {
SocialPlatform.GOOGLE -> googleHandler.login()
SocialPlatform.KAKAO -> kakaoHandler.login()
SocialPlatform.NAVER -> naverHandler.login()
}
// ์๋ฒ์ ํ ํฐ ์ ์กํ๊ณ JWT ๋ฐ๊ธฐ
val response = ApiService.authenticateSocial(
platform = platform.name.lowercase(),
token = socialToken
)
// ํ ํฐ ์ ์ฅ
tokenManager.saveTokens(
accessToken = response.accessToken,
refreshToken = response.refreshToken
)
Result.success(response.user)
} catch (e: Exception) {
Log.e(TAG, "Login failed", e)
Result.failure(e)
}
}
}
suspend fun logout() {
tokenManager.clearTokens()
googleHandler.logout()
kakaoHandler.logout()
naverHandler.logout()
}
fun isLoggedIn(): Boolean {
return tokenManager.hasValidToken()
}
}
enum class SocialPlatform {
GOOGLE, KAKAO, NAVER
}
2. LoginActivity (UI)
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
private lateinit var socialLoginManager: SocialLoginManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
socialLoginManager = SocialLoginManager(this)
setupButtons()
}
private fun setupButtons() {
binding.btnGoogleLogin.setOnClickListener {
performLogin(SocialPlatform.GOOGLE)
}
binding.btnKakaoLogin.setOnClickListener {
performLogin(SocialPlatform.KAKAO)
}
binding.btnNaverLogin.setOnClickListener {
performLogin(SocialPlatform.NAVER)
}
}
private fun performLogin(platform: SocialPlatform) {
binding.progressBar.visibility = View.VISIBLE
setButtonsEnabled(false)
lifecycleScope.launch {
val result = socialLoginManager.login(platform)
binding.progressBar.visibility = View.GONE
setButtonsEnabled(true)
result.onSuccess { user ->
Toast.makeText(
this@LoginActivity,
"ํ์ํฉ๋๋ค, ${user.name}๋!",
Toast.LENGTH_SHORT
).show()
navigateToMain()
}.onFailure { error ->
showErrorDialog(error.message ?: "๋ก๊ทธ์ธ์ ์คํจํ์ต๋๋ค.")
}
}
}
private fun setButtonsEnabled(enabled: Boolean) {
binding.btnGoogleLogin.isEnabled = enabled
binding.btnKakaoLogin.isEnabled = enabled
binding.btnNaverLogin.isEnabled = enabled
}
private fun navigateToMain() {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
private fun showErrorDialog(message: String) {
AlertDialog.Builder(this)
.setTitle("๋ก๊ทธ์ธ ์คํจ")
.setMessage(message)
.setPositiveButton("ํ์ธ", null)
.show()
}
}
3. ์๋ฒ ์ธก ์ธ์ฆ API
// routes/auth.js
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const {verifyGoogleToken, getKakaoUserInfo, getNaverUserInfo} = require('../utils/tokenVerifier');
router.post('/social', async (req, res) => {
try {
const {platform, token} = req.body;
// ํ๋ซํผ๋ณ ์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
let userInfo;
switch(platform) {
case 'google':
userInfo = await verifyGoogleToken(token);
break;
case 'kakao':
userInfo = await getKakaoUserInfo(token);
break;
case 'naver':
userInfo = await getNaverUserInfo(token);
break;
default:
return res.status(400).json({error: 'Invalid platform'});
}
// ์ฌ์ฉ์ ์ฐพ๊ธฐ ๋๋ ์์ฑ
let user = await User.findOne({
socialId: userInfo.id,
socialPlatform: platform
});
if (!user) {
user = new User({
socialId: userInfo.id,
socialPlatform: platform,
email: userInfo.email,
name: userInfo.name,
profileImage: userInfo.picture,
createdAt: new Date()
});
await user.save();
} else {
// ๊ธฐ์กด ์ฌ์ฉ์ ์ ๋ณด ์
๋ฐ์ดํธ
user.lastLoginAt = new Date();
user.name = userInfo.name;
user.profileImage = userInfo.picture;
await user.save();
}
// JWT ํ ํฐ ์์ฑ
const accessToken = jwt.sign(
{userId: user._id, email: user.email},
process.env.JWT_SECRET,
{expiresIn: '1h'}
);
const refreshToken = jwt.sign(
{userId: user._id},
process.env.JWT_REFRESH_SECRET,
{expiresIn: '7d'}
);
// ๋ฆฌํ๋ ์ ํ ํฐ ์ ์ฅ
user.refreshToken = refreshToken;
await user.save();
res.json({
success: true,
accessToken,
refreshToken,
user: {
id: user._id,
email: user.email,
name: user.name,
profileImage: user.profileImage
}
});
} catch (error) {
console.error('Social auth error:', error);
res.status(500).json({
error: 'Authentication failed',
message: error.message
});
}
});
// ํ ํฐ ๊ฐฑ์ ์๋ํฌ์ธํธ
router.post('/refresh', async (req, res) => {
try {
const {refreshToken} = req.body;
if (!refreshToken) {
return res.status(401).json({error: 'Refresh token required'});
}
// ๋ฆฌํ๋ ์ ํ ํฐ ๊ฒ์ฆ
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const user = await User.findById(decoded.userId);
if (!user || user.refreshToken !== refreshToken) {
return res.status(401).json({error: 'Invalid refresh token'});
}
// ์ ์ก์ธ์ค ํ ํฐ ๋ฐ๊ธ
const newAccessToken = jwt.sign(
{userId: user._id, email: user.email},
process.env.JWT_SECRET,
{expiresIn: '1h'}
);
res.json({
success: true,
accessToken: newAccessToken
});
} catch (error) {
res.status(401).json({error: 'Token refresh failed'});
}
});
module.exports = router;
๐ก ์ฌ๋ฅ๋ท์์ ๋์๋ฐ๊ธฐ! ์ด๋ฐ ๋ณต์กํ ์์คํ ์ ํผ์ ๊ตฌ์ถํ๊ธฐ ์ด๋ ต๋ค๋ฉด, ์ฌ๋ฅ๋ท(https://www.jaenung.net)์์ ๊ฒฝํ ๋ง์ ๊ฐ๋ฐ์๋ค์๊ฒ ๋์์ ๋ฐ์ ์ ์์ด. OAuth ์ ๋ฌธ๊ฐ๋ค์ด ๋น์ ์ ํ๋ก์ ํธ์ ๋ง๋ ์ต์ ์ ์๋ฃจ์ ์ ์ ๊ณตํด์ค ๊ฑฐ์ผ!
๐ฎ ๋ฏธ๋ ํธ๋ ๋์ ๋ฐ์ ๋ฐฉํฅ
์์ ๋ก๊ทธ์ธ๊ณผ OAuth ๊ธฐ์ ์ ๊ณ์ ์งํํ๊ณ ์์ด. ์์ผ๋ก์ ํธ๋ ๋๋ฅผ ์ดํด๋ณด์! ๐
1. Passwordless Authentication (๋น๋ฐ๋ฒํธ ์๋ ์ธ์ฆ)
๋น๋ฐ๋ฒํธ ์์ฒด๋ฅผ ์์ ๋ ๋ฐฉํฅ์ผ๋ก ๋ฐ์ ํ๊ณ ์์ด:
โข WebAuthn: ์์ฒด ์ธ์ฆ, ๋ณด์ ํค ์ฌ์ฉ
โข Magic Link: ์ด๋ฉ์ผ๋ก ์ผํ์ฉ ๋ก๊ทธ์ธ ๋งํฌ ์ ์ก
โข OTP: ์ผํ์ฉ ๋น๋ฐ๋ฒํธ
โข Passkeys: ์ ํ, ๊ตฌ๊ธ, MS๊ฐ ๊ณต๋ ๊ฐ๋ฐ ์ค์ธ ์ฐจ์ธ๋ ์ธ์ฆ
2. Decentralized Identity (ํ์ค์ํ ์ ์)
๋ธ๋ก์ฒด์ธ ๊ธฐ๋ฐ์ ์๊ธฐ์ฃผ๊ถ ์ ์(Self-Sovereign Identity):
โข ์ฌ์ฉ์๊ฐ ์์ ์ ๋ฐ์ดํฐ๋ฅผ ์์ ํ ํต์
โข ์ค์ ์๋ฒ ์์ด ์ ์ ์ฆ๋ช
โข ๊ฐ์ธ์ ๋ณด ๋ณดํธ ๊ฐํ
โข W3C DID(Decentralized Identifiers) ํ์ค
3. Zero-Knowledge Proof (์์ง์ ์ฆ๋ช )
์ ๋ณด๋ฅผ ๊ณต๊ฐํ์ง ์๊ณ ๋ ์ฆ๋ช
ํ ์ ์๋ ๊ธฐ์ :
โข ๋์ด ํ์ธ ์ ์๋
์์ผ ๊ณต๊ฐ ์์ด "์ฑ์ธ์"๋ง ์ฆ๋ช
โข ๊ฐ์ธ์ ๋ณด ์ต์ํ
โข ํ๋ผ์ด๋ฒ์ ๋ณดํธ ๊ทน๋ํ
4. AI ๊ธฐ๋ฐ ๋ณด์
์ธ๊ณต์ง๋ฅ์ ํ์ฉํ ์ด์ ํ์ง:
โข ๋น์ ์์ ์ธ ๋ก๊ทธ์ธ ํจํด ๊ฐ์ง
โข ๋ด ๊ณต๊ฒฉ ์๋ ์ฐจ๋จ
โข ์ฌ์ฉ์ ํ๋ ๋ถ์์ ํตํ ๋ฆฌ์คํฌ ํ๊ฐ
โข ์ ์ํ ์ธ์ฆ (์ํ๋์ ๋ฐ๋ผ ์ถ๊ฐ ์ธ์ฆ ์๊ตฌ)
๐ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ฝ์๋ค๋ฉด ์ ๋ง ๋๋จํด! ๐
์ฐ๋ฆฌ๋ ์์
๋ก๊ทธ์ธ๊ณผ OAuth์ ๋ํด ์ ๋ง ๋ง์ ๊ฒ์ ๋ฐฐ์ ์ด:
โข OAuth 2.0์ ๊ธฐ๋ณธ ๊ฐ๋
๊ณผ ์๋ ์๋ฆฌ
โข ์ฃผ์ ํ๋ซํผ๋ณ ๊ตฌํ ๋ฐฉ๋ฒ (๊ตฌ๊ธ, ์นด์นด์ค, ๋ค์ด๋ฒ, ํ์ด์ค๋ถ)
โข ๋ณด์ Best Practices (PKCE, State, ํ ํฐ ๊ด๋ฆฌ)
โข ์ฌ์ฉ์ ๊ฒฝํ ์ต์ ํ ์ ๋ต
โข ์ค์ ํ๋ก์ ํธ ๊ตฌ์ถ ๋ฐฉ๋ฒ
โข ๋ฏธ๋ ํธ๋ ๋์ ๋ฐ์ ๋ฐฉํฅ
์์
๋ก๊ทธ์ธ์ ์ด์ ์ ํ์ด ์๋ ํ์์ผ. ์ฌ์ฉ์๋ค์ ๋น ๋ฅด๊ณ ํธ๋ฆฌํ ๋ก๊ทธ์ธ์ ์ํ๊ณ , ๊ฐ๋ฐ์๋ค์ ์์ ํ๊ณ ํจ์จ์ ์ธ ์ธ์ฆ ์์คํ
์ ์ํ์ง. OAuth๋ ์ด ๋ ๊ฐ์ง๋ฅผ ๋ชจ๋ ๋ง์กฑ์์ผ์ฃผ๋ ํ๋ฅญํ ์๋ฃจ์
์ด์ผ! ๐ฏ
์ฒ์์๋ ๋ณต์กํด ๋ณด์ผ ์ ์์ง๋ง, ํ ๋ฒ ์ ๋๋ก ๊ตฌํํด๋๋ฉด ์ ๋ง ๊ฐ๋ ฅํ ๋ฌด๊ธฐ๊ฐ ๋ผ. ๊ทธ๋ฆฌ๊ณ ๊ณ์ ๋ฐ์ ํ๋ ๊ธฐ์ ์ด๋๊น ์ต์ ํธ๋ ๋๋ ๋์น์ง ๋ง๊ณ ๋ฐ๋ผ๊ฐ์!
๊ธฐ์ตํด์ผ ํ ํต์ฌ ํฌ์ธํธ:
1. ๋ณด์์ด ์ต์ฐ์ - PKCE, State, HTTPS๋ ํ์!
2. ์ฌ์ฉ์ ๊ฒฝํ์ ์๊ฐํ์ - ๋น ๋ฅด๊ณ ์ง๊ด์ ์ธ UI
3. ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ๊ผผ๊ผผํ - ๋ช
ํํ ํผ๋๋ฐฑ ์ ๊ณต
4. ํ ํฐ ๊ด๋ฆฌ๋ฅผ ์ฒ ์ ํ - ์์ ํ ์ ์ฅ๊ณผ ๊ฐฑ์
5. ๋ชจ๋ํฐ๋ง๊ณผ ๋ถ์ - ์ง์์ ์ธ ๊ฐ์
์ด์ ๋น์ ๋ ์์
๋ก๊ทธ์ธ ์ ๋ฌธ๊ฐ์ผ! ๋ฐฐ์ด ๋ด์ฉ์ ๋ฐํ์ผ๋ก ๋ฉ์ง ์ฑ์ ๋ง๋ค์ด๋ณด์. ๐
ํน์ ๊ตฌํ ์ค์ ๋งํ๋ ๋ถ๋ถ์ด ์๋ค๋ฉด, ๊ฐ ํ๋ซํผ์ ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ๊ฑฐ๋, ๊ฐ๋ฐ์ ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ. ๊ทธ๋ฆฌ๊ณ ์ฌ๋ฅ๋ท์์ ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ๋ ๊ฒ๋ ์์ง ๋ง!
Happy Coding! ๐ปโจ
๐
์์ ๋ก๊ทธ์ธ์ผ๋ก ๋ ๋์ ์ฌ์ฉ์ ๊ฒฝํ์ ๋ง๋ค์ด๊ฐ์ธ์!
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.