python 单点登录

前言

  最近项目中小组负责开发的知识图谱子系统需要增加单点登录这个功能,由于我也是头一次开发这个,也请教了java后端大佬,再根据自己具体业务和使用的框架(fastapi)完成了任务,现在回过头来整理一下。
  单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。在整个项目中,存在多个业务系统,也有独立登录系统,把授权的逻辑与用户信息的相关逻辑独立,由统一认证中心进行管理。因此子系统部分只需处理具体的业务逻辑,在登录时,将用户的登录请求转发给统一认证中心进行处理,统一认证中心处理完毕返回凭证,子系统验证凭证,然后登录用户。

单点登录流程图

在这里插入图片描述

代码

  本项目使用fastapi搭建后端服务框架,因此使用其中间件middleware完成对各功能接口的token认证(所有接口请求都需要先经过middleware后,才能进入到具体接口,处理业务逻辑)。整体逻辑是根据不同API的url进行不同的操作逻辑:单点登录相关的接口,无需token,通过middleware不进行判断;登录界面需要用户名称信息,解析authorization获取用户名称,用于登出展示;退出登录接口,解析authorization,调用log_out函数,删除存放在redis中的token;其余接口,每次请求,先解析authorization判断用户是否符合要求,此处为机关用户,符合则刷新token,不符合则返回403,前端展示当前用户暂无权限。

from fastapi import FastAPI
platform_app = FastAPI()

# 中间件,每个API请求先经过该函数
@platform_app.middleware("http")
async def token_update(request, call_next):
    """验证token是否过期,未过期则刷新token"""
    # 单点登录相关接口,无需token
    if request.url.path == '/xxx/log-in-url/' or request.url.path == '/xxx/authorization/' or request.url.path == '/xxx/taskid-to-token/':
        platform_logger.info('no jwt verify')
    # 从token中获取用户名称,用于登出展示
    elif request.url.path == '/xxx/userInfo/':
        headers = request.headers
        if "Authorization" in headers:
            token = headers.get("authorization")
            if r.get('IIKG:{}'.format(token)):  # 判断token是否存在与redis中
                decode, dedata = decode_token(token)
                dedata = dedata.get("sub")
                if decode == 200:
                    global userName
                    userName = dedata
                else:
                    json_compatible_item_data = jsonable_encoder({"code": 401, "msg": "请登录后再试!", "data": ''})
                    return JSONResponse(content=json_compatible_item_data)
            else:
                json_compatible_item_data = jsonable_encoder({"code": 401, "msg": "请登录后再试!", "data": ''})
                return JSONResponse(content=json_compatible_item_data)
        else:
            json_compatible_item_data = jsonable_encoder({"code": 401, "msg": "Token is missing or incorrect header name", "data": ""})
            return JSONResponse(content=json_compatible_item_data)
    # 退出登录接口,删除token
    elif request.url.path == '/xxx/log-out/':
        headers = request.headers
        if "Authorization" in headers:
            token = headers.get("authorization")
            log_out(r, token)
        else:
            json_compatible_item_data = jsonable_encoder({"code": 401, "msg": "Token is missing or incorrect header name", "data": ""})
            return JSONResponse(content=json_compatible_item_data)
    # 其余接口,每次请求,先刷新token
    else:
        platform_logger.info('jwt verify')
        headers = request.headers
        if "Authorization" in headers:
            token = headers.get("authorization")
            if r.get('IIKG:{}'.format(token)):  # 判断token是否存在与redis中
                if len(token) != 0:
                    decode, dedata = decode_token(token)
                    if dedata.get("type") == "大佬用户":
                        msg = update_token(r, token)
                        platform_logger.info(msg)
                        if msg == "token已过期,请重新登录!":
                            json_compatible_item_data = jsonable_encoder({"code": 401, "msg": "请登录后再试!", "data": ''})
                            return JSONResponse(content=json_compatible_item_data)
                    else:
                        json_compatible_item_data = jsonable_encoder({"code": 403, "msg": "当前用户暂无权限", "data": ''})
                        return JSONResponse(content=json_compatible_item_data)
                # token参数为空时,返回登录提示信息
                else:
                    json_compatible_item_data = jsonable_encoder({"code": 401, "msg": "请登录后再试!", "data": ''})
                    return JSONResponse(content=json_compatible_item_data)
            else:
                json_compatible_item_data = jsonable_encoder({"code": 401, "msg": "请登录后再试!", "data": ''})
                return JSONResponse(content=json_compatible_item_data)
        else:
            json_compatible_item_data = jsonable_encoder({"code": 401, "msg": "Token is missing or incorrect header name", "data": ""})
            return JSONResponse(content=json_compatible_item_data)
    response = await call_next(request)
    return response
# 单点登录
@platform_app.get("/xxx/log-in-url/")
def ca_url():
    """返回统一认证中心url"""
    # 读取配置文件的统一认证中心URL
    url = scheme + "://" + host + ":" + port + sso_login_path + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + redirectUri
    platform_logger.info("log-in-url>>>:{}".format(url))
    return {"code": 200, "msg": "请求成功", "data": url}


