[Python] Backend Authentication - Bcrypt & PyJWT

SungjoonAnยท2022๋…„ 2์›” 22์ผ
0

Django

๋ชฉ๋ก ๋ณด๊ธฐ
7/12
post-thumbnail

๐Ÿ”’Backend Authentication

์ธ์ฆ(Authentication)์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ž์‹ ์ด ์ฃผ์žฅํ•œ ์‚ฌ๋žŒ์ด ๋งž๋Š”์ง€ ํ™•์ธํ•˜๋Š” ์ ˆ์ฐจ๋ฅผ ๊ฑฐ์นฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์‚ฌ์šฉํ•œ ๊ธฐ๋Šฅ์ด ๋กœ๊ทธ์ธ, ๋กœ๊ทธ์•„์›ƒ, ๊ทธ๋ฆฌ๊ณ  ํšŒ์›๊ฐ€์ž…์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด Web์—์„œ๋Š” ์‚ฌ์šฉ์ž์˜ ID์™€ Password๋ฅผ ๋ฐ›์•„์„œ ๋“ฑ๋กํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ๋งž๋Š”์ง€ ๊ฒ€์ฆ์„ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์‹คํ˜„ํ•  ๋•Œ ์œ ์ €์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์ ˆ๋Œ€๋กœ DB์— ๊ทธ๋Œ€๋กœ ์ €์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ ์ด์œ ๋Š” ๋งŒ์ผ์— DB๊ฐ€ ๋‹ค๋ฅธ ํ—ˆ๊ฐ€๋ฐ›์ง€ ์•Š๋Š” ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ๋…ธ์ถœ์ด ๋  ๊ฒฝ์šฐ ๊ฐœ์ธ์ •๋ณด๊ฐ€ ๋ณดํ˜ธ ๋ฐ›์„ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์ผ๋ฐ˜์ ์œผ๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•”ํ˜ธํ™”๋ฅผ ํ•  ๋•Œ๋Š” ๋‹จ๋ฐ˜ํ–ฅ ํ•ด์‰ฌ ํ•จ์ˆ˜(One-way hash function)์ด ์“ฐ์ž…๋‹ˆ๋‹ค. ๋‹จ๋ฐฉํ–ฅ์ด๋ผ๊ณ  ๋ถˆ๋ฆฌ๋Š” ์ด์œ ๋กœ๋Š” ์›๋ณธ ๋ฉ”์„ธ์ง€๋ฅผ ๋ณ€ํ™˜ํ•˜์—ฌ ์ƒ์„ฑ๋œ ์•”ํ˜ธํ™”๋œ ๋ฉ”์„ธ์ง€(digest)๋งŒ์œผ๋กœ๋Š” ๋‹ค์‹œ ์›๋ณธ์œผ๋กœ ๊ฐˆ ์ˆ˜๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

bcrypt

์‚ฌ์šฉ์ž๊ฐ€ ๋งž๋Š”์ง€ ์ธ์ฆ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์ด์ „์— Web์—์„œ๋Š” ์šฐ์„ ์ ์œผ๋กœ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ๋ณดํ˜ธํ•  ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. HTTP๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๊ฐ€ ํ†ต์‹ ๋˜๋Š” ์™€์ค‘์— ๋‹ค๋ฅธ ์‚ฌ๋žŒ์ด data๋ฅผ ๊ฐ€์ ธ๊ฐ€ ์ •๋ณด๋ฅผ ๋ณผ ์ˆ˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฅผ ๋ณดํ˜ธํ•˜๊ธฐ ์œ„ํ•ด์„œ ์šฐ๋ฆฌ๋Š” ํ•„์ˆ˜์ ์œผ๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•”ํ˜ธํ™”ํ•ด์„œ ์ค‘๊ฐ„์— ์ •๋ณด๊ฐ€ ์ƒˆ์–ด๋‚˜๊ฐ€๋”๋ผ๋„ ์•Œ์•„๋ณผ์ˆ˜ ์—†๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. Salting๊ณผ Key Stretching์„ ๊ตฌํ˜„ํ•œ ํ•ด์‰ฌ ํ•จ์ˆ˜์ค‘ ๊ฐ€์žฅ ๋„๋ฆฌ ์‚ฌ์šฉ๋˜๋Š” ๊ฒƒ์ด bcrypt์ž…๋‹ˆ๋‹ค. bcrypt๋Š” ์ฒ˜์Œ๋ถ€ํ„ฐ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋‹จ๋ฐฉํ–ฅ ์•”ํ˜ธํ™” ํ•˜๊ธฐ ์œ„ํ•ด ๋งŒ๋“ค์ด์ „ ํ•ด์‰ฌํ•จ์ˆ˜ ์ด๋‹ค. ๊ทธ ์ฒซ๋ฒˆ์งธ ๊ณผ์ •์œผ๋กœ bcrypt๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.
pip install bcrypt

