魔改flask-httpauth

前情提要:由于前后端分离开发涉及到的无状态问题(前端每次请求后端接口都是新建一个会话,很难通过传统的cookie-session方式来实现验证用户登录,存储用户登录状态等功能),所以考虑了前后端传递token的方式对用户的登录状态进行验证。而为了token的安全性(以及实现的方便性),我使用了flask-httpauth这个第三方模块来实现用户token验证的功能。(注意,我这里使用的是此模块的一部分 – HTTPTokenAuth,如果要使用基本认证请左转出门)

但没想到,这是噩梦的开端

初步使用flask-httpauth

当考虑使用第三方模块的时候,我的第一动作就是去观摩一下前人的使用方式。我很幸运,成功的找到了关于flask-httpauth的教程案例。

首先,是万能第一步

在你项目目录下的命令行(虚拟环境也一样)中敲入这句

pip install flask-httpauth

这样,flask-httpauth就被装到了你的项目环境中

接下来,找到你要用到这个模块的python文件

# 引入头文件
from flask_httpauth import HTTPTokenAuth
# 引入istdangerous模块中的TimedJSONWebSignatureSerializer类用来生成一个具有过期时间的token
# BadSignature是一个异常,当token错误的时候会抛出此异常
# SignatureExpored是一个异常,当token过期的时候会抛出此异常
from itsdangerous import TimedJSONWebSignatureSerializer, BadSignature, SignatureExpired

# 初始化HTTPTokenAuth对象。传入的scheme表示前缀,具体什么作用稍后再说
auth = HTTPTokenAuth(scheme="DAHU")

# 写一个获取token的函数,使用的是API形式,方便测试时获取
@app.route('/api/test-http-auth-token/token', methods=["GET"])
def get_token():
    # 创建一个TimedJSONWebSignatureSerializer对象
    # 第一个参数是随便一串字符串
    # 第二个参数设定过期时间,单位为秒,比如我这里设定的就是1分钟过期(方便测试能否正常捕获token过期异常)
    s1 = TimedJSONWebSignatureSerializer("DAHUBIGFOXSHABI", expires_in=60)
    # 将userId作为数据装到token中,方便我们之后校验用户
    # 默认s1.dumps执行完后返回的是byte格式,但我们需要的是字符串格式,所以用decode函数对结果解码得到token字符串
    token = s1.dumps({"userId": 1}).decode('ascii')
    # 返回json格式的token
    return jsonify(Authorization=token)

# 你可以在这个装饰器下面的函数内实现你自己的token验证方法
@auth.verify_token
def verify_token(token):
    """验证token
		
        :param token: 要验证的token
    """
    # 创建TimedJSONWebSignatureSerializer,传入一个参数,就是前文中那一串字符串(要和前文一样)
    s = TimedJSONWebSignatureSerializer("DAHUBIGFOXSHABI")
    try:
        print(token)
        # 解析token,获取数据(也就是咱们之前包装在token中的{"userId":1})
        data = s.loads(token)
        print(data)
    except SignatureExpired:
        print("token过期")
        return False
    except BadSignature:
        print("token错误")
        return False
    uid = data['userId']
    print(uid)
    return True

@app.route('/api/test-http-auth-token/token', methods=["POST"])
# 加了这条语句就表示要访问此路由需要进行token认证
@auth.login_required
def verify_token():
    return jsonify(code=200)

以上代码还有一点是需要注意的: 捕捉SignatureExpired异常和捕捉BadSignature异常的顺序是不可以颠倒的,因为在源程序中SignatureExpired类的继承顺序是这样的: SignatureExpired继承于BadTimeSignature, BadTimeSignature继承于BadSignature。

显而易见,SignatureExpired是BadSignature的子孙类,如果先捕获BadSignature异常的话,SignatureExpired也会被作为BadSignature异常被捕获,也就不能精确的获取token过期的错误消息了。

接口全部书写完毕,接下来就是紧张刺激的测试环节了

我使用的测试接口的工具是ApiPost, 具体操作我就不赘述了,我创建了两个接口测试用例

  1. 第一个是获取token, 也就是对应着刚刚写的代码中的get_token函数
  2. 第二个是验证token, 也就是对应着刚刚写的代码中的verify_token函数

首先把flask运行起来

然后获取token,对接口发送请求后会返回这么一条json数据

get_token返回的数据

Authorization对应的字符串就是我们需要的token

最后就是验证token了,将Authorization作为请求头,如下

验证token的请求头示例
  Authorization:DAHU eyJhbGciOiJIUzUx...

发送请求后会有三种情况

  1. 认证成功

  2. token错误

  3. token过期

这样,token验证就基本完成了

一切源于不满足

但是,在上面例子里面我们还是能发现一些问题,也就是当token验证失败的时候的返回值(响应)只是一串字符串 – Unauthorized Access,这显然不能满足我们后端API开发的需求,我们需要一个更规范化的返回值。

了解问题,实践开始