@platform_app.post("/xxx/authorization/")
async def search_user_info(item: Item, background_tasks: BackgroundTasks):
    """根据授权码,验证信息,生成token,存入redis"""
    item_dict = item.dict()
    authCode = item_dict["license_code"]
    taskId = str(random.randint(1000000, 10000000))
    redisKey = 'IIKG:{}'.format(taskId)
    callback_ret = {"status": 1, "data": {}}
    r.set(redisKey, '{}'.format(callback_ret), ex=300)
    background_tasks.add_task(getmain, r, redisKey, callback_ret, authCode)
    return {"code": 200, "msg": "请求成功", "data": 'IIKG:{}'.format(taskId)}


@platform_app.post("/xxx/taskid-to-token/")
def get_user_token(item: Item):
    """根据任务ID,查询redis,返回任务结果"""
    item_dict = item.dict()
    taskId = item_dict["taskId"]
    result = search_db(r, taskId)
    return {"code": 200, "msg": "请求成功", "data": eval(result)}


@platform_app.get("/xxx/userInfo/")
def logOut_userInfo():
    """退出登录用户信息"""
    return {"code": 200, "msg": "请求成功", "data": userName}


@platform_app.post("/xxx/log-out/")
def user_logOut():
    """退出登录"""
    url = scheme + "://" + host + ":" + port + sso_logout_path + "?redirect_uri=" + redirectHomeUri
    platform_logger.info("log-out-url>>>:{}".format(url))
    return {"code": 200, "msg": "请求成功", "data": url}

  首先在fast_app.py(接口文件)中,需要一个get请求返回统一认证中心的登录地址(/xxx/log-in-url/);用户完成登录后前端返回给我们一个授权码。
  根据之前得到的授权码,请求获取统一认证中心获取access_token;然后再次请求统一认证中心并根据access_token获取用户信息;最后用户正确,则生成token,并将token存入redis。

import base64
import requests
from jose import jwt
from apps.project_config import *
from datetime import datetime, timedelta
from apps.log_util import configure_logger

def getmain(r, redisKey, callback_ret, authCode):
    """请求用户中心主程序
    Args:
        r: (:obj), redis数据库实例化
        redisKey: (:str), 任务id
        callback_ret: (dict), 存入redis的待修改对象
        authCode: (:str), 授权码
    """
    ssoCode, ssoToken = getSsoTokenFromAuthServer(r, callback_ret, redisKey, authCode)
    if ssoCode == 200:
        userCode, userInfo = getUserInfoFromAuthServer(ssoToken)
        if userCode == 200:
            # 用户正确通过,则生成token,将token存入redis
            redisKey = create_access_token(r, callback_ret, redisKey, userInfo)
            return redisKey
    callback_ret["status"] = 4
    r.set(redisKey, '{}'.format(callback_ret), ex=300)
    return
def getUserInfoFromAuthServer(access_token):
    """从统一认证中心获取用户信息
    Args:
        access_token: (:str), 访问接口使用的token

    Returns:
    """
    data = {}
    # url
    sso_url = scheme + "://" + host + ":" + port + "/v1.0/user/me"
    headers = {"Authorization": "Bearer {}".format(access_token)}
    try:
        ret = requests.get(sso_url, headers=headers)
        userType = ret.json().get("extend").get("userTypeText")  # 取出用户类型信息
        user_name = ret.json().get("username")
        user_id = ret.json().get("user_id")
        data = {"user_name": user_name, "user_id": user_id, "user_type": userType}
        return 200, data
    except:
        platform_logger.info("单点登录异常")
    return 500, data
def create_access_token(r, callback_ret, redisKey, data):
    """生成token"""
    expire = datetime.utcnow() + timedelta(minutes=180)
    to_encode = {"exp": expire, "sub": data["user_name"], "uid": data["user_id"], "type": data["user_type"]}
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    callback_ret["status"] = 3
    callback_ret["data"] = encoded_jwt
    r.set(redisKey, '{}'.format(callback_ret), ex=300)  # 生成以taskId为key的redis存储(供前端使用)
    r.set('IIKG:{}'.format(encoded_jwt), '{}'.format(data["user_id"]), ex=1800)  # 生成以token为key的redis存储(用于后续token刷新)
    return redisKey

  除此之外还有其他接口及功能用到的以下函数,包括对生成的token进行解码的decode_token函数;根据任务ID查询redis中token的search_db函数;刷新token寿命的update_token函数;退出登录,删除redis中存放的token的log_out函数。

def decode_token(token):
    """解密token"""
    try:
        decode_jwt = jwt.decode(token, SECRET_KEY, algorithms=ALGORITHM)
        platform_logger.info("解码信息[%s]>>>" % decode_jwt)
        return 200, decode_jwt
    except:
        platform_logger.info("解码信息[%s]>>>" % "token已过期")
        return 401, "token已过期"
def search_db(r, taskId):
    """根据任务ID查询"""
    token = r.get("{}".format(taskId))
    if token: return token
    return "{'status': 4, 'data': {}}"
def update_token(r, token):
    """刷新token"""
    redis_key = 'IIKG:{}'.format(token)  # 拼凑redis的key
    status = r.expire(redis_key, time=1800)
    if status:
        return "刷新token成功!"
    else:
        return "token已过期,请重新登录!"
def log_out(r, token):
    """根据输入的token,删除redis中的token数据"""
    redis_key = 'IIKG:{}'.format(token)  # 拼凑redis的key
    r.delete(redis_key)
    return "退出登录token成功!"
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风吹半夏灬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值