๊ทธ ๋‹ค์Œ์œผ๋กœ๋Š” python interpreter๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ importํ•ฉ๋‹ˆ๋‹ค.
>>> import bcrypt

๋‹จ๋ฐฉํ–ฅ ํ•ด์‰ฌ ํ•จ์ˆ˜์˜ ์ทจ์•ฝ์ ๋“ค์„ ๋ณด์•ˆํ•˜๊ธฐ ์œ„ํ•ด ์ผ๋ฐ˜์ ์œผ๋กœ 2๊ฐ€์ง€ ๋ณด์•ˆ์ ๋“ค์ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

  • Salting
    ์‹ค์ œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ด์™ธ์— ์ถ”๊ฐ€์ ์œผ๋กœ ๋žœ๋ค ๋ฐ์ดํ„ฐ๋ฅผ ๋”ํ•ด์„œ ํ•ด์‹œ๊ฐ’์„ ๊ณ„์‚ฐํ•˜๋Š” ๋ฐฉ๋ฒ•.
  • Key Stretching
    ๋‹จ๋ฐ˜ํ–ฅ ํ•ด์‰ฌ๊ฐ’์„ ๊ณ„์‚ฐ ํ•œ ํ›„ ํ•ด์‰ฌ๊ฐ’์„ ๋˜ ๋‹ค์‹œ ํ•ด์‰ฌํ•˜๊ณ  ๊ทธ ๋‹ค์Œ ํ•ด์‰ฌํ•˜๋Š” ๊ณ„์†๋œ ํ•ด์‰ฌ ๋ฐ˜๋ณต

bcrypt์˜ ์•”ํ˜ธํ™” ๋ฐฉ๋ฒ•

bcrypt๋Š” strํ˜•์‹์˜ data๊ฐ€ ์•„๋‹Œ bytesํ˜•์‹์˜ data๋ฅผ ์•”ํ˜ธํ™”ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์•”ํ˜ธํ™”์‹œ์—๋Š” bytesํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜์„ ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

  • Python์—์„œ๋Š” str์„ encodeํ•˜๋ฉด bytes(์ด์ง„ํ™”)๋˜๊ณ , Bytes๋ฅผ decodeํ•˜๋ฉด strํ˜•์‹์œผ๋กœ ๋‹ค์‹œ ๋Œ์•„๊ฐ‘๋‹ˆ๋‹ค.
  • Encode์™€ decode์‹œ์—๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ธ์‹ํ•  ์ˆ˜ ์žˆ๋Š” ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด UTF-8 ์œ ๋‹ˆ์ฝ”๋“œ ๋ฌธ์ž ๊ทœ๊ฒฉ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
#๋น„๋ฐ€๋ฒˆํ˜ธ 1234 ์•”ํ˜ธํ™” ์˜ˆ์‹œ
password = '1234'

hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
print(hashed_password)

b'$2b$12$YFs9rh.1LgJwZuf9ibyjpuLvBoCaGX0MzedFWF2Jo0zU3lMZurZ4a'
#ํ•ด์‹œ๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ์˜ ๊ฐ’

์œ„์˜ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์•”ํ˜ธํ™”๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•”ํ˜ธํ™”๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธํ•˜๊ธฐ

์ด์ œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•”ํ˜ธํ™”๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋น„๋ก ์•”ํ˜ธํ™”๊ฐ€ ๋˜์—ˆ์ง€๋งŒ ์‚ฌ์šฉ์ž๊ฐ€ ์ด ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋งž๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค. bcrypt.checkpw() method๋ฅผ ์ด์šฉ์„ ํ•˜๋ฉด ์‚ฌ์šฉ์ž๋Š” ์•”ํ˜ธ๊ฐ€ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ์ผ์น˜๋ฅผ ํ•˜๊ฒŒ ๋˜๋ฉด True๊ฐ’์„ ๋ฐ›๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

>>> new_password = '1234'
>>> bcrypt.checkpw(new_password.encode('utf-8'),hashed_password)
>>> True

