如何保护Superset自定义免登场景下的/login/端点安全性?
Superset自定义免登接口的安全加固方案
方案1:共享密钥签名校验(最高优先级实现)
这是成本最低、安全性足够的方案,操作逻辑如下:
- 自有Web应用和Superset提前约定好唯一的共享密钥,保存在两边的服务配置中,绝不对外暴露
- 自有应用生成跳转Superset的链接时,额外传递3个参数:
timestamp(当前10位/13位时间戳)、nonce(随机字符串,防重放)、sign(签名值)- 签名算法参考:
sign = HMAC-SHA256(共享密钥, username + "|" + role + "|" + timestamp + "|" + nonce)
- 签名算法参考:
- Superset的login逻辑中,先做三层校验,全部通过再执行自动登录逻辑:
- 校验timestamp是否在当前时间前后5分钟范围内,过期直接拒绝
- 用相同的算法和参数重新计算签名,和请求传递的sign对比,不一致直接拒绝
- 可选:把用过的nonce存入缓存,设置10分钟过期,同一个nonce只能用一次,防止链接被盗用重放
- 额外注意:角色信息不要直接从URL参数取,要么包含在签名校验范围内,要么Superset直接对接自有系统的用户角色接口查询,避免用户篡改URL提升权限
对应的代码修改示例:
import hmac import hashlib import time from flask import request, redirect from flask_login import login_user from superset.security import SupersetSecurityManager from flask_appbuilder.security.views import AuthDBView from flask_appbuilder import expose # 共享密钥,建议从环境变量读取,不要硬编码 SHARED_SECRET = b"your_custom_shared_secret_here" # 签名有效期,单位秒 VALID_DURATION = 300 class CustomAuthDBView(AuthDBView): @expose('/login/', methods=['GET', 'POST']) def login(self): redirect_url = self.appbuilder.get_url_for_index user_name = request.args.get('username') user_role = request.args.get('role') timestamp = request.args.get('timestamp') sign = request.args.get('sign') nonce = request.args.get('nonce') # 开启自动登录逻辑前先做安全校验 if user_name is not None: # 校验所有必填参数是否存在 if not all([user_role, timestamp, sign, nonce]): return super(CustomAuthDBView,self).login() # 校验时间是否有效 try: ts = int(timestamp) if abs(time.time() - ts) > VALID_DURATION: print('链接已过期', 'warning') return super(CustomAuthDBView,self).login() except ValueError: return super(CustomAuthDBView,self).login() # 校验签名 raw_str = f"{user_name}|{user_role}|{timestamp}|{nonce}".encode() computed_sign = hmac.new(SHARED_SECRET, raw_str, hashlib.sha256).hexdigest() if not hmac.compare_digest(computed_sign, sign): print('签名校验失败', 'warning') return super(CustomAuthDBView,self).login() # 校验通过后再执行原有登录、创建用户逻辑 user = self.appbuilder.sm.find_user(username=user_name) if not user: role = self.appbuilder.sm.find_role(user_role) if not role: return super(CustomAuthDBView,self).login() user = self.appbuilder.sm.add_user(user_name, user_name, 'last_name', user_name + "@domain.com", role, password = "random_generated_password_not_fixed") if user: login_user(user, remember=False) return redirect(redirect_url) else: print('Unable to auto login', 'warning') return super(CustomAuthDBView,self).login() class CustomSecurityManager(SupersetSecurityManager): authdbview = CustomAuthDBView def __init__(self, appbuilder): super(CustomSecurityManager, self).__init__(appbuilder)
方案2:辅助加固措施
- 校验请求的Referer头,只允许来自自有应用域名的请求进入自动登录逻辑,不过Referer可伪造,只能作为辅助手段,不能作为唯一校验
- 原有逻辑中创建用户的固定密码要改成随机生成的强密码,避免有人用固定密码走常规登录入口登录
- 不要开启GET请求的用户创建逻辑,如果用户不存在,建议先跳转自有系统做校验后再同步创建,避免被遍历用户名批量创建账号
- 生产环境不要在URL中传递敏感参数,可以改用JWT令牌把所有用户信息加密后作为单个参数传递,Superset侧只需要校验JWT的签名和有效期即可,不用处理多个参数的签名逻辑
内容的提问来源于stack exchange,提问作者Swastik
相关产品推荐
相关产品推荐

