View on GitHub

mission_craft

flask之token认证

使用token的理由

为符合restful api风格,我们的服务器应该是无状态的,即不应该在服务端维持每个用户状态,不需要为每个登录用户维护一个会话,从而改进服务器的性能

这种无状态的前后端通信可以通过token实现,具体讲就是

从头到尾,服务器都没有维持过用户的登录信息,服务器都不知道具体的某个用户是否是在登录状态,只有等到请求到达,检查出token合法,服务器才知道,这个用户是处于登录之中的

flask中封装token的package使用

flask在itsdangerous这个package中封装了TimedJSONWebSignatureSerializer类,即有一定时效性的json web token(简称jwt)

使用方法

  1. 构造实例时传入秘钥参数、有效时长参数’expires_in’(单位是s),这个秘钥应该由你自己设定,不建议使用明文写在代码、或者明文存在磁盘数据库中,为了安全起见,建议在程序运行时手动输入一个秘钥,由程序读取后存入内存,下次服务器程序再次启动时重新手动输入上次输入的秘钥,一次保证安全性

  2. 使用上一步构造的实例,调用TimedJSONWebSignatureSerializer类的dumps方法,传入一个对象以此创建一个带有加密用户信息、具有时效性的token,对象中的键值对记录了该用户的信息,比如 { id:’1’, username:’user’ } 就是一个合格的例子。创建好之后,将这个token放到response的body中,返回给前端
  3. 当有请求从前端过来,解析request中的header,获取其中的token字段,进行解密,判断是否有SignatureRxpired过期、BadSignature无法解密,如果都没有,就将解密的用户信息放入服务器内存,进行使用

具体代码如下:

# 步骤1代码:
s = Serializer(current_app.config['SECRET_KEY'], expires_in=3600)

# 步骤2代码:
return jsonify(data={
    'message':'Login successfully',
    'token':s.dumps({'idUser': user['idUser'], 'email': user['email']}).decode('utf-8')
})

#前端收到token后将token放入后续请求的头部:
get({headers={'Authorization':'加密后的token'},body={}})

# 步骤3代码
# 请求代码:在请求函数前加上login_required
@bp.route('/user/', methods=['GET'])
@auth.login_required
def get_info():
    pass

上面的代码中,我们看到,通过在请求处理函数之前加上修饰@auth.login_required就能够完成token验证(如果验证失败则通过另外的处理机制向前端返回报错),其实只有上面的代码还不够,我们还需要定义login_required函数来处理、解析token:

想要定义自己的login_required函数,可以这样做:

from flask_httpauth import HTTPTokenAuth
auth = HTTPTokenAuth()

@auth.verify_token
def verify_token(token):
    g.user = None
    s = Serializer(current_app.config['SECRET_KEY'])
    try:
        data = s.loads(token)
    except SignatureExpired:
        # token正确但是过期了
        return False
    except BadSignature:
        # token错误
        return False
    if 'idUser' in data:
        db = get_db()
        g.user = db.execute(
            'SELECT * FROM User WHERE idUser = ?', (data['idUser'],)
        ).fetchone()
        return True
    return False

当然,上面提到的处理验证失败的机制也是可以自定义的,可以这样:

@auth.error_handler
def error_handler():
    return jsonify({ code:401, message:'401 Unauthorized Access' })

实现原理探究

如果你只想知道怎么做能够完成flask的token认证,那么这篇教学到此为止,可以不用看下去了

但是作为一名合格的程序员,我们不应该满足于怎么做,还应该去探求为什么,所以我们是不是应该进一步知道为什么这样写可以呢:

想要知道为什么,最简单的方法就是翻源码,直接到flask_httpauth的github上看具体实现。

flask_httpauth.HTTPTokenAuth的实现,代码&解读如下:

# HTTPTokenAuth类继承HTTPAuth
class HTTPTokenAuth(HTTPAuth):
    def __init__(self, scheme='Bearer', realm=None):
        super(HTTPTokenAuth, self).__init__(scheme, realm)

        self.verify_token_callback = None

    def verify_token(self, f):
        self.verify_token_callback = f
        return f

    def authenticate(self, auth, stored_password):
        if auth:
            token = auth['token']
        else:
            token = ""
        if self.verify_token_callback:
            return self.verify_token_callback(token)
        return False
    
# 父类HTTPAuth
class HTTPAuth(object):
    def __init__(self, scheme=None, realm=None):
        self.scheme = scheme
        self.realm = realm or "Authentication Required"
        self.get_password_callback = None
        self.auth_error_callback = None

        def default_get_password(username):
            return None

        def default_auth_error():
            return "Unauthorized Access"

        self.get_password(default_get_password)
        self.error_handler(default_auth_error)
        
	# 认证错误处理机制
    def error_handler(self, f):
        @wraps(f)
        def decorated(*args, **kwargs):
            res = f(*args, **kwargs)
            res = make_response(res)
            if res.status_code == 200:
                res.status_code = 401
            if 'WWW-Authenticate' not in res.headers.keys():
                res.headers['WWW-Authenticate'] = self.authenticate_header()
            return res
        # 把auth_error_callback设置为自己,login_required认证失败之后调用自己,处理异常
        self.auth_error_callback = decorated
        return decorated
    
	# 登录需要
    def login_required(self, f):
        @wraps(f)
        def decorated(*args, **kwargs):
            auth = self.get_auth()
            if request.method != 'OPTIONS':  # pragma: no cover
                password = self.get_auth_password(auth)

                if not self.authenticate(auth, password):
                    request.data
                    return self.auth_error_callback()

            return f(*args, **kwargs)
        return decorated

为了抓住重点,我上面只是贴了重要部分的代码,完整代码参见flaskauth

其实你可能会疑惑,为什么完成token验证是在请求处理函数之前加上修饰@auth.login_required但是实际上定义自己的login_required函数是通过@auth.verify_token修改来完成的,下面就为你解答:

我们可以试着溯源,从请求到达后端开始分析,前端发来/user/,method=get的api过来,被flask通过路由@bp.route(‘/user/’, methods=[‘GET’]),引导到get_info函数,然而在执行get_info函数之前,需要先执行login_required

login_required是哪来的呢,我们翻看HTTPTokenAuth,没有这个成员函数,再向前看,看到HTTPTokenAuth的父类HTTPAuth,终于找到HTTPTokenAuth.login_required函数

这个函数接受一个函数f作为参数,并对f进行修饰,其实在这里,get_info函数就是这个f,login_required中,先执行用户认证,认证通过再调用f,即get_info,否则调用auth_error_callback处理错误

总结

如果您只想知道用法,那么仅仅需要了解flask中封装token的package使用

如果您想要知道为什么能够做到认证,那么您还需要看实现原理探究

以上,足以讲清楚flask如何使用token认证用户身份这一技术,以及为什么能够这样做,让您能够知其然并且知其所以然。

最后感谢阅读