์œ„์— ์žˆ๋Š” new_password.encode('utf-8)์€ ์‚ฌ์šฉ์ž๋กœ๋ถ€ํ„ฐ ์ž…๋ ฅ๋ฐ›์€ ํŒจ์Šค์›Œ๋“œ์ด๊ณ  hashed_passoword๋Š” ์ €์žฅ๋œ ์•”ํ˜ธํ™”๋œ ํŒจ์Šค์›Œ๋“œ์ž…๋‹ˆ๋‹ค. ๋‘ ํŒจ์Šค์›Œ๋“œ ๋‹ค data type์€ bytes์ž…๋‹ˆ๋‹ค.

๐Ÿ‘จโ€๐Ÿ’ปBcrypt๋„ ๋ช‡๊ฐ€์ง€์˜ ์ทจ์•ฝ์ ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

  • Rainbow table attack: ๋ฏธ๋ฆฌ ํ•ด์‰ฌ๊ฐ’๋“ค์„ ๊ณ„์‚ฐํ•ด ๋†“์€ ํ…Œ์ด๋ธ”์„ ์ด์šฉํ•˜๊ฒŒ ๋œ๋‹ค๋ฉด ์—ญ์œผ๋กœ ์•”ํ˜ธํ™”๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์œ ์ถ”ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

JWT - JSON Web Tokens

์œ ์ €๊ฐ€ ๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ•œ ํ›„์—๋Š” access token ์ด๋ผ๊ณ  ํ•˜๋Š” ์•”ํ˜ธํ™”๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์ฒจ๋ถ€ํ•ด์„œ request๋ฅผ ๋ณด๋‚ด๊ฒŒ ๋œ๋‹ค.

  • ์œ ์ € ๋กœ๊ทธ์ธ ์˜ˆ์‹œ:
POST /auth HTTP/1.1 
Host: localhost:5000
Content-Type: application/json

{
    "username": "joe",
    "password": "pass"
}
  • Access token ์˜ˆ์‹œ:
HTTP/1.1 200 OK
Content-Type: application/json

{
    "access_token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6MSwiaWF0IjoxNDQ0OTE3NjQwLCJuYmYiOjE0NDQ5MTc2NDAsImV4cCI6MTQ0NDkxNzk0MH0.KPmI6WSjRjlpzecPvs3q_T3cJQvAgJvaQAPtk1abC_E"
}

access token์„ ๋ฐ›์€ ์„œ๋ฒ„๋Š” ํ† ํฐ์„ ๋ณตํ˜ธํ™” ํ•ด์„œ ํ•ด๋‹น ์œ ์ €์˜ ์ •๋ณด๋ฅผ ์–ป๊ฒŒ๋ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฐ์‹์œผ๋กœ ํ† ํฐ์„ ๋ฐ›๋Š” ์ด์œ ๋Š” ํ•ด๋‹น ์œ ์ €๊ฐ€ ๋งค๋ฒˆ ๋กœ๊ทธ์ธ์„ ํ•˜์ง€ ์•Š๋„๋ก ํ•˜๋Š”๊ฒƒ์ด๋‹ค.

์ด๋Ÿฐ access token์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์—ฌ๋Ÿฌ๊ฐ€์ง€๊ฐ€ ์กด์žฌํ•˜๋Š”๋ฐ ๊ฐ€์žฅ ๋„๋ฆฌ ์•Œ๋ ค์ง„ ๊ธฐ์ˆ ์ด JWT(JSON Web Tokens) ์ด๋‹ค. JWT๋Š” ์œ ์ €์ •๋ณด๋ฅผ ๋‹ด์€ JSON ๋ฐ์ดํ„ฐ๋ฅผ ์•”ํ˜ธํ™”ํ•ด์„œ Client์™€ Server๊ฐ„์— ์ฃผ๊ณ  ๋ฐ›๋Š” ๊ฒƒ์„ ๋œปํ•ฉ๋‹ˆ๋‹ค.

  • JWT ๊ตฌ์กฐ:

    ์œ„์˜ ๊ตฌ์กฐ๋ฅผ ๋ณด๋ฉด Header, Payload, ๊ทธ๋ฆฌ๊ณ  Signature ์„ธ ๋ถ€๋ถ„์œผ๋กœ ๊ตฌ์„ฑ์ด ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

    • Header : ํ† ํฐ ํƒ€์ž… ๋ฐ ํ•ด์‹œ ์‚ฌ์šฉ๋˜๋Š” ํ•ด์‹œ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ €์žฅ
    • Payload : JWT๋ฅผ ํ†ตํ•ด ์‹ค์ œ ์„œ๋ฒ„๊ฐ„์— ์ „์†ก๋˜๋Š” ๋ฐ์ดํ„ฐ (HTTP body ๋ถ€๋ถ„๊ณผ ์œ ์‚ฌ)
    • Signature : ๋ฐœํ–‰๋œ JWT๊ฐ€ ๋งž๋Š”์ง€ ๊ฒ€์ฆํ•˜๋Š” ๋ถ€๋ถ„, ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๋ถ€๋ถ„์ด ์‹ค์ œ๋กœ ์•”ํ˜ธํ™”๋˜๋Š” ๋ถ€๋ถ„์ด๋‹ค.

      ๐Ÿ’ก Signature ๋ถ€๋ถ„ ์ด์™ธ์— ๋‚˜๋จธ์ง€ ๋ถ€๋ถ„์€ ์•”ํ˜ธํ™”๊ฐ€ ์•„๋‹Œ ์ธ์ฝ”๋”ฉ ํ•˜์—ฌ ์ „์†กํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ๋ˆ„๊ตฌ๋‚˜ ์›๋ณธ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์„ ์ˆ˜ ์žˆ๊ธฐ ๋–„๋ฌธ์— ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ๋Š” ์ €์žฅํ•˜์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•˜์—ฌ์•ผ ํ•œ๋‹ค,

JWT ๊ตฌํ˜„ํ•˜๊ธฐ

PyJWT ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜ํ•˜๊ธฐ:

pip install pyjwt

bcrypt์™€ ๋™์ผํ•˜๊ฒŒ python interpreter์‹คํ–‰ํ•˜๊ณ . ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋™์ž‘์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import jwt #ํŒจํ‚ค์ง€๋ช…์€ pyjwt์ด์ง€๋งŒ ์ž„ํฌํŠธํ• ๋•Œ์˜ ์ด๋ฆ„์€ jwt์ž…๋‹ˆ๋‹ค.

SECRET = 'secret' #'๋žœ๋คํ•œ ์กฐํ•ฉ์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ' ์˜ˆ์ œ์ด๋ฏ€๋กœ ๋‹จ์ˆœํ•˜๊ฒŒ 'secret'์ด๋ผ๊ณ  ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

access_token = jwt.encode({'id' : 1}, SECRET, algorithm = 'HS256')
print(access_token)
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MX0.-xXA0iKB4mVNvWLYFtt2xNiYkFpObF54J9lj2RwduAI'

๐Ÿ’ก JWT์˜ ๊ฒฐ๊ณผ๋ฌผ์€ pyjwt์˜ ๋ฒ„์ „์— ๋”ฐ๋ผ bytes(ver. 1.7)ํƒ€์ž… ๋˜๋Š” str(ver. 2.0 ์ด์ƒ)ํƒ€์ž… ์ž…๋‹ˆ๋‹ค.

์ด์ œ ๋งŒ๋“ค์–ด์ง„ ์ธ์ฆ์„ ์œ„ํ•œ ๋งค๊ฐœ์ฒด๋ฅผ JWT Token, ์ฆ‰access_toekn์„ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฐœ๊ธ‰๋œ token์„ ๋กœ๊ทธ์ธ์ด ์„ฑ๊ณตํ•˜๊ฒŒ ๋˜๋ฉด Frontend engineer์—๊ฒŒ ์ „๋‹ฌ์„ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ธ์ฆ์„ ํ†ต๊ณผํ•œ ์‚ฌ์šฉ์ž๋งŒ ์ ‘๊ทผ์„ ํ•˜๊ฒŒ ํ•˜๋ ค๋ฉด token์„ ๋ฐ›์•„์„œ ๋ฐœํ–‰๋œ token์ด ๋งž๋Š”์ง€ ํ™•์ธํ•˜๋Š” ์ ˆ์ฐจ๋ฅผ ๊ฑธ์นฉ๋‹ˆ๋‹ค.

header = jwt.decode(access_token, SECRET, algorithm = 'HS256')
print(header)
{'id': 1}

# decode ๊ฒฐ๊ณผ๋Š” ์šฐ๋ฆฌ๊ฐ€ encode ํ•  ๋•Œ ๋„˜๊ฒผ๋˜ header๊ฐ’์ธ {'id' : 1}์ž…๋‹ˆ๋‹ค.

์ด header ๊ฐ’์„ ํ†ตํ•ด ์œ ์ €๋ฅผ ์‹๋ณ„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ธ์ฆํ•˜๋Š” ์ฝ”๋“œ๋Š” endpoint์— ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•˜๊ณ  ๊ตฌํ˜„์€ ๋ณดํ†ต user app์— utils.py๋ฅผ ๋งŒ๋“ค์–ด์„œ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€