【Python开发】Flask中的单点登录解决方案

news2024/11/20 8:38:57

Flask中的单点登录解决方案

1.SSO 和 CAS

单点登录(Single Sign OnSSO)就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是比较流行的。

中央认证服务(Central Authentication ServiceCAS),一种独立开放指令协议,是耶鲁大学发起的一个企业级开源项目,旨在为 Web 应用系统提供一种可靠的 SSO 解决方案。

在这里插入图片描述

在这里插入图片描述

2.Flask-CAS

那么对于 Flask,我们如何实现单点登录呢?此处提供一个开源的解决方案:Flask-CAS。在实际开发中,直接使用它的 API 可能无法满足我们的需求,这个时候就需要对源码进行一些修改。目前最新的版本是 1.0.2 版本。

在这里插入图片描述

__init__.py:主要是一些配置和基本的函数。
cas_urls.py:主要是生成一些 CAS 登录相关的 url 以及跳转的 url。
routing.py:项目的核心所在,主要包含了 登录、登出、验证 三个函数,这三个函数也是我们自定义时候重点修改的地方。

"""
flask_cas.__init__
"""

import flask
from flask import current_app

# Find the stack on which we want to store the database connection.
# Starting with Flask 0.9, the _app_ctx_stack is the correct one,
# before that we need to use the _request_ctx_stack.
try:
    from flask import _app_ctx_stack as stack
except ImportError:
    from flask import _request_ctx_stack as stack

from . import routing

from functools import wraps

class CAS(object):
    """
    Required Configs:

    |Key             |
    |----------------|
    |CAS_SERVER      |
    |CAS_AFTER_LOGIN |

    Optional Configs:

    |Key                        | Default               |
    |---------------------------|-----------------------|
    |CAS_TOKEN_SESSION_KEY      | _CAS_TOKEN            |
    |CAS_USERNAME_SESSION_KEY   | CAS_USERNAME          |
    |CAS_ATTRIBUTES_SESSION_KEY | CAS_ATTRIBUTES        |
    |CAS_LOGIN_ROUTE            | '/cas'                |
    |CAS_LOGOUT_ROUTE           | '/cas/logout'         |
    |CAS_VALIDATE_ROUTE         | '/cas/serviceValidate'|
    |CAS_AFTER_LOGOUT           | None                  |
    """

    def __init__(self, app=None, url_prefix=None):
        self._app = app
        if app is not None:
            self.init_app(app, url_prefix)

    def init_app(self, app, url_prefix=None):
        # Configuration defaults
        app.config.setdefault('CAS_TOKEN_SESSION_KEY', '_CAS_TOKEN')
        app.config.setdefault('CAS_USERNAME_SESSION_KEY', 'CAS_USERNAME')
        app.config.setdefault('CAS_ATTRIBUTES_SESSION_KEY', 'CAS_ATTRIBUTES')
        app.config.setdefault('CAS_LOGIN_ROUTE', '/cas')
        app.config.setdefault('CAS_LOGOUT_ROUTE', '/cas/logout')
        app.config.setdefault('CAS_VALIDATE_ROUTE', '/cas/serviceValidate')
        # Requires CAS 2.0
        app.config.setdefault('CAS_AFTER_LOGOUT', None)
        # Register Blueprint
        app.register_blueprint(routing.blueprint, url_prefix=url_prefix)

        # Use the newstyle teardown_appcontext if it's available,
        # otherwise fall back to the request context
        if hasattr(app, 'teardown_appcontext'):
            app.teardown_appcontext(self.teardown)
        else:
            app.teardown_request(self.teardown)

    def teardown(self, exception):
        ctx = stack.top
    
    @property
    def app(self):
        return self._app or current_app

    @property
    def username(self):
        return flask.session.get(
            self.app.config['CAS_USERNAME_SESSION_KEY'], None)

    @property
    def attributes(self):
        return flask.session.get(
            self.app.config['CAS_ATTRIBUTES_SESSION_KEY'], None)

    @property
    def token(self):
        return flask.session.get(
            self.app.config['CAS_TOKEN_SESSION_KEY'], None)

