Streamlit Authenticator を使ってログイン画面を用意

# Streamlit でログイン画面

Streamlit でログイン画面が必要になったときのためのメモです。

## 実行環境
- OS
    - macOS Sonoma 14.4.1
- Python 3.11.8
    - [streamlit](https://streamlit.io/)
    - [streamlit_authenticator](https://github.com/mkhorasani/Streamlit-Authenticator)

## 環境作成

```shell
pip install streamlit
pip install streamlit-authenticator
```

## 準備ファイル一覧

### ユーザ情報

ログインに使用するサンプルデータです。ログインには id と password を使います。

```csv:user_info.csv
id,name,password,email
A12345,JohnDoe,johndoepass,johndoe@example.com
B23456,AliceSmith,alicepass,alice.smith@example.com
C34567,BobJohnson,bobjohnsonpass,bobj@example.com
D45678,EmilyBrown,emilypass,emily.brown@example.com
E56789,MichaelClark,mikepass,michael.clark@example.com
F67890,SarahLee,sarahpass,sarah.lee@example.com
G78901,DavidWilson,davidpass,david.wilson@example.com
H89012,JenniferTaylor,jenniferpass,jennifer.taylor@example.com
I90123,DanielMartinez,danielpass,daniel.martinez@example.com
```

### config.yaml テンプレート 

yaml ファイルのテンプレートです。下記をコピーして `config.yaml` という名前で保存してください。

```yaml:config.yaml
cookie:
  expiry_days: 1
  key: some_signature_key
  name: some_cookie_name
credentials:
  usernames:
```

### config.yaml へユーザー情報の書き込み

下記のプログラムを実行することで、パスワードがハッシュ化され yaml へユーザー情報が書き込まれます。

```python:create_yaml.py
import csv
import yaml
from streamlit_authenticator.utilities.hasher import Hasher

users_csv_path = "usersetting.csv"
config_yaml_path = "config.yaml"



## ユーザー設定の一覧が記述されたデータを読み込み
with open(users_csv_path, "r") as f:
    csvreader = csv.DictReader(f)
    users = list(csvreader)

## yaml 設定一覧が記述されたデータを読み込み
with open(config_yaml_path,"r") as f:
    yaml_data = yaml.safe_load(f)

## パスワードのハッシュ化
users_dict = {}
for user in users:
    user["password"] = Hasher([user["password"]]).generate()[0]
    tmp_dict = {
        "name": user["name"],
        "password": user["password"],
        "email": user["email"],
    }
    users_dict[user["id"]] = tmp_dict

## yaml 書き込み
yaml_data["credentials"]["usernames"] = users_dict
with open(config_yaml_path, "w") as f:
    yaml.dump(yaml_data, f)
    print("完了")
```

### 実行結果

`create_yaml.py` を実行すると下記のような yaml が生成されます。
こちらのデータを streamlit 実行時に読み込みます。

```yaml:config.yaml 
cookie:
  expiry_days: 1
  key: some_signature_key
  name: some_cookie_name
credentials:
  usernames:
    A12345:
      email: johndoe@example.com
      name: JohnDoe
      password: $2b$12$WFKXoFg4aBMMO...
    B23456:
      email: alice.smith@example.com
      name: AliceSmith
      password: $2b$12$46mN1PHwisDNy...
    C34567:
      email: bobj@example.com
      name: BobJohnson
      password: $2b$12$8Dln18/gdwLd...

以下略
```




## streamlit 実行
あとは streamlit を実行してみましょう。

```python:streamlit_auth.py
import streamlit as st
import streamlit_authenticator as stauth

import yaml
from yaml.loader import SafeLoader

## ユーザー設定読み込み
yaml_path = "config.yaml"

with open(yaml_path) as file:
    config = yaml.load(file, Loader=SafeLoader)

authenticator = stauth.Authenticate(
    credentials=config['credentials'],
    cookie_name=config['cookie']['name'],
    cookie_key=config['cookie']['key'],
    cookie_expiry_days=config['cookie']['expiry_days'],
)

## UI 
authenticator.login()
if st.session_state["authentication_status"]:
    ## ログイン成功
    with st.sidebar:
        st.markdown(f'## Welcome *{st.session_state["name"]}*')
        authenticator.logout('Logout', 'sidebar')
        st.divider()
    st.write('# ログインしました!')

elif st.session_state["authentication_status"] is False:
    ## ログイン成功ログイン失敗
    st.error('Username/password is incorrect')

elif st.session_state["authentication_status"] is None:
    ## デフォルト
    st.warning('Please enter your username and password')
```

実行すると、下記のようなログイン画面が表示されるので、`user_info.csv`に書かれている id と password を入力してみましょう。

![ログイン画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174323/13b69ab7-e47b-9ae5-c7c1-bab9fad0b8f8.png)

ログインに成功すると、下記画面が表示されます。
ログインに使用したユーザーの名前も表示されています。

![ログイン完了.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174323/5cdba647-a173-99c8-976b-6f3b68cae926.png)

Logout ボタンを押せばログアウトできます。