惯例第一步,面向搜索引擎。但是这次“可靠的”搜索引擎某度辜负了我的期待,我并没有在搜索引擎上面找到能满足我们方案的办法。

没办法,只能源码走起了

因为是为了自定义验证后的返回值,所以直接聚焦@auth.login_required装饰器的代码

注意:HTTPTokenAuth是继承于HTTPAuth类的,login_required也是定义在HTTPAuth中的

# flask_httpauth.py
# HTTPAuth类

def login_required(self, f=None, role=None, optional=None):
        if f is not None and \
                (role is not None or optional is not None):  # pragma: no cover
            raise ValueError(
                'role and optional are the only supported arguments')

        def login_required_internal(f):
            @wraps(f)
            def decorated(*args, **kwargs):
                auth = self.get_auth()

                # Flask normally handles OPTIONS requests on its own, but in
                # the case it is configured to forward those to the
                # application, we need to ignore authentication headers and
                # let the request through to avoid unwanted interactions with
                # CORS.
                if request.method != 'OPTIONS':  # pragma: no cover
                    password = self.get_auth_password(auth)

                    status = None
                    user = self.authenticate(auth, password)
                    if user in (False, None):
                        status = 401
                    elif not self.authorize(role, user, auth):
                        status = 403
                    if not optional and status:
                        # Clear TCP receive buffer of any pending data
                        request.data
                        try:
                            return self.auth_error_callback(status)
                        except TypeError:
                            return self.auth_error_callback()

                    g.flask_httpauth_user = user if user is not True \
                        else auth.username if auth else None
                return f(*args, **kwargs)
            return decorated

        if f:
            return login_required_internal(f)
        return login_required_internal

因为是要解决错误时候的返回值自定义问题,所以直接聚焦31~34行的错误处理语句

try:
    return self.auth_error_callback(status)
except TypeError:
    return self.auth_error_callback()

可以发现一个关键点 – auth_error_callback(),所以直接向上翻,可以看到auth_error_callback是在__init__中定义的属性

# HttpAuth

 def __init__(self, scheme=None, realm=None, header=None):
        self.scheme = scheme
        self.realm = realm or "Authentication Required"
        self.header = header
        self.get_password_callback = None
        self.get_user_roles_callback = None
        self.auth_error_callback = None

        def default_get_password(username):
            return None

        def default_auth_error(status):
            return "Unauthorized Access", status

        self.get_password(default_get_password)
        self.error_handler(default_auth_error)

由于这里的auth_error_callback是None,做不到上面的错误处理功能,所以我们可以猜测HTTPAuth类中会在某个地方对其进行赋值。

直接在文件中搜索auth_error_callback

suprise!我们在error_handler中找到了对auth_error_callback的赋值

# HttpAuth

def error_handler(self, f):
        @wraps(f)
        def decorated(*args, **kwargs):
            res = f(*args, **kwargs)
            check_status_code = not isinstance(res, (tuple, Response))
            res = make_response(res)
            if check_status_code and res.status_code == 200:
                # if user didn't set status code, use 401
                res.status_code = 401
            if 'WWW-Authenticate' not in res.headers.keys():
                res.headers['WWW-Authenticate'] = self.authenticate_header()
            return res
        self.auth_error_callback = decorated
        return decorated

显而易见,这里传递的参数是一个函数,auth_error_callback得到的值也是一个函数

下一步,找到error_handler函数的调用位置。如果之前仔细看了HttpAuth的__init__函数,会发现error_handler的调用就在那里(见上个代码块第18行)

self.error_handler(default_auth_error)

老惯例,找default_auth_error,也在HttpAuth的__init__函数中

def default_auth_error(status):
    return "Unauthorized Access", status

发现了吗?如此熟悉的字符串。对,就是在这里,我们只要修改了这里就可以修改错误时的返回值了!

但是,在源文件上改显然不是个好方法。

我们需要创建一个新类继承于HTTPTokenAuth

Q:既然要重写的函数都是定义在HTTPAuth中的为什么不直接继承于HTTPAuth呢?

A:因为我们要用到HTTPTokenAuth中独有的装饰器verify_token啊小傻瓜

class HTTPTokenAuthReReturn(HTTPTokenAuth):
    def __init__(self, scheme=None, realm=None, header=None):
        super().__init__(scheme, realm, header)
		
        # 重写default_auth_error, 或者也可以重新定义一个函数
        def default_auth_error(status):
        return jsonify(data={}, header={},
        				message="token错误!",code=status,result=False),status
		
        #如果重新定义函数的话,这里就传入新定义的函数名
        super().error_handler(default_auth_error)

然后直接创建HTTPTokenAuthReReturn对象取代之前的HTTPTokenAuth对象

auth = HTTPTokenAuthReReturn(scheme="DAHU")

进入紧张刺激的测试环节

随便输入一个错误的token,结果如下

然后输入正确的token,结果如下

成功了!

两种异常