def login():
    return flask.redirect(flask.url_for('cas.login', _external=True))

def logout():
    return flask.redirect(flask.url_for('cas.logout', _external=True))

def login_required(function):
    @wraps(function)
    def wrap(*args, **kwargs):
        if 'CAS_USERNAME' not in flask.session:
            flask.session['CAS_AFTER_LOGIN_SESSION_URL'] = (
                flask.request.script_root +
                flask.request.full_path
            )
            return login()
        else:
            return function(*args, **kwargs)
    return wrap
"""
flask_cas.cas_urls

Functions for creating urls to access CAS.
"""

try:
    from urllib import quote
    from urllib import urlencode
    from urlparse import urljoin
except ImportError:
    from urllib.parse import quote
    from urllib.parse import urljoin
    from urllib.parse import urlencode


def create_url(base, path=None, *query):
    """ Create a url.

    Creates a url by combining base, path, and the query's list of
    key/value pairs. Escaping is handled automatically. Any
    key/value pair with a value that is None is ignored.

    Keyword arguments:
    base -- The left most part of the url (ex. http://localhost:5000).
    path -- The path after the base (ex. /foo/bar).
    query -- A list of key value pairs (ex. [('key', 'value')]).

    Example usage:
    >>> create_url(
    ...     'http://localhost:5000',
    ...     'foo/bar',
    ...     ('key1', 'value'),
    ...     ('key2', None),     # Will not include None
    ...     ('url', 'http://example.com'),
    ... )
    'http://localhost:5000/foo/bar?key1=value&url=http%3A%2F%2Fexample.com'
    """
    url = base
    # Add the path to the url if it's not None.
    if path is not None:
        url = urljoin(url, quote(path))
    # Remove key/value pairs with None values.
    query = filter(lambda pair: pair[1] is not None, query)
    # Add the query string to the url
    url = urljoin(url, '?{0}'.format(urlencode(list(query))))
    return url


def create_cas_login_url(cas_url, cas_route, service, renew=None, gateway=None):
    """ Create a CAS login URL .

    Keyword arguments:
    cas_url -- The url to the CAS (ex. http://sso.pdx.edu)
    cas_route -- The route where the CAS lives on server (ex. /cas)
    service -- (ex.  http://localhost:5000/login)
    renew -- "true" or "false"
    gateway -- "true" or "false"

    Example usage:
    >>> create_cas_login_url(
    ...     'http://sso.pdx.edu',
    ...     '/cas',
    ...     'http://localhost:5000',
    ... )
    'http://sso.pdx.edu/cas?service=http%3A%2F%2Flocalhost%3A5000'
    """
    return create_url(
        cas_url,
        cas_route,
        ('service', service),
        ('renew', renew),
        ('gateway', gateway),
    )


def create_cas_logout_url(cas_url, cas_route, service=None):
    """ Create a CAS logout URL.

    Keyword arguments:
    cas_url -- The url to the CAS (ex. http://sso.pdx.edu)
    cas_route -- The route where the CAS lives on server (ex. /cas/logout)
    url -- (ex.  http://localhost:5000/login)

    Example usage:
    >>> create_cas_logout_url(
    ...     'http://sso.pdx.edu',
    ...     '/cas/logout',
    ...     'http://localhost:5000',
    ... )
    'http://sso.pdx.edu/cas/logout?service=http%3A%2F%2Flocalhost%3A5000'
    """
    return create_url(
        cas_url,
        cas_route,
        ('service', service),
    )


def create_cas_validate_url(cas_url, cas_route, service, ticket,
                            renew=None):
    """ Create a CAS validate URL.

    Keyword arguments:
    cas_url -- The url to the CAS (ex. http://sso.pdx.edu)
    cas_route -- The route where the CAS lives on server (ex. /cas/serviceValidate)
    service -- (ex.  http://localhost:5000/login)
    ticket -- (ex. 'ST-58274-x839euFek492ou832Eena7ee-cas')
    renew -- "true" or "false"

    Example usage:
    >>> create_cas_validate_url(
    ...     'http://sso.pdx.edu',
    ...     '/cas/serviceValidate',
    ...     'http://localhost:5000/login',
    ...     'ST-58274-x839euFek492ou832Eena7ee-cas'
    ... )
    'http://sso.pdx.edu/cas/serviceValidate?service=http%3A%2F%2Flocalhost%3A5000%2Flogin&ticket=ST-58274-x839euFek492ou832Eena7ee-cas'
    """
    return create_url(
        cas_url,
        cas_route,
        ('service', service),
        ('ticket', ticket),
        ('renew', renew),
    )
