Streamlit如何跳转第三方页面实现登陆鉴权

本文关键字:streamlit python cas authentication multi-page

Python可以使用streamlit框架实现0前端代码,全栈开发数据大屏等简单的可交互可视化web项目。

Streamlit: A faster way to build and share data apps

普通的提交表单用户名密码的方式实现登陆可以使用streamlit-authenticator第三方包轻松实现。
该文章主要记录了在使用streamlit实际场景中,遇到需要跳转第三方cas页面进行登陆的情况,改如何实现streamlit应用的鉴权登陆。

streamlit相关参考文档

  1. 官方文档:https://docs.streamlit.io/
  2. 实现表单登陆文档: https://blog.streamlit.io/streamlit-authenticator-part-1-adding-an-authentication-component-to-your-app/
  3. 实现跳转登陆参考文档: https://levelup.gitconnected.com/building-a-multi-page-app-with-streamlit-and-restricting-user-access-to-pages-using-aws-cognito-89a1fb5364a3

效果展示

  • 首次进入页面不展示内容,需要点击LOGIN登陆
    需要登陆
  • 点击登陆跳转到鉴权页面,登陆成功自动返回,并出现Logout按钮, 子页面相同
    登录后展示主页面

    登录后展示子页面

代码实现

streamlit基本代码释义请参考官方文档,不再赘述。

代码结构

代码结构

代码展示

实现方法直接用代码来说明,在每个展示页面前,需要先判断鉴权,未登陆时则只提示并展示登陆按钮;登陆成功后展示页面内容。
直接参考以下代码,即可复用在其余场景中。

1. 配置文件

抽取的配置文件位于config.py

import os

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# 后端数据接口地址,用于请求实际的数据
API_HOST = "http://127.0.0.1:8080/api"

# streamlit运行后访问地址
FE_HOST = "http://127.0.0.1:8501"

# CAS跳转登陆地址
CAS_LOGIN_URL = "https://127.0.0.1:4436/sec/login"

# 允许访问的用户名
ALLOWED_USERS = [
    "user1",
    "user2",
    "user3"
]

2. 跳转鉴权组件代码

鉴权组件代码位于utils/authenticate.py

import streamlit as st
import requests
import json

from config import FE_HOST, CAS_LOGIN_URL, ALLOWED_USERS


# ------------------------------------
# Initialise Streamlit state variables
# ------------------------------------
def initialise_st_state_vars():
    if "auth_code" not in st.session_state:
        st.session_state["auth_code"] = ""
    if "authenticated" not in st.session_state:
        st.session_state["authenticated"] = False
    if "user_cognito_groups" not in st.session_state:
        st.session_state["user_cognito_groups"] = []


# ----------------------------------
# Get authorization code after login
# ----------------------------------
def get_auth_code():
    auth_query_params = st.experimental_get_query_params()
    try:
        auth_code = dict(auth_query_params)["sid"][0]
    except (KeyError, TypeError):
        auth_code = ""

    return auth_code

# -----------------------------
# Set Streamlit state variables
# -----------------------------
def set_st_state_vars():
    initialise_st_state_vars()
    auth_code = get_auth_code()
    user_info = get_auth_user(sid=auth_code) or {}

    if user_info.get("user"):
        st.session_state["auth_code"] = auth_code
        st.session_state["authenticated"] = True
        st.session_state["user"] = user_info.get("user")
        st.session_state["email"] = user_info.get("mail")
        st.session_state["display"] = user_info.get("display")


# -----------------------------
# Login/ Logout HTML components
# -----------------------------
login_link = f"{CAS_LOGIN_URL}?ref={FE_HOST}"


html_css_login = """
<style>
.button-login {
  background-color: skyblue;
  color: white !important;
  padding: 1em 1.5em;
  text-decoration: none;
  text-transform: uppercase;
}

.button-login:hover {
  background-color: #555;
  text-decoration: none;
}

.button-login:active {
  background-color: black;
}

</style>
"""

html_button_login = (
    html_css_login
    + f"<a href='{login_link}' class='button-login' target='_self'>Log In</a>"
)


def button_login():
    """

    Returns:
        Html of the login button.
    """
    _, col, _ = st.columns(3)
    return col.markdown(f"{html_button_login}", unsafe_allow_html=True)


def button_logout():
    """

    Returns:
        Html of the logout button.
    """
    def logout_click():
        st.session_state["authenticated"] = False
    st.sidebar.button("Logout", on_click=logout_click)
    print(st.session_state)


def get_auth_user(sid, ref=FE_HOST):
    cas_url = f"{CAS_LOGIN_URL}?sid=%s&ref=%s" % (sid, ref)
    if not sid or not ref:
        return

    user_info = requests.get(cas_url ).text
    try:
        user_dict = json.loads(user_info)
    except json.decoder.JSONDecodeError:
        return
    else:
        return user_dict


def is_allowed_user():
    if st.session_state["email"] in ALLOWED_USERS:
        return True
    return False
3. 入口主页面文件

入口位于Welcome.py
需要在所有展示的页面代码文件前都加上鉴权代码

import streamlit as st

from utils import authenticate
from utils.authenticate import is_allowed_user


st.set_page_config(
    page_title="Welcome",
    page_icon="👋",
)

# 初始化鉴权变量
authenticate.set_st_state_vars()

# Add login/logout buttons,点击可跳转
if not st.session_state.get("authenticated"):
    st.warning("Please login!")
    authenticate.button_login()
