最近项目中小组负责开发的知识图谱子系统需要增加单点登录这个功能,由于我也是头一次开发这个,也请教了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成功!"