"""
flask_cas.routing
"""

import flask
from xmltodict import parse
from flask import current_app
from .cas_urls import create_cas_login_url
from .cas_urls import create_cas_logout_url
from .cas_urls import create_cas_validate_url


try:
    from urllib import urlopen
except ImportError:
    from urllib.request import urlopen

blueprint = flask.Blueprint('cas', __name__)


@blueprint.route('/login/')
def login():
    """
    This route has two purposes. First, it is used by the user
    to login. Second, it is used by the CAS to respond with the
    `ticket` after the user logs in successfully.

    When the user accesses this url, they are redirected to the CAS
    to login. If the login was successful, the CAS will respond to this
    route with the ticket in the url. The ticket is then validated.
    If validation was successful the logged in username is saved in
    the user's session under the key `CAS_USERNAME_SESSION_KEY` and
    the user's attributes are saved under the key
    'CAS_USERNAME_ATTRIBUTE_KEY'
    """

    cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY']

    redirect_url = create_cas_login_url(
        current_app.config['CAS_SERVER'],
        current_app.config['CAS_LOGIN_ROUTE'],
        flask.url_for('.login', origin=flask.session.get('CAS_AFTER_LOGIN_SESSION_URL'), _external=True))

    if 'ticket' in flask.request.args:
        flask.session[cas_token_session_key] = flask.request.args['ticket']

    if cas_token_session_key in flask.session:

        if validate(flask.session[cas_token_session_key]):
            if 'CAS_AFTER_LOGIN_SESSION_URL' in flask.session:
                redirect_url = flask.session.pop('CAS_AFTER_LOGIN_SESSION_URL')
            elif flask.request.args.get('origin'):
                redirect_url = flask.request.args['origin']
            else:
                redirect_url = flask.url_for(
                    current_app.config['CAS_AFTER_LOGIN'])
        else:
            del flask.session[cas_token_session_key]

    current_app.logger.debug('Redirecting to: {0}'.format(redirect_url))

    return flask.redirect(redirect_url)


@blueprint.route('/logout/')
def logout():
    """
    When the user accesses this route they are logged out.
    """

    cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
    cas_attributes_session_key = current_app.config['CAS_ATTRIBUTES_SESSION_KEY']

    if cas_username_session_key in flask.session:
        del flask.session[cas_username_session_key]

    if cas_attributes_session_key in flask.session:
        del flask.session[cas_attributes_session_key]

    if(current_app.config['CAS_AFTER_LOGOUT'] is not None):
        redirect_url = create_cas_logout_url(
            current_app.config['CAS_SERVER'],
            current_app.config['CAS_LOGOUT_ROUTE'],
            current_app.config['CAS_AFTER_LOGOUT'])
    else:
        redirect_url = create_cas_logout_url(
            current_app.config['CAS_SERVER'],
            current_app.config['CAS_LOGOUT_ROUTE'])

    current_app.logger.debug('Redirecting to: {0}'.format(redirect_url))
    return flask.redirect(redirect_url)


