第十四章 应用编程接口(三)

       与普通WEB应用一样,WEB服务也需要保护信息,确保未授权的用户无法访问。我们已经知道,REST式WEB服务的特征是无状态,即服务器在2次请求之间不能记住“客户端”的任何信息。客户端必须在发出的请求中包含所有必要信息,因此所有请求都必须包含用户凭据。

        Flasky应用当前的登录是由Flask-login实现的,数据存储在用户会话中。我们不妨先来回顾下Flask-Login的工作流程:

  1. 首先获取user id,如果获取不到有效的id,就将user设为anonymous user;
  2. 获取到id后,再通过@login_manager.user_loader装饰的函数获取到user对象,如果没有获取到有效的user对象,就认为是anonymous user;
  3. 最后不管是是正常的用户还是anonymous用户,均保存到请求上下文中;

默认情况下,Flask把会话存储在客户端cookie中,因此服务器没有保存任何用户信息,都转交给客户端保存。这种实现方式看起来遵守了REST架构无状态的要求,但在REST式WEB服务中使用cookie有点不现实,因为WEB浏览器之外的客户端很难实现对cookie的支持。

       REST架构基于HTTP协议,所以发送凭据的最佳方式是使用HTTP身份验证,用户凭据包含在每个请求的Authorization首部中。HTTP身份验证协议很简单,可以直接实现。Flask-HTTPAuth扩展提供了一个遍历的包装,把协议的细节隐藏在装饰器中。类似于Flask-Login中的@login_required。

一. 使用Flask-HTTPAuth验证用户身份

Flask-HTTPAuth不对验证用户凭据的步骤做任何假设,所需的信息在回调函数中提供。

app/api/authentication.py:初始化Flask-HTTPAuth

from flask import g
from flask_httpauth import HTTPBasicAuth
from ..models import User

auth = HTTPBasicAuth()


@auth.verify_password
def verify_password(email, password):
    if email_or_token == '':
        return False
    if not user:
        return False
    g.current_user = user
    g.token_used = False
    return user.verify_password(password)

        如果登录凭据不正确这个函数返回False,否则返回True。如果请求中没有身份验证信息,Flask-HTTPAuth也会调用回调函数,把2个参数都设为空字符串。此处我们把通过身份验证的用户保存到Flask的上下文变量g中,供视图函数稍后访问。

       如果身份验证凭据不正确,则服务器向客户端返回401状态码。默认情况下,Flask-HTTPAuth会自动生成这个状态码,但为了与API返回的其它错误保持一致,我们可以自定义这个错误响应。

@auth.error_handler
def auth_error():
    return unauthorized('Invalid credentials')


@api.before_request
@auth.login_required
def before_request():
    if g.current_user.is_authenticated and not g.current_user.confirmed:
        return forbidden('Unconfirmed account.')

       像上述代码那样,为了保护路由,可以使用@auth.login_required装饰器。不过这个蓝本中所有的路由都需要使用相同的方式进行保护。我们可以在@api.before_request装饰器中使用一次,将其 应用到整个蓝本。

二. 基于令牌的身份验证

       每次请求,客户端都要发送身份验证凭据。为了避免总是发送邮箱,密码等敏感信息,我们可以使用基于令牌的身份验证方案。并且处于安全考虑,令牌有过期时间,令牌过期后,客户端必须重新发送登录凭据,获取新的令牌。为了生成和核查身份验证令牌,我们要在User模型中定义2个新方法。

app/models.py:支持基于令牌的身份验证

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from itsdangerous import BadSignature, SignatureExpired

class User(UserMinxin, db.Model):
    ...
    def generate_auth_token(self, expiration):
        s = Serializer(current_app.config['SECRET_KEY'], expires_in=expiration)
        return s.dumps({'id': self.id}).decode('utf-8')

    @staticmethod
    def verify_auth_token(token):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token)
        except(SignatureExpired, BadSignature):
            return None
        return User.query.get(data['id'])

verify_auth_token是静态方法,因为只有解码令牌后才知道用户是谁。特别注意,即使不知道SECRET_KEY借助工具还是可以获取到令牌中的信息,所以令牌中不能包含敏感信息。

为了能够使用令牌验证请求,需要修改verify_password()回调,既可以接收用户名、密码,也可以接收令牌:

@auth.verify_password
def verify_password(email_or_token, password):
    """即支持密码验证,也支持token验证"""
    if email_or_token == '':
        return False
    if password == '':
        g.current_user = User.verify_auth_token(email_or_token)
        g.token_used = True
        return g.current_user is not None
    user = User.query.filter_by(email=email_or_token).first()
    if not user:
        return False
    g.current_user = user
    g.token_used = False
    return user.verify_password(password)

       当password为空字符串时,说明客户端只发送了令牌。我们还添加了g.token_used全局上下文变量,以便在视图函数中区分两中身份验证方法。下面我们定义用户获取令牌的视图函数:

@api.route('/token', methods=['POST'])
def get_token():
    """客户端申请令牌时,必须使用邮箱验证"""
   if g.current_user.is_anonymous or g.token_used:
        return unauthorized('Invalid credentials')
    return jsonify({'token': g.current_user.generate_auth_token(expiration=3600), 'expiration': 3600})

 在获取令牌时,检查了g.token_used值,拒绝使用令牌申请令牌,防止用户绕过令牌过期机制,使用旧令牌请求新令牌。

使用postman测试get_token视图:

postman生成的请求头:

正常响应:

使用token申请令牌:

请求头:

返回401未授权回应:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值