Streamlitでログイン画面とユーザ登録画面を作る

お疲れ様です。

PythonのWebアプリフレームワークであるStreamlitを使ってログイン画面とユーザ登録画面を作ってみたのでその紹介。
以前作成していたチャットボットのアプリに実装していたものをデモ用に機能だけを取り出した感じです。↓

github.com

今回作成のコードは以下にあります。

github.com


使用したライブラリはStreamlitとStreamlit-Authenticatorです。
Streamlit-Authenticatorにユーザ認証に必要な機能がそろっています。公式のGitHubが以下↓ github.com

  • インストール

    pip install streamlit
    pip install streamlit-authenticator

それぞれの画面の機能とソースコードについて軽く説明します。

ログイン画面(トップページ)

  • main.py
    最初に開くページにユーザ名とパスワードを入力するUIが表示されます。

プログラムの最初に認証用のクラスを準備します。クラスはst.session_stateに格納してほかのページでも使用できるようにします。
auth.yamlはユーザ名とパスワード等の情報を記載したファイルになります。(今回は作成していませんが、存在しないときに初期化で作成するような処理をつくると良いかもしれません。)

# 定数
AUTH_FILE_PATH = Path("auth.yaml")

with open(AUTH_FILE_PATH, mode="r") as f:
    auth = yaml.safe_load(f)

st.session_state.auth_controller = AuthenticationController(auth['credentials'])

Streamlit-Authenticatorではログイン画面自体を簡単に作成する機能を備えているのですが、カスタマイズがしにくいので自作しています。Streamlitのst.markdownやst.text_inputを使用しています。

st.title("パスワード認証デモ")

st.write("ログインを試してみる!")
with st.container(border=True):
    st.markdown(f"ユーザー名:")
    username = st.text_input(" ", label_visibility="collapsed", key="username_login")
    st.markdown(f"パスワード:")
    password = st.text_input(" ", type='password', label_visibility="collapsed", key="password_login")

    # ログインボタン
    login_button(username, password)

st.divider()

col1, col2 = st.columns([2, 1])
with col1:
    st.write("ユーザ登録する!")
with col2:
    if st.button("新規登録", use_container_width=True):
        st.switch_page("./pages/signup.py")

st.text_inputに入力された内容を取得して、Streamlit-Authenticatorの機能のみで認証するというイメージです。ログインボタンの機能がそれにあたります。
st.session_state.auth_controller.login(username, password)の部分で入力されたユーザ名とパスワードをチェックしてTrue/Falseを返してくれる感じです。

def login_button(username: str, password: str) -> None:
    """ログインのボタン
    """
    if st.button("ログイン", use_container_width=True, type='primary'):
        auth_ok = st.session_state.auth_controller.login(username, password)
        # ログイン成功
        if auth_ok:
            st.switch_page("./pages/page.py")
        # ログイン失敗
        else:
            st.error("ユーザ名またはパスワードが間違っています。", icon="🚨")

ユーザ登録画面

  • pages/signup.py
    ログイン画面から遷移できるような作りになっています。

基本的には入力されたユーザ名とパスワードをauth.yamlに合わせた形に成形して書き込むだけの処理です。ただしパスワードに関しては入力された文字列をHasher.hash(password)でハッシュ化(元の文字列を不規則な文字列に置換する処理)して書き込む形になります。
また、UIにメールアドレスの入力欄があるように、ユーザ名とパスワード以外の情報を付加することも可能です。ログイン認証したときに一緒に情報として取得できます。

st.title("新規ユーザ登録")

st.write("ユーザ名とパスワードを入力してください。")

with st.container(border=True):
    st.markdown(f"ユーザー名{REQUIRED_MARK}:", unsafe_allow_html=True)
    username = st.text_input(" ", label_visibility="collapsed", key="username_signup")
    st.markdown(f"パスワード{REQUIRED_MARK}:", unsafe_allow_html=True)
    password = st.text_input(" ", type='password', label_visibility="collapsed", key="password_signup")
    st.markdown(f"メールアドレス:", unsafe_allow_html=True)
    email = st.text_input(" ", label_visibility="collapsed", key="email")

    col1, col2 = st.columns(2)
    with col1:
        if st.button("登録", use_container_width=True):
            # 認証データが書き込まれたファイルを読み込み
            with open(AUTH_FILE_PATH, mode="r") as f:
                yaml_data = yaml.safe_load(f)
                
                # ユーザデータが空の場合の対処
                if yaml_data["credentials"]["usernames"] is None:
                    yaml_data["credentials"]["usernames"] = dict()
            
            if (username == "") or (password == ""):
                # ユーザ名orパスワードの入力欄が空欄の場合
                st.error("ユーザ名およびパスワードの入力は必須です。", icon="🚨")
            elif username in yaml_data["credentials"]["usernames"].keys():
                # 既に登録済みのユーザ名の場合
                st.error("登録済みのユーザ名と重複しています。<br>別のユーザ名で登録してください。", icon="🚨")
            else:
                # パスワードをハッシュ化
                password_hashed = Hasher.hash(password)
                
                # 認証データファイルに書き込み
                yaml_data["credentials"]["usernames"][username] = {
                    "name": username,
                    "password": password_hashed,
                    "email": email
                }
                with open(AUTH_FILE_PATH, "w") as f:
                    yaml.dump(yaml_data, f)
                    
                # トップ画面に戻る
                complate_signup_alert()

    with col2:
        if st.button("キャンセル", use_container_width=True):
            # 登録せずにトップページに戻る
            st.switch_page("./main.py")

ログイン後の画面

  • pages/page.py
    「ログアウトボタン」があるので少しだけ紹介。といってもst.session_state.auth_controller.logout()を使用するだけですね。
@st.dialog("ログアウト")
def logout_dialog() -> None:
    """ログアウトの確認ダイアログ
    """
    st.write("ログアウトしますか?")
    col1, col2 = st.columns(2)
    with col1:
        if st.button("OK", use_container_width=True):
            st.session_state.auth_controller.logout()
            st.switch_page("./main.py")
    with col2:
        if st.button("キャンセル", use_container_width=True):
            st.rerun()
あとがき

今回はStreamlit-Authenticatorを使ってログイン画面とユーザ登録画面をざっくり作ってみました。今回のコードでは使用しませんでしたがCookieを利用した処理などもあるようなのでまだまだやれることは多そうです。
StreamlitもPythonのみでかなり良い感じにWebアプリを作成できますし、関連ライブラリも増えてきたのでかなり使いやすくなっている印象です!