본문 바로가기
  • You find inspiration to create your own path !
업무 자동화/python & CAD

Python 학습] 로그인 프로그램 - 사용자 계정

by ToolBOX01 2025. 8. 17.
반응형

PostgreSQL에서 사용자 로그인 및 비밀번호를 사용하는 프로그램을 위한 테이블 구성 시 고려해야 할 주요 항목은 다음과 같습니다.

로그인 화면

1. 보안 (Security)

  • 비밀번호 해싱 (Password Hashing): 절대로 비밀번호를 평문으로 저장해서는 안 됩니다. 비밀번호를 데이터베이스에 저장하기 전에 강력한 단방향 해싱 알고리즘(예: bcrypt, scrypt, Argon2)을 사용하여 해시(hash)해야 합니다. 이 알고리즘들은 비밀번호 해싱을 위해 특별히 설계되었으며, 해독이 불가능합니다.
  • 솔트(Salt) 사용: 각 비밀번호 해시에 고유한 랜덤 솔트(salt)를 사용해야 합니다. 솔트는 동일한 비밀번호라도 다른 해시 값을 가지게 하여, 레인보우 테이블 공격(rainbow table attack)과 같은 사전 연산 공격을 방지하는 데 필수적입니다.
  • 해싱 알고리즘과 솔트 저장: 비밀번호 해시, 솔트, 그리고 사용된 해싱 알고리즘을 함께 저장하는 것이 좋습니다. 이렇게 하면 나중에 더 강력한 알고리즘으로 업그레이드할 때 유연하게 대응할 수 있습니다.
  • 비밀번호 만료 정책: 정기적으로 비밀번호 변경을 강제하는 정책을 고려할 수 있습니다. 이는 특히 높은 보안이 요구되는 시스템에서 유용합니다.
  • 계정 잠금 (Account Lockout): 일정 횟수 이상 로그인 실패 시 해당 계정을 일시적으로 잠금 처리하여 무차별 대입 공격(brute-force attack)을 방지해야 합니다.
  • 비밀번호 재설정 (Password Reset): 비밀번호를 잊어버렸을 때 사용자가 안전하게 비밀번호를 재설정할 수 있는 메커니즘을 고려해야 합니다. 이때 임시 토큰(token)을 이메일로 보내고, 이 토큰이 만료되도록 설계하는 것이 일반적입니다.

2. 효율성 및 기능성 (Efficiency & Functionality)

  • 테이블 구조:
    • users 테이블:
      • user_id: 사용자 식별을 위한 기본 키(Primary Key) (UUID 또는 SERIAL)
      • username: 사용자 이름 (UNIQUE 제약 조건)
      • email: 이메일 주소 (UNIQUE 제약 조건, NULL 허용 여부)
      • password_hash: 해시된 비밀번호를 저장하는 열 (TEXT)
      • salt: 비밀번호 해시에 사용된 솔트를 저장하는 열 (TEXT)
      • created_at: 계정 생성 시간 (TIMESTAMP)
      • last_login_at: 마지막 로그인 시간 (TIMESTAMP)
      • is_active: 계정 활성화 여부 (BOOLEAN)
      • failed_login_attempts: 로그인 실패 횟수 (INTEGER)
      • locked_until: 계정이 잠금 해제될 시간 (TIMESTAMP)
  • 인덱스 (Indexes): 로그인 시 자주 검색되는 열(예: username, email)에 인덱스를 생성하여 검색 성능을 향상시켜야 합니다.
  • 역할 기반 접근 제어 (RBAC - Role-Based Access Control): 사용자의 권한을 관리하기 위해 별도의 roles 테이블과 user_roles 테이블을 구성하여, 사용자에게 하나 이상의 역할을 부여할 수 있도록 설계하는 것이 좋습니다. 이는 시스템 확장 시 유연성을 제공합니다.
  • 토큰 관리 (Token Management): 세션 관리나 "로그인 상태 유지" 기능을 위해 JWT(JSON Web Token)나 다른 형태의 세션 토큰을 사용한다면, 이 토큰의 무효화(invalidate)와 같은 관리 메커니즘도 고려해야 합니다.


▣  사용자 계정 생성 프로그램 (Create An Account)

1. Python 환경 설정 - 라이브러리 설치

pip install psycopg2-binary
  • psycopg2-binary: PostgreSQL 데이터베이스 연결.

2. 데이터베이스 생성 (예: user_accounts)

3. User 테이블 만들기

사용자 정보를 입력하면, 자동으로 User 테이블이 생성 되고 테이블에 정보가 저장 됩니다. 

테이블의 파이썬 코드는 아래와 같이 정의 됩니다

