Melon-Shake / API

2 stars 1 forks source link

사용자 계정 비밀번호 암호화 #8

Closed mews2000 closed 1 year ago

mews2000 commented 1 year ago

사용자 계정 비밀번호 암호화

python의 bcrypt 해시함수를 이용한 사용자 비밀번호의 암호화 및 인증 절차에 대한 설명입니다.

해시함수와 사용 목적?

해시함수를 사용했으므로 정확히는 비밀번호를 암호화한 것이 아닌 해싱을 한 것인데 해시함수가 하는 일은 임의의 길이를 갖는 비트열을 입력으로 받아서 정해진 길이의 비트열을 반환하는 것입니다. 이 반환된 값을 해시값이라고 하며 해시값을 원본 비밀번호로 복호화하는 것은 불가능합니다. 그 이유는 어떤 특정 해시값으로 해싱되는 원본 값이 한 가지가 아니기 때문입니다. 즉 비밀번호를 해싱한 후 테이블에 원본 비밀번호 대신 해시값을 저장하면 계정 정보가 담긴 테이블이 유출되어도 원본 비밀번호를 알아내기가 어렵습니다.

사용자 계정 테이블에는 아래와 같이 userid와 password를 입력받습니다. 로그인 시에 필요한 정보만을 담을 것이므로 사용자의 상세 정보는 다른 관계 테이블에 담고 이 테이블에는 최소한의 인증 정보만을 담습니다. 그 이유는 테이블이 유출되었을때 정보를 지키기 위해 비밀번호를 해싱하여 넣으면서 다른 중요한 개인정보를 같은 테이블에 입력하게 될 경우 비밀번호가 유출되지 않더라도 다른 개인정보가 유출되므로 해시함수의 사용 의미가 희석되기 때문입니다.

연관 코드

테이블 구조는 아래와 같다고 가정합니다.

melon_shake=# select * from "user";
 id | userid | password
----+--------+----------

회원 가입 시에 userid와 password를 입력받으면 그중 password는 해싱을 거친 후에 사용자 계정 테이블에 입력하게 됩니다. 파이썬 상의 코드로는 이렇습니다. 현재 데모를 streamlit을 통해 제작중이므로 streamlit을 사용한다고 가정합니다.

import streamlit as st
import requests
from mysql import connector
import bcrypt

con = connector.connect(
    host="호스트 주소",
    port="포트번호",
    user="DB 계정 이름",
    password="DB 계정 비밀번호",
    database="DB 이름"
)

id = st.text_input("ID를 입력하세요")
password = st.text_input("password를 입력하세요")
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
if st.button("입력"):
    cursor = con.cursor()
    query = "INSERT INTO a (userid,password) values (%s,%s)"
    values = (id,hashed_password)
    try:
        cursor.execute(query,values)
        con.commit()
        cursor.close()
        con.close()
        st.write("등록이 완료되었습니다.")
    except mysql.connector.IntegrityError as e:
        st.write("이미 등록된 ID입니다.")

이렇게 우선 사용자 비밀번호를 utf-8로 인코딩한 후 bcrypt의 hashpw메서드를 이용하여 해싱합니다. 이때 bcrypt.gensalt() 옵션을 통해 난수로 만들어진 솔트가 원본 비밀번호와 함께 해싱됩니다. 이 솔트값에 의해 같은 비밀번호를 해시해도 다른 결과가 나오게 됩니다.
userid 컬럼에 UNIQUE 옵션을 줌으로써 같은 아이디는 입력되지 못하게 하면서 예외처리를 해둡니다.


회원가입이 끝나면 이후 로그인 과정에서 역시 userid와 password를 입력받는데 회원가입할 때 저장된 password는 해시값이므로 복원이 불가능하기 때문에 로그인하면서 입력된 password와 직접적인 비교가 불가능합니다. 따라서 bcrypt의 checkpw 메서드를 사용합니다. 코드상으로는 아래와 같습니다.

login_id=st.text_input("ID를 입력하세요.")
login_pw=st.text_input("비밀번호를 입력하세요.")
if st.button("로그인"):
    cursor = con.cursor()
    login_query = f"select password from a where userid = '{login_id}'"
    cursor.execute(login_query)
    result = cursor.fetchone()
    condition = bcrypt.checkpw(login_pw.encode('utf-8'), result[0].encode('utf-8'))
    con.commit()
    cursor.close()
    if condition:
        st.write("로그인되었습니다")
    else:
        st.write("잘못된 ID 혹은 비밀번호입니다")
  1. 로그인할때 입력된 userid를 이용하여 쿼리로 해시값을 불러옵니다.
  2. 불러온 해시값을 utf-8로 인코딩해줍니다.
  3. 이 해시값과 입력된 password를 bcrypt.checkpw를 사용하여 비교합니다.
  4. 반환된 값은 boolean 형식이므로 일치하면 True, 일치하지 않으면 False로 반환됩니다