使用Token与HTTPBasic验证

这一篇我们将用户的信息加密后作为令牌返回到客户端,客户端在访问服务器API时必须以HTTP Basic的方式携带令牌,我们再读取令牌信息后,将用户信息存入到g变量中,共业务代码全局使用。

1. Token概述

一般登录方式有两种,网页下请求和API的方式。

我们看下网页的请求。

image-20180830173845523

我们在浏览器输入账号密码,然后发送到服务器,服务器验证通过后会将一个票据写入到cookie中,最后将cookie返回到浏览器中,由浏览器保存,下次再访问的时候就不再需要登录了。

我们再看下API的方式。

image-20180830174313176

相比于浏览器请求,我们使用API的方式的时候需要自己管理票据而不是通过浏览器cookie的方式了。

使用API方式登录成功后,服务端将Token发送给客户端,由客户端存储,在下次登录的时候,携带上这个Token。这样我们就完成了用户的验证。

一般Token都有几个标识:

  1. 有效期:一般安全考虑要设置一个有效期
  2. 可以标识用户身份:能够标识用户才能完成用户的登录,比如我们存储一个用户ID到Token中。
  3. Token要加密:这样才能防止被篡改
2. 生成Token令牌返回给客户端

我们已经知道API的流程,现在我们首先需要在用户验证登录的时候生成一个Token返回给客户端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from flask import current_app, jsonify

from app.libs.enums import ClientTypeEnum
from app.libs.error_code import AuthFailed
from app.libs.redprint import Redprint
from app.models.user import User
from app.validators.forms import ClientForm, TokenForm
from itsdangerous import (TimedJSONWebSignatureSerializer as Serializer, SignatureExpired,
BadSignature)


api = Redprint('token')


@api.route('', methods=['POST'])
def get_token():
form = ClientForm().validate_for_api()

promise = {
ClientTypeEnum.USER_EMAIL: User.verify,
}

identity = promise[ClientTypeEnum(form.type.data)](
form.account.data,
form.secret.data
)

expiration = current_app.config['TOKEN_EXPIRATION']
# 这里的`itsdangerous`就是之前我们学习的,原理不再重复了
token = generate_auth_token(identity['uid'],
form.type.data,
identity['scope'],
expiration)
t = {
# 上面生成的是byte字节字符串 这里解码成 ascii
'token': token.decode('ascii')
}
return jsonify(t), 201

上面代码调用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# User模型下
@staticmethod
def verify(email, password):
user = User.query.filter_by(email=email).first_or_404()
# 调用下面的验证方法
if not user.check_password(password):
raise AuthFailed()
# 这里是老师做的权限 可以不用管
scope = 'AdminScope' if user.auth == 2 else 'UserScope'
# 返回是字典形式,方便调用
return {'uid': user.id, 'scope': scope}

def check_password(self, raw):
if not self._password:
return False
# 存数据库的是加密的 我们使用check_password_hash进行密码验证
return check_password_hash(self._password, raw)

AuthFailed也是自定义的异常类。

1
2
3
4
5
6
7
8
9
10
def generate_auth_token(uid, ac_type, scope=None,
expiration=7200):
"""生成令牌"""
s = Serializer(current_app.config['SECRET_KEY'],
expires_in=expiration)
return s.dumps({
'uid': uid,
'type': ac_type.value,
'scope':scope
})

完成上面代码之后我们就生成了一个加密有时长的Token了。

3. Token令牌的作用

我们在API访问的时候,有些需要登陆才能访问的接口,不能总是让用户输入账号密码。我们可以在第一次登陆成功后将Token返回给客户端,等以后访问的时候携带上Token,通过验证Token(是否合法,是否过期),当是有效Token的时候我们就能获取到用户相关的信息。

4. @auth拦截器执行流程

如何验证客户端传来的Token呢,在哪验证呢?在每个接口的地方都验证难免会重复。

这种需求一般我们都是使用装饰器来完成。Flask已经帮我们设计好了这种装饰器。

我们可以使用HTTPBasicAuth提供的装饰器来进行接口保护。

1
2
3
4
5
6
7
8
from flask_httpauth import HTTPBasicAuth


auth = HTTPBasicAuth()