user_id SERIAL PRIMARY KEY
  • user_id: 각 사용자를 고유하게 식별하는 기본 키(primary key)입니다.
  • SERIAL: 자동 증가하는 정수 값을 생성하여 새 레코드마다 고유한 user_id를 할당합니다. (예: 1, 2, 3, ...)
username VARCHAR(50) UNIQUE NOT NULL
  • username: 사용자의 이름(또는 별칭)을 저장합니다. 최대 50자까지 허용하는 가변 길이 문자열(VARCHAR)입니다.
  • UNIQUE: 동일한 username을 가진 레코드를 허용하지 않아 중복 방지.
  • NOT NULL: 빈 값(NULL)을 허용하지 않음.
email VARCHAR(100) UNIQUE NOT NULL
  • username: 사용자의 이름(또는 별칭)을 저장합니다. 최대 50자까지 허용하는 가변 길이 문자열(VARCHAR)입니다.
  • UNIQUE: 동일한 username을 가진 레코드를 허용하지 않아 중복 방지.
  • NOT NULL: 빈 값(NULL)을 허용하지 않음.
email VARCHAR(100) UNIQUE NOT NULL
  • email: 사용자의 이메일 주소를 저장합니다. 최대 100자까지 허용.
  • UNIQUE: 동일한 이메일 주소를 가진 레코드를 허용하지 않음.
  • NOT NULL: 빈 값(NULL)을 허용하지 않음.
password_hash BYTEA NOT NULL
  • password_hash: 사용자의 비밀번호를 해시(hash)한 값을 저장합니다. BYTEA 타입은 바이너리 데이터를 저장하며, 해시된 비밀번호(예: bcrypt로 생성된 값)를 저장하기에 적합.
  • NOT NULL: 빈 값(NULL)을 허용하지 않음.
salt BYTEA NOT NULL
  • salt: 비밀번호 해시에 사용되는 무작위 문자열(salt)을 저장합니다. 보안 강도를 높이기 위해 사용됨.
  • BYTEA: 바이너리 데이터 타입.
  • NOT NULL: 빈 값(NULL)을 허용하지 않음.
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  • created_at: 사용자가 계정을 생성한 시간을 저장합니다. TIMESTAMP는 날짜와 시간을 포함.
  • DEFAULT CURRENT_TIMESTAMP: 새 레코드가 추가될 때 현재 시간으로 자동 설정.
last_login_at TIMESTAMP last_login_at: 사용자가 마지막으로 로그인한 시간을 저장합니다. NULL로 남길 수도 있음(처음 로그인 전까지).
is_active BOOLEAN DEFAULT TRUE
  • is_active: 계정이 활성화되어 있는지 여부를 나타냅니다. BOOLEAN 타입으로 TRUE(활성) 또는 FALSE(비활성).
  • DEFAULT TRUE: 새 레코드가 추가될 때 기본값은 TRUE로 설정.
failed_login_attempts INTEGER DEFAULT 0
  • failed_login_attempts: 로그인 실패 횟수를 추적합니다. INTEGER 타입.
  • DEFAULT 0: 새 레코드가 추가될 때 기본값은 0으로 설정.
locked_until TIMESTAMP
  • locked_until: 계정이 잠금 해제될 시간을 저장합니다. 예를 들어, 실패 횟수가 많아 계정이 잠겼을 때 해제 시점.
  • NULL로 남길 수 있으며, 필요에 따라 설정.

 

프로그램 코드 : 사용자 정보를 저장 하는 기능 

import customtkinter as ctk
import psycopg2
import bcrypt
from tkinter import messagebox

# --- 데이터베이스 연결 정보 ---
# 실제 환경에서는 이 정보를 환경 변수나 설정 파일에서 안전하게 관리해야 합니다.
DB_NAME = "user_accounts"
DB_USER = "designer"
DB_PASSWORD = "7777"
DB_HOST = "localhost"
DB_PORT = "5432"

# --- 데이터베이스 헬퍼 함수 ---

def get_db_connection():
    """PostgreSQL 데이터베이스 연결을 생성하고 반환합니다."""
    try:
        conn = psycopg2.connect(
            dbname=DB_NAME,
            user=DB_USER,
            password=DB_PASSWORD,
            host=DB_HOST,
            port=DB_PORT,
        )
        return conn
    except psycopg2.OperationalError as e:
        # 연결 정보에 한글이 있거나 인코딩 문제가 있을 경우 발생하는 오류
        messagebox.showerror("Database Connection Error", f"데이터베이스 연결에 실패했습니다. 다음 사항을 확인해주세요:\n- 연결 정보(DB_NAME, DB_USER 등)에 한글이 있는지\n- 스크립트 파일이 UTF-8로 저장되었는지\n\n오류 내용: {e}")
        return None

# --- 메인 애플리케이션 클래스 ---