经过本文上述的操作之后,已经能成功的返回我们自定义的返回值了。但是依旧有一个问题 – token有两种异常状态:1、token错误 2、token过期, 但是我们现在的返回值只能返回token错误,并不能根据实际的情况返回对应的异常。

需求明确,试验开始

首先要明确这两种异常是在哪里被捕获的

即我们前面自定义的verify_token函数

# verify_token
except SignatureExpired:
    print("token过期")
    return False
except BadSignature:
    print("token错误")
    return False

从这里可以看出,要区别这两个异常就要从返回值入手。

废话不多说,转入源码

# flask_httpauth.py
# HTTPTokenAuth类
def verify_token(self, f):
    self.verify_token_callback = f
    return f

这里verify_token_callback被赋值为f,也就是我们自定义的,被@verify_token修饰的verify_token函数

搜索verify_token_callback, 可以发现verify_token_callback被同类下的authenticate函数使用

# flask_httpauth.py
# HTTPTokenAuth类
def authenticate(self, auth, stored_password):
	if auth:
		token = auth['token']
	else:
		token = ""
	if self.verify_token_callback:
        return self.verify_token_callback(token)

先不用在意前面的处理细节,我们只需要注意到这个函数的返回值就相当于verify_token(token),这里的token就是我们在请求头里传入的Authorization

搜索authenticate的使用,会发现这个函数在login_required中被使用了(下面代码块中第16行)

def login_required(self, f=None, role=None, optional=None):
        if f is not None and \
                (role is not None or optional is not None):  # pragma: no cover
            raise ValueError(
                'role and optional are the only supported arguments')

        def login_required_internal(f):
            @wraps(f)
            def decorated(*args, **kwargs):
                auth = self.get_auth()
                
                if request.method != 'OPTIONS':  # pragma: no cover
                    password = self.get_auth_password(auth)

                    status = None
                    user = self.authenticate(auth, password)
                    if user in (False, None):
                        status = 401
                    elif not self.authorize(role, user, auth):
                        status = 403
                    if not optional and status:
                        # Clear TCP receive buffer of any pending data
                        request.data
                        try:
                            return self.auth_error_callback(status)
                        except TypeError:
                            return self.auth_error_callback()

                    g.flask_httpauth_user = user if user is not True \
                        else auth.username if auth else None
                return f(*args, **kwargs)
            return decorated

        if f:
            return login_required_internal(f)
        return login_required_internal

所以,user就是verify_token(token)的返回值。

思路来了:如果是两个异常中的一个,我们就在verify_token返回一串对应的字符串,而不仅仅是True和False,在login_required中对user进行判断,如果是我们的异常字符串,就给错误处理函数传递一个message,再组合在API返回值中返回

具体实施如下

1、对default_auth_error函数再次重写,添加一个message参数

def default_auth_error(status, message):
            return jsonify(data={}, header={},message=message
            				, code=status, result=False), status

2、修改verify_token函数

@auth.verify_token
def verify_token(token):
    s = TimedJSONWebSignatureSerializer("DAHUBIGFOXSHABI")
    try:
        print(token)
        data = s.loads(token)
        print(data)
    except SignatureExpired:
        print("token过期")
        # 这里不用False,而是用自定义字符串
        return "SignatureExpired"
    except BadSignature:
        print("token错误")
        # 这里不用False,而是用自定义字符串
        return "BadSignature"
    uid = data['userId']
    print(uid)
    return True

3、重写login_required函数

def login_required(self, f=None, role=None, optional=None):
        if f is not None and \
                (role is not None or optional is not None):  # pragma: no cover
            raise ValueError(
                'role and optional are the only supported arguments')

        def login_required_internal(f):
            @wraps(f)
            def decorated(*args, **kwargs):
                auth = self.get_auth()

                if request.method != 'OPTIONS':  # pragma: no cover
                    password = self.get_auth_password(auth)

                    status = None
                    message = None
                    user = self.authenticate(auth, password)
                    print(user)
                    # 这里判断verify_token的返回值是否是这里的一员
                    if user in (False, None, 'BadSignature', 'SignatureExpired'):
                        status = 401
                        if user == 'BadSignature':
                            message = "登录验证失败"
                        elif user == 'SignatureExpired':
                            message = "登录过期"
                    elif not self.authorize(role, user, auth):
                        status = 403
                    if not optional and status:
                        # Clear TCP receive buffer of any pending data
                        request.data
                        try:
                        	# 因为之前重写了default_auth_error所以多传入一个message
                            return self.auth_error_callback(status, message)
                        except TypeError:
                            return self.auth_error_callback()

                    g.flask_httpauth_user = user if user is not True \
                        else auth.username if auth else None
                return f(*args, **kwargs)
            return decorated

        if f:
            return login_required_internal(f)
        return login_required_internal

接下来就是紧张刺激的测试环节了!

  1. 传入一个过期的token

  2. 传入一个错误的token

  3. 传入正确的token

成功!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值