def validate(ticket):
    """
    Will attempt to validate the ticket. If validation fails, then False
    is returned. If validation is successful, then True is returned
    and the validated username is saved in the session under the
    key `CAS_USERNAME_SESSION_KEY` while tha validated attributes dictionary
    is saved under the key 'CAS_ATTRIBUTES_SESSION_KEY'.
    """

    cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
    cas_attributes_session_key = current_app.config['CAS_ATTRIBUTES_SESSION_KEY']

    current_app.logger.debug("validating token {0}".format(ticket))

    cas_validate_url = create_cas_validate_url(
        current_app.config['CAS_SERVER'],
        current_app.config['CAS_VALIDATE_ROUTE'],
        flask.url_for('.login', origin=flask.session.get('CAS_AFTER_LOGIN_SESSION_URL'), _external=True),
        ticket)

    current_app.logger.debug("Making GET request to {0}".format(
        cas_validate_url))

    xml_from_dict = {}
    isValid = False

    try:
        xmldump = urlopen(cas_validate_url).read().strip().decode('utf8', 'ignore')
        xml_from_dict = parse(xmldump)
        isValid = True if "cas:authenticationSuccess" in xml_from_dict["cas:serviceResponse"] else False
    except ValueError:
        current_app.logger.error("CAS returned unexpected result")

    if isValid:
        current_app.logger.debug("valid")
        xml_from_dict = xml_from_dict["cas:serviceResponse"]["cas:authenticationSuccess"]
        username = xml_from_dict["cas:user"]
        flask.session[cas_username_session_key] = username

        if "cas:attributes" in xml_from_dict:
            attributes = xml_from_dict["cas:attributes"]

            if "cas:memberOf" in attributes:
                if not isinstance(attributes["cas:memberOf"], list):
                    attributes["cas:memberOf"] = attributes["cas:memberOf"].lstrip('[').rstrip(']').split(',')
                    for group_number in range(0, len(attributes['cas:memberOf'])):
                        attributes['cas:memberOf'][group_number] = attributes['cas:memberOf'][group_number].lstrip(' ').rstrip(' ')
                else:
                    for index in range(0, len(attributes['cas:memberOf'])):
                        attributes["cas:memberOf"][index] = attributes["cas:memberOf"][index].lstrip('[').rstrip(']').split(',')
                        for group_number in range(0, len(attributes['cas:memberOf'][index])):
                            attributes['cas:memberOf'][index][group_number] = attributes['cas:memberOf'][index][group_number].lstrip(' ').rstrip(' ')

            flask.session[cas_attributes_session_key] = attributes
    else:
        current_app.logger.debug("invalid")

    return isValid

3.Example

首先需要导入包。

from flask_cas import CAS

使用方法一:

app = Flask(__name__)
CAS(app)

使用方法二:

cas = CAS()
app = Flask(__name__)
cas.init_app(app)

CAS 类将添加两条路由 /login//logout/。也可以把一个路由前缀作为第二个参数传给 CAS 的构造方法或初始化方法。

/login/ 路由会将用户重定向到 CAS_SERVERE 配置值指定的 CAS。如果登录成功,用户将被重定向到由 CAS_AFTER_LOGIN 配置值指定的端点,登录用户的用户名也将被存储到由 CAS_USERNAME_SESSION_KEY 指定的键的 session 中。如果属性可用,它们将被存储在 CAS_ATTRIBUTES_SESSION_KEY 指定的键的 session 中。

/logout/ 路由会将用户重定向到 CAS 注销页面,用户名和属性将会从 session 中被删除。

下面给出一个实例:

import flask
from flask import Flask
from flask_cas import CAS
from flask_cas import login_required

app = Flask(__name__)
cas = CAS(app)
# 此处填写server端地址
app.config['CAS_SERVER'] = 'https://server.cas.com:8443/cas'
# 填写cas登录后跳转的路由
app.config['CAS_AFTER_LOGIN'] = 'login'
app.config['SECRET_KEY'] ='dasdasdasdasdada123123423423aASDAS'
# 全局取消证书验证
import ssl
ssl._create_default_https_context = ssl._create_unverified_context


@app.route('/')
@login_required
def login():
    return flask.render_template(
        'demo.html',
        username=cas.username,
    )


if __name__ == '__main__':
    app.run("app1.cas.com", 9100, debug=True)  # 此处填写客户端路由

demo.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>{{ username }}</h1>
</body>
</html>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/77003.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

E. DS哈希查找--Trie树