else:
    authenticate.button_logout()
    if not is_allowed_user():
        st.error("You do not have access. Please contact the administrator.")
    else:
        # else,页面展示代码位于通过鉴权后
        st.title("欢迎使用XX仪表盘 👋")

        st.markdown(
            """
           
            该项目为streamlit跳转登陆测试项目\n
            **👈 请从侧边栏进入功能页**
            ### 官方参考文档
            - Streamlit: [Streamlit](https://docs.streamlit.io/)
            - 表单登陆: [streamlit-authenticator](https://blog.streamlit.io/streamlit-authenticator-part-1-adding-an-authentication-component-to-your-app/)
        
            ### 实现跳转登陆参考文档
            - [参考文档](https://levelup.gitconnected.com/building-a-multi-page-app-with-streamlit-and-restricting-user-access-to-pages-using-aws-cognito-89a1fb5364a3)
    
        """
        )
4. 分页面文件

分页示例文件位于pages/1_📊__Overview.py

import datetime

import streamlit as st

from utils import dict_to_dataframe, authenticate
from utils.authenticate import is_allowed_user

from utils.plotly_utils import plotly_pie, plotly_bar

st.set_page_config(
    page_title="Overview",
    page_icon="👋",
    layout="wide"
)

authenticate.set_st_state_vars()

# Add login/logout buttons
if not st.session_state.get("authenticated"):
    st.warning("Please login!")
    authenticate.button_login()
else:
    authenticate.button_logout()
    # 可以继续限制允许登陆的用户
    if not is_allowed_user():
        st.error("You do not have access. Please contact the administrator.")
    # 通过登陆后的页面代码
    else:
        st.subheader("数据量概览")
        overview_data = {
            "updateTime": "2023-01-01 00:00:00",
            "total": "12345614",
            "todaySeen": "12345",
            "recent7daysSeen": "1234561",
            "black": "2435"
        }
        overview_group_data = {
            'all': {
                'source': {
                    'source1': 13467,
                    'source2': 56900,
                    'source3': 89580409,
                    'source4': 25405
                },
                'tag1st': {
                    'tag1': 1414953,
                    'tag2': 3112059,
                    'tag3': 48486,
                    'tag4': 23226,
                    'tag5': 4907815,
                    'tag6': 9690544
                },
                'category': {
                    'red': 1382345,
                    'green': 258362
                },
                'updateTime': '2023-03-01T08:31:00.345000Z'
            },
            'recent7Days': {
                'source': {
                    'source1': 245,
                    'source2': 2457,
                },
                'tag1st': {
                    'tag1': 12345,
                    'tag3': 12345,
                    'tag4': 1235,
                },
                'category': {
                    'red': 1341,
                    'green': 3456
                },
                'updateTime': '2023-03-01T08:31:00.345000Z'
            },
            'calculateDate': '20230301'
        }

        st.markdown(f"`概览统计时间:{overview_data['updateTime']}`")

        recent_7_group_data = overview_group_data["recent7Days"]
        all_group_data = overview_group_data["all"]
        overview_group_date = overview_group_data["calculateDate"]

        col1, col2, col3, col4 = st.columns(4)
        col1.metric("数据总量", overview_data["total"])
        col2.metric("今日生效", overview_data["todaySeen"])
        col3.metric("最近7日生效", overview_data["recent7daysSeen"])
        col4.metric("无效数据", overview_data["black"])

        st.markdown("### 数据分类统计")
        st.markdown(f"`分类数据生效时间:{datetime.datetime.strptime(overview_group_date, '%Y%m%d')}`")
        date_range = st.sidebar.radio(
            "您关注哪一时间段数据?",
            ('最近七日', '全量'),
            index=1
        )

        display_type = st.sidebar.multiselect(
            "您想使用哪些方式展示数据?",
            ('图形', '表格'),
            default=('图形', '表格')
        )

        if date_range == "最近七日":
            group_data = recent_7_group_data
        else:
            group_data = all_group_data

        source_data = group_data["source"]
        tag1st_data = group_data["tag1st"]
        category_data = group_data["category"]


        def write_overview_data_charts(data):
            tab1, tab2 = st.tabs(["Pie(default)", "Bar"])
            with tab1:
                fig_pie = plotly_pie(data)
                st.plotly_chart(fig_pie, use_container_width=True)
            with tab2:
                fig_bar = plotly_bar(data)
                st.plotly_chart(fig_bar, use_container_width=True)


        def write_overview_data_table(data, key_name):
            tab1, tab2 = st.tabs(["table(default)", "json"])
            with tab1:
                st.dataframe(dict_to_dataframe(data, key_name), use_container_width=True)
            with tab2:
                st.json(data)


        def write_overview_to_column(data, title, table_key):
            st.markdown(f"#####  {title}")
            if "图形" in display_type:
                write_overview_data_charts(data, )
            if "表格" in display_type:
                write_overview_data_table(data, key_name=table_key)


        if not display_type:
            st.sidebar.error("请至少选择一种展示方式")
            st.error("请在左侧复选框选择至少一种展示方式")

        if display_type:
            col5, col6, col7 = st.columns(3)
            with col5:
                write_overview_to_column(source_data, title="情报源分布", table_key="数据源")
            with col6:
                write_overview_to_column(tag1st_data, title="标签分布", table_key="标签")
            with col7:
                write_overview_to_column(category_data, title="数据类别分布", table_key="数据类别")

最后

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容