class UserRegistrationApp(ctk.CTk):
    def __init__(self):
        super().__init__()

        # --- 창 설정 ---
        self.title("Join The Membership")
        self.geometry("400x480")
        self.resizable(False, False)
        ctk.set_appearance_mode("Dark")  # "Dark", "Light" 또는 "System"
        ctk.set_default_color_theme("blue")

        # --- 메인 프레임 ---
        self.main_frame = ctk.CTkFrame(self, corner_radius=15)
        self.main_frame.pack(pady=40, padx=40, fill="both", expand=True)

        # --- 위젯 생성 ---
        self.create_widgets()

    def create_widgets(self):
        """회원가입 UI에 필요한 위젯들을 생성하고 배치합니다."""

        # 제목 레이블
        title_label = ctk.CTkLabel(self.main_frame, text="Create a user account", font=ctk.CTkFont(size=24, weight="bold"))
        title_label.pack(pady=(30, 20))

        # 사용자 이름 입력 필드
        self.username_entry = ctk.CTkEntry(self.main_frame, width=250, placeholder_text="사용자 이름")
        self.username_entry.pack(pady=12, padx=10)

        # 이메일 입력 필드
        self.email_entry = ctk.CTkEntry(self.main_frame, width=250, placeholder_text="이메일 주소")
        self.email_entry.pack(pady=12, padx=10)

        # 비밀번호 입력 필드
        self.password_entry = ctk.CTkEntry(self.main_frame, width=250, placeholder_text="비밀번호", show="*")
        self.password_entry.pack(pady=12, padx=10)

        # 비밀번호 확인 입력 필드
        self.confirm_password_entry = ctk.CTkEntry(self.main_frame, width=250, placeholder_text="비밀번호 확인", show="*")
        self.confirm_password_entry.pack(pady=12, padx=10)

        # 회원가입 버튼
        register_button = ctk.CTkButton(self.main_frame, text="회원가입", command=self.register_event, width=250, height=40)
        register_button.pack(pady=20, padx=10)

    def register_event(self):
        """회원가입 버튼 클릭 시 호출되는 함수."""
        username = self.username_entry.get()
        email = self.email_entry.get()
        password = self.password_entry.get()
        confirm_password = self.confirm_password_entry.get()

        if not username or not email or not password or not confirm_password:
            messagebox.showwarning("입력 오류", "모든 필드를 입력해주세요.")
            return

        if password != confirm_password:
            messagebox.showerror("비밀번호 불일치", "비밀번호가 일치하지 않습니다. 다시 확인해주세요.")
            return

        conn = get_db_connection()
        if conn is None:
            return

        try:
            with conn.cursor() as cur:
                # 비밀번호 해싱
                salt = bcrypt.gensalt()
                hashed_password = bcrypt.hashpw(password.encode('utf-8'), salt)

                # 사용자 정보 저장
                cur.execute(
                    """
                    INSERT INTO users (username, email, password_hash, salt)
                    VALUES (%s, %s, %s, %s)
                    ON CONFLICT (username) DO NOTHING
                    """,
                    (username, email, hashed_password, salt)
                )
                conn.commit()

                if cur.rowcount > 0:
                    messagebox.showinfo("회원가입 성공", f"'{username}'님, 회원가입이 완료되었습니다!")
                    self.destroy()  # 성공 시 창 닫기
                else:
                    messagebox.showwarning("회원가입 실패", "이미 존재하는 사용자 이름입니다.")

        except Exception as e:
            conn.rollback()
            messagebox.showerror("오류", f"회원가입 중 오류가 발생했습니다: {e}")
        finally:
            if conn:
                conn.close()

# --- 애플리케이션 실행 ---
if __name__ == "__main__":
    # 데이터베이스에 'users' 테이블이 없으면 생성
    conn = get_db_connection()
    if conn:
        try:
            with conn.cursor() as cur:
                cur.execute("""
                    CREATE TABLE IF NOT EXISTS users (
                        user_id SERIAL PRIMARY KEY,
                        username VARCHAR(50) UNIQUE NOT NULL,
                        email VARCHAR(100) UNIQUE NOT NULL,
                        password_hash BYTEA NOT NULL,
                        salt BYTEA NOT NULL,
                        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                        last_login_at TIMESTAMP,
                        is_active BOOLEAN DEFAULT TRUE,
                        failed_login_attempts INTEGER DEFAULT 0,
                        locked_until TIMESTAMP
                    );
                """)
                conn.commit()
                print("Users 테이블이 성공적으로 생성되거나 이미 존재합니다.")
        except Exception as e:
            conn.rollback()
            print(f"테이블 생성 중 오류 발생: {e}")
        finally:
            conn.close()

    app = UserRegistrationApp()
    app.mainloop()

 

by korealionkk@gmail.com


반응형