@auth.verify_password
def verify_password(account, password):
pass
1
2
3
4
5
6
7
from app.libs.token_auth import auth


@api.route('/<int:uid>', methods=['GET'])
@auth.login_required
def super_get_user(uid):
pass

我们看下上面的代码,解释下执行流程。

首先我们实例化一个HTTPBasicAuth对象,创建一个verify_password方法,传入的参数是账户名和密码。并加上auth.verify_password装饰器。然后我们在需要保护的API接口视图函数上增加装饰器auth.login_required。这样,当用户访问受保护的接口时候会先执行verify_password方法,只有verify_password返回True的时候才能继续访问API视图函数,否则将禁止继续访问。

5.HTTPBasicAuth的基本原理

有没有小伙伴有疑问 在方法verify_password传入的账号,密码怎么传递呢?

这里的传递方式是通过在HTTP的头信息中加入账号密码来传递的,而不是像之前使用Form方式在HTTPbody中传递。

我们知道在HTTP的头部信息都是键值对的形式,HTTPBasicAuth规定的也是键值对的形式,不过稍微有所不同。

1
Authorization: basic base64(account:password)

我在上面写出来样式,详细探讨下。

键值对的键为Authorization,值的开头是basic + 空格 + base64加密过后的账号和密码。

注意: 账后和密码中间有冒号

image-20180901165905702

image-20180901165948814

上面就是在Postman中的使用。

有的小伙伴要说了,我们这里使用的是token登录,不是账号密码,应该如何传递呢?

很简单,我们只需不要传密码就好了,将account改为token即可。

6. 验证token

我们既然已经知道如何生存token,和获得token了,下面就是验证token的正确性了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from itsdangerous import TimedJSONWebSignatureSerializer \
as Serializer, BadSignature, SignatureExpired

# 这里使用命名元组保存数据
User = namedtuple('User', ['uid', 'ac_type', 'scope'])

def verify_auth_token(token):
# 使用同一个字符串实例化出序列化对象
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)

# 在解析数据的时候使用 BadSignature 验证token的有效性
except BadSignature:
raise AuthFailed(msg='token is invalid',
error_code=1002)
# 使用SignatureExpired验证token是否过期
except SignatureExpired:
raise AuthFailed(msg='token is expired',
error_code=1003)
uid = data['uid']
ac_type = data['type']
scope = data['scope']

return User(uid, ac_type, scope)

调用验证方法

1
2
3
4
5
6
7
8
9
10
@auth.verify_password
def verify_password(token, password):

user_info = verify_auth_token(token)
if not user_info:
return False
else:
# 我们将用户信息写入到 g 变量中
g.user = user_info
return True

上面就是完成的API模式下的登录验证。

文档1:https://flask-httpauth.readthedocs.io/en/latest/

文档2:http://www.pythondoc.com/flask-restful/third.html#id5

参考博客1:http://www.bjhee.com/flask-ext9.html

7. 重写first_or_404和get_or_404

之前我们有过重写filter_by函数,现在我们重写下first_or_404get_or_404

先看下get_or_404的源码:

1
2
3
4
5
6
7
8
def get_or_404(self, ident):
"""Like :meth:`get` but aborts with 404 if not found instead of
returning `None`.
"""
rv = self.get(ident)
if rv is None:
abort(404)
return rv

我们在调用abort的地方改成我们自定义的异常抛出,而不是抛出HTTPException异常。

1
2
3
4
5
def get_or_404(self, ident):
rv = self.get(ident)
if not rv:
raise NotFound()
return rv

我们再看下first_or_404的源码:

1
2
3
4
5
6
7
8
def first_or_404(self):
"""Like :meth:`first` but aborts with 404 if not found instead of
returning `None`.
"""
rv = self.first()
if rv is None:
abort(404)
return rv

我们同样是在abort的地方调用我们自己的异常。

1
2
3
4
5
def first_or_404(self):
rv = self.first()
if not rv:
raise NotFound()
return rv

其中的NotFound为:

1
2
3
4
class NotFound(APIException):
code = 404
msg = 'the resource are not found'
error_code = 1001

这样我们在查询数据库的时候,无需再进行异常捕捉。

1
user = User.query.filter_by(email=email).first_or_404()
知识就是财富
如果您觉得文章对您有帮助, 欢迎请我喝杯水!