目录 题目描述 思路分析 AC代码 题目描述 Trie树又称单词查找树&#xff0c;是一种树形结构&#xff0c;如下图所示。 它是一种哈希树的变种。典型应用是用于统计&#xff0c;排序和保存大量的字符串&#xff08;但不仅限于字符串&#xff09;&#xff0c;所以经常被搜索引擎…

【经验版】Linux相关教程(二)

一、参考资料 二、常用指令 1. 安装run软件包 # 可执行权限 chmod x 软件包名.run# 校验软件包安装文件的一致性和完整性 ./软件包名.run --check# 指定安装路径 ./软件包名.run --install如果用户未指定安装路径&#xff0c;则软件会安装到默认路径下&#xff0c;默认安装路…

kafka一致性保证

1、概念 水位标记&#xff1a; 水位或水印&#xff08;watermark&#xff09;一词&#xff0c;表示位置信息&#xff0c;即位移&#xff08;offset&#xff09;。Kafka源码中使用的名字是高水位&#xff0c;HW&#xff08;high watermark&#xff09;。 副本角色&#xff1a;…

华硕编程竞赛11月JAVA专场 E题太空漫步 题解

作者主页&#xff1a;Designer 小郑 作者简介&#xff1a;Java全栈软件工程师一枚&#xff0c;来自浙江宁波&#xff0c;负责开发管理公司OA项目&#xff0c;专注软件前后端开发&#xff08;Vue、SpringBoot和微信小程序&#xff09;、系统定制、远程技术指导。CSDN学院、蓝桥云…

cleanmymac4.12最新版下载安装教程

cleanmymac2023的“智能扫描”功能略不同于两外两款软件。除垃圾扫描以外&#xff0c;它还连带有搜索mac潜在威胁以及寻找提升系统性能方案的功能。在垃圾文件分类方面&#xff0c;它将垃圾首先分为系统垃圾、iTunes垃圾、照片垃圾3大类&#xff0c;每一类再做具体细分。但这样…

AUTOSAR OTA升级

一、OTA技术概念 随着高级辅助驾驶的发展和自动驾驶的引入&#xff0c;汽车变得越来越智能&#xff0c;这些智能汽车被软件控制&#xff0c;装有巨量的软件程序&#xff0c;当出现一个软件程序问题或者更新时&#xff0c;如果 按照传统的解决方式 &#xff0c;那都将是一项很繁…

美腾科技科创板上市:预计年营收4.7亿到5.7亿 市值44亿

雷递网 雷建平 12月9日天津美腾科技股份有限公司&#xff08;简称&#xff1a;“美腾科技”&#xff0c;股票代码为&#xff1a;“688420”&#xff09;今日在科创板上市。美腾科技此次发行2211万股&#xff0c;发行价为48.96元&#xff0c;募资总额为10.83亿元。美腾科技开盘价…

Leetcode 1691. 堆叠长方体的最大高度 [Java/C++] 排序+动态规划(附详细证明过程)

给你 n 个长方体 cuboids &#xff0c;其中第 i 个长方体的长宽高表示为 cuboids[i] [widthi, lengthi, heighti]&#xff08;下标从 0 开始&#xff09;。请你从 cuboids 选出一个 子集 &#xff0c;并将它们堆叠起来。如果 widthi < widthj 且 lengthi < lengthj 且 h…

计算机网络(自顶向下)—第八章习题

在下面的空格中填入“谁的什么密钥”&#xff1a; &#xff08;1&#xff09; A 向 B 发送一个一次性会话密钥&#xff0c;A 用B的公钥加密该会话密钥。 &#xff08;2&#xff09; Certifier.com 用Certifier.com的私钥 为 foo.com 签发公钥证书。 &#xff08;3&#xff…

红队隧道应用篇之Neo-reGeorg实现内网穿透(四)

简介 reGeorg是一个能够实现内网穿透的工具&#xff0c;基于socks5协议&#xff0c;且能支持众多脚本 由于此工具使用率过高&#xff0c;导致容易被杀毒软件拦截, 现有一个项目是由reGeorg修改而来, 而且做了加密和免杀处理, 这款工具的名字就叫Neo-reGeorg Neo-reGeorg下载…

Python+Selenium+Unittest 之selenium1--环境搭建

对于学习一个新东西来说&#xff0c;最开始就是要搭建环境了&#xff0c;关于python的环境搭建这里就不说了&#xff0c;主要说下selenium的环境搭建相关内容和安装过程中可能遇到的坑&#xff0c;细节不太一致的可以自行百度解决下&#xff0c;本章所使用的版本为python3.9sel…

Xcode安装特定版本系统的模拟器(不支持断点下载所以总是下载失败)

Xcode里下载太慢就算了&#xff0c;他不支持断点下载&#xff0c;一直一直一直下载失败&#xff0c;根本就装不上嘛&#xff01;&#xff01;&#xff01; 添加模拟器、下载需要的iOS版本 添加模拟器 没有要的iOS版本则点击Download more 然而因为Xcode不支持断点下载&…

【C语言】内存操作函数

目录 一、memcpy函数 1、memcpy函数的用途 2、memcpy函数的使用 3、memcpy函数的模拟实现 二、memmove函数 1、memmove函数的用途 2、memmove函数的使用 3、memmove函数的模拟实现 三、memset函数 1、memset函数的用途 2、memset函数的使用 3、memset函数的模拟实现 四、memcmp…

React 入门:实战案例 TodoList 修改 Todo Item的状态

文章目录目标实现效果实现思路实现步骤第一步&#xff1a;定义更改 Todo 状态的方法&#xff0c;以供调用第二步&#xff1a;App 组件传递更改 Todo 状态的方法给子组件 List第三步&#xff1a;List 组件传递更改 Todo 状态的方法给子组件 Item第四步&#xff1a;Item 调用更改…

Spring 6.0 正式发布,一文了解新特性

正式发布 Spring Framework 6.0 首个 RC 版本正式发布&#xff0c;可以开始使用了。 新特性 我们一起来看看这次6.0版本带来了哪些特性&#xff1f;需要注意的是该版本整个框架代码库现在基于 Java 17 源代码级别&#xff0c;所以如果你想使用需要升级版本到 JDK 17 才可以&a…

什么是FPGA fpga的核心作用

fpga名词解释&#xff1a;FPGA是英文Field Programmable Gate Array的缩写&#xff0c;即现场可编程门阵列&#xff0c;它是在PAL、GAL、EPLD等可编程器件的基础上进一步发展的产物。 fpga核心做用&#xff1a;它是作为专用集成电路(ASIC)领域中的一种半定制电路而出现的&#…

【吴恩达机器学习笔记】十七、总结

✍个人博客&#xff1a;https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 &#x1f4e3;专栏定位&#xff1a;为学习吴恩达机器学习视频的同学提供的随堂笔记。 &#x1f4da;专栏简介&#xff1a;在这个专栏&#xff0c;我将整理吴恩达机器学习视频的所有内容的笔记&…

计算机毕业设计springboot+vue基本医院公众号建设推动医疗卫生服务现状研究

项目介绍 随着信息技术和网络技术的飞速发展,人类已进入全新信息化时代,传统管理技术已无法高效,便捷地管理信息。为了迎合时代需求,优化管理效率,各种各样的管理系统应运而生,各行各业相继进入信息管理时代,医院公众号建设推动医疗卫生服务就是信息时代变革中的产物之一。 任…

从零开始学Java之eclipse的安装配置与使用,看这篇就够了

前言 在上一篇文章中&#xff0c;壹哥给大家介绍了Notepad这个更高级点的记事本&#xff0c;它进行Java开发相比windows自带的记事本要更方便一些。但是即便如此&#xff0c;用这种记事本进行Java开发效率依然很低。如果是少量的代码编写还好说&#xff0c;大量代码的开发&…

计算机毕业设计springboot+vue基本微信小程序的电子书阅读器小程序

项目介绍 随着信息技术和网络技术的飞速发展,人类已进入全新信息化时代,传统管理技术已无法高效,便捷地管理信息。为了迎合时代需求,优化管理效率,各种各样的管理系统应运而生,各行各业相继进入信息管理时代,电子书阅读器小程序就是信息时代变革中的产物之一。 任何系统都要遵循…