14 使用Flask提供REST Web服务

一. 创建API蓝本

  REST API相关的路由是一个自成一体的程序子集, 所以为了更好的组织代码, 我们最好把这些路由放到独立的蓝本中。

1)API蓝本的结构

|-flasky

  |-app/

    |-api_1_0

      |-__init__.py

      |-users.py

      |-posts.py

      |-comments.py

      |-authentication.py

      |-errors.py

      |-decorators.py

这个API蓝本中, 各资源分别在不同的模块中实现。 蓝本还包含处理认证, 错误以及提供自定义修饰器的模块。

2)蓝本的构造文件__init__.py

from flask import Blueprint

api = Blueprint('api', __name__)

from . import users, posts, comments, authentication, errors

3)注册API蓝本app/__init__.py

def create_app(config_name):

    #...

    from .api_1_0 import api as api_1_0_blueprint

    app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1.0')

    #...

二. 错误处理

1)客户端能从web服务得到的常见状态码如下表所示:

HTTP状态码名称说明
200OK(成功)请求成功完成
201Created(创建)请求成功完成并创建了一个新资源
400Bad Request(坏请求)请求不可用或不一致
401Unauthorized(未授权)请求未包含认证信息
403Forbidden(禁止)请求中发送的认证密令无权访问目标
404Notfound(未找到)URL对应的资源不存在
405Method not allowed(不允许使用的方法)指定资源不支持请求使用的方法
500Interval server error(内部服务器错误)处理请求的过程中发生意外错误

  为所有客户端生成相应的一种方法是, 在错误处理程序中根据客户端请求的格式改写相应, 这种技术称为内容协商。

2)改进后的404错误处理程序

@main.app_errorhandler(404)

def page_not_found(e):

    if request.accept_mimetypes.accept_json and \

            not request.accept_mimetypes.accept_html: #如果客户端只接受json相应

        response = jsonify({'error': 'not found'})

        response.code_status = 404

        return response

    return render_template('404.html'), 404
app_error_handler作用在全局, 所有路由中发生404错误就会由该错误处理程序处理。

3)API蓝本中的错误处理程序app/api_1_0/errors.py

def forbidden(message):
    response = jsonify({'error': 'forbidden', 'message': message})
    response.status_code = 403
    return response


def unauthorized(message):
    response = jsonify({'error': 'unauthorized', 'message': message})
    response.status_code = 401
    return response


def bad_request(message):
    response = jsonify({'error': 'bad request', 'message': message})
    response.status_code = 400
    return response

@api.errorhandler(ValidationError)#如果在api蓝本注册的路由中遇到ValidaitonError, 由改程序处理错误
def validation_error(e):
    return bad_request(e.args[0])

这样视图函数就可以调用这些辅助函数生成错误响应了

4)ValidationError自定义错误app/exceptions.py

class ValidationError(ValueError):

    pass

三. 使用Flask-HTTPAuth认证用户

  我们的认证支持匿名用户, 邮箱密码认证, token令牌认证;

1)安装flask-httpauth

&pip install flask-httpauth

2)初始化Flask-HTTPAuth app/api_1_0/authentication.py

from flask_httpauth import HTTPBasicAuth


auth = HTTPBasicAuth()


@auth.verify_password

def verify_password(email_or_token, password):

    if email_or_token == '':

        g.current_user = AnonymousUser()

        return True

    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.token_used = False

    g.current_user = user

    return user.verify_password(password)


@auth.error_handler

def auth_error():

    return unauthorized('Invalid credentials')


#使用修饰器保护路由
@api.route('/posts/')
@auth.login_required
def get_posts():
    pass

"""
上面的代码是否有些不好理解? 没关系, 我来介绍一下它的作用。
部署了上面的代码以后, 我们就可以使用修饰器@auth.login_reqired保护路由了,
只要客户端向该修饰器修饰的路由发出请求, 浏览器就会弹出一个对话框要求填写邮箱和密码, 即需要认证
如果填写的信息通过认证, 则可以访问路由, 未通过认证不可访问路由。
"""

#...email-password认证是默认的, token认证是我们自己添加的, 因为不想每次都发送敏感信息
#为了实现token认证我们还需要实现生成token, 验证token, 获取token的函数

#在那之前, 我们先处理一个问题, 在每个路由上都添加@auth.login_required修饰器稍显麻烦, 
#我们可以在before_request函数上添加该修饰器, 那样该修饰器就会应用到所有api路由上了

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

#我们访问任意api蓝本注册的路由, 就会先访问@api.before_request修饰器注册的before_request函数
#因为before_request函数被@auth.login_required修饰器修饰, 所以我们就需要通过认证
#认证通过后才可以进入before_request函数, 执行完before_request函数后自动执行我们想访问的路由
#不过用户要是匿名用户或者是confirm属性为True的用户才可以

3)生成, 验证token app/models.py

class User(UserMixin, db.Model):

    #...

    def generate_auth_token(self, expiration):

        s = Serializer(current_app.config['FLASKY_SECRET_KEY'], expires_in=expiration)

        return s.dumps({'id': self.id})

   

    @staticmethod

    def verify_auth_token(token):  #验证失败返回None, 验证成功返回对应用户

        s = Serializer(current_app.config['FLASKY_SECRET_KEY'])

        try:

            data = s.loads(token)

        except:

            return None

        return User.query.get(data['id'])

4)发送token app/api_1_0/authentications.py

@api.route('/token')

def get_token(): #能通过认证才能访问该路由, 有三种方式通过认证: 匿名用户, token认证, email-password认证

    if g.current_user.is_anonymous or g.token_used == True:  #匿名用户无法获取token, 无法用旧token获取新token

        return unauthorized('Invalid credentials')

    return jsonify({'token': g.current_user.generate_auth_token(expiration=3600), 'expiration': 3600}) 

四. 资源和json的转换

1)post&json_post app/models.py

class Post(db.Model):

    #...

    def to_json(self):

        json_post = {

            'url': url_for('api.get_post', id=self.id, _external=True),

            'body': self.body,

            'body_html': self.body_html,

            'timestamp': self.timestamp,

            'author': url_for('api.get_user', id=self.author_id, _external=True),

            'comments': url_for('api.get_post_comments', id=self.id, _external=True),

            'comment_count': self.comments.count()

        }

        return json_post


    @staticmethod

    def from_json(json_post):

        body = json_post.get('body')

        if body == None or body == '':

            raise ValidationError('post does not have a body')

        return Post(body=body)

"""
我们创建Post实例的时候只需要使用body字段,post的author属性在视图函数中定义。
"""

#http客户端和web服务之间传递信息使用json编码, 所以web服务要实现json和资源之间的相互转换
#把post请求发来的json转换成资源存入数据库
#把get请求请求的资源转换成json发送给客户端

2)user&json_user app/models.py

class User(UserMixin, db.Model):

    def to_json(self):

        json_user = {
                'url': url_for('api.get_user', id=self.id, _external=True),
                'username': self.username,
                'member_since': self.member_since,
                'last_seen': self.last_seen,
                'posts': url_for('api.get_user_posts', id=self.id, _external=True),
                'followed_posts': url_for('api.get_user_followed_posts', id=self.id, _external=True),
                'post_count': self.posts.count()
                }
        return json_user
#为了保护用户隐私, 我们没有把email, password, role等属性放入json字典
#这说明我们把资源转成json时没必要把所有属性都放进去
#user并没有post_count属性, 这说明我们可以在json里面增加虚拟属性

3)comment&json_comment app/models.py

class Comment(db.Model):

    #...

    def to_json(self):
        json_comment = {
                'url': url_for('api.get_comment', id=self.id, _external=True),
                'body': self.body,
                'body_html': self.body_html,
                'timestamp': self.timestamp,
                'author_url': url_for('api.get_user', id=self.author.id, _external=True),
                'post_url': url_for('api.get_post', id=self.post.id, _external=True)
                }
        return json_comment

    @staticmethod
    def from_json(json_comment):
        body = json_comment.get('body')
        if body == None or body == '':
            raise ValidationError('comment does not have a body')
        return Comment(body)

五. 实现资源端点

1)app/api_1_0/posts.py

from ..models import Post, Permission
from flask import request, g, jsonify, url_for, current_app
from . import api
from .. import db
from .errors import forbidden
from .decorators import permission_required


@api.route('/posts/')
def get_posts():
    page = request.args.get('page', 1, type=int)
    pagination = Post.query.paginate(page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], error_out=False)
    posts = pagination.items
    prev = None
    if pagination.has_prev:
        prev = url_for('api.get_posts', page=page-1, _external=True)
    next = None
    if pagination.has_next:
        next = url_for('api.get_posts', page=page+1, _external=True)
    return jsonify({
        'posts': [post.to_json() for post in posts],
        'prev': prev,
        'next': next,
        'count': pagination.total
        })


@api.route('/posts/<int:id>')
def get_post(id):
    post = Post.query.get_or_404(id)
    return jsonify(post.to_json())


@api.route('/posts/', methods=['POST'])
@permission_required(Permission.WRITE_ARTICLES) #创建文章的用户需要写权限, 该修饰函数定义在文章后面
def new_post():
    post = Post.from_json(request.json) #利用请求发送的json数据创建post实例, 如果遇到ValidationError, 由errorhandler处理,视图函数变得简洁
    post.author = g.current_user  
    db.session.add(post)
    db.session.commit()
    return jsonify(post.to_json()), 201, \
            {'location': url_for('api.get_post', id=post.id, _external=True)}


@api.route('/posts/<id>', methods=['PUT'])
@permission_required(Permission.WRITE_ARTICLES)
def edit_post(id):
    post = Post.query.get_or_404(id)
    if g.current_user != post.author and \ #通过认证的用户不是文章的作者, 并且不是管理员
            not g.current_user.can(Permission.ADMINISTER):
                return forbidden('Insufficient permissions')
    post.body = request.json.get('body', post.body)
    db.session.add(post)
    return jsonify(post.to_json())

2)app/api_1_0/users.py

from . import api
from ..models import User
from flask import jsonify, request, current_app, url_for


@api.route('/users/<int:id>')
def get_user(id):
    user = User.query.get_or_404(id)
    return jsonify(user.to_json())


@api.route('/users/<int:id>/posts/')
def get_user_posts(id):
    user = User.query.get_or_404(id)
    page = request.args.get('page', 1, type=int)
    pagination = user.posts.paginate(page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], error_out=False)
    posts = pagination.items
    prev = None
    if pagination.has_prev:
        prev = url_for('api.get_user.posts', id=id, page=page-1)
    next = None
    if pagination.has_next:
        next = url_for('api.get_user_posts', id=id, page=page+1)
    return jsonify({
        'posts': [post.to_json() for post in posts],
        'prev': prev,
        'next': next,
        'count': user.posts.count()
        })

@api.route('/users/<int:id>/timeline/')
def get_user_followed_posts(id):
    user = User.query.get_or_404(id)
    page = request.args.get('page', 1, type=int)
    pagination = user.followed_posts.paginate(page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], error_out=False)
    posts = pagination.items
    prev = None
    if pagination.has_prev:
        prev = url_for('api.get_user_followed_posts', id=id, page=page-1)
    next = None
    if pagination.has_next:
        next = url_for('api.get_user_followed_posts', id=id, page=page+1)
    return jsonify({
        'posts': [post.to_json() for post in posts],
        'prev': prev,
        'next': next,
        'count': user.followed_posts.count()
        })

3)app/api_1_0/comments.py

from . import api
from flask import request, current_app, jsonify, url_for, g
from ..models import Comment, Permission, Post
from .decorators import permission_required
from .. import db


@api.route('/comments/')
def get_comments():
    page = request.args.get('page', 1, type=int)
    pagination = Comment.query.order_by(Comment.timestamp.desc()).paginate(page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'], error_out=False)
    comments = pagination.items
    prev = None
    if pagination.has_prev:
        prev = url_for('api.get_comments', page=page-1)
    next = None
    if pagination.has_next:
        next = url_for('api.get_comments', page=page+1)
    return jsonify({
        'comments': [comment.to_json() for comment in comments],
        'prev': prev,
        'next': next,
        'count': pagination.total()
        })


@api.route('/comments/<int:id>')
def get_comment(id):
    comment = Comment.query.get_or_404(id)
    return jsonify(comment.to_json())


@api.route('/posts/<int:id>/comments/')
def get_post_comments(id):
    post = Post.query.get_or_404(id)
    page = request.args.get('page', 1, type=int)
    pagination = post.comments.order_by(Comment.timstamp.desc()).paginate(page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'], error_out=False)
    comments = pagination.items
    prev = None
    if pagination.has_prev:
        prev = url_for('api.get_post.comments', id=id, page=page-1)
    next = None
    if pagination.has_next:
        next = url_for('api.get_post_comments', id=id, page=page+1)
    return jsonify({
        'comments': [comment.to_josn() for comment in comments],
        'prev': prev,
        'next': next,
        'count': pagination.total
        })


@api.route('/posts/<int:id>/comments/', methods=['POST'])
@permission_required(Permission.COMMIT)
def new_post_comment(id):
    post = Post.query.get_or_404(id)
    comment = Comment.from_json(request.json)
    comment.author = g.current_user
    comment.post = post
    db.session.add(comment)
    db.session.commit()
    return jsonify(comment.to_json), 201, \
            {'location': url_for('api.get_comment', id=comment.id, _external=True)}

六. permission_required修饰器 app/api_1_0/decorations.py

还记得我们是如何利用该修饰器修饰路由, 以达到限制权限用户才能访问路由的目的的吗?

@api.route('/posts/', methods=['POST'])
@permission_required(Permission.WRITE_ARTICLES) #只有具有写权限的用户才能访问该路由
def new_post():
    pass

我们来推理一下permission_required函数的构造, 首先修饰器语句等价于:

new_post = permission_required(Permission.WRITE_ARTICLES)(new_post)

即:

new_post() = permission_required(Permission.WRITE_ARTICLES)(new_post)()

当用户访问该路由时首先需要通过认证, 通过认证以后g.current_user中存储的就是通过认证的用户, 然后如果用户是匿名用户或者confirm属性为True, 通过before_request函数, 即可访问路由函数new_post()

就相当于访问:

permission_required(Permission.WRITE_ARTICLES)(new_post)()

这样的话看来是两层闭包, 通过两层调用把Permission.WRITE_ARTICLES和new_post参数传到最里面的函数, 最里面的函数进行用户权限认证, 通过的话执行new_post函数即可:

def permission_required(permission):
    
    def decorator(f):
        
      @wraps(f)
 def decorator_function(): if not g.current_user.can(permission): return forbidden('Insufficient permissions') return f() return decorator_function return decorator

如果被修饰的函数有参数的话, 版本就是下面这样:(有无参数都适用)

def permission_required(permission):
    
    def decorator(f):
        @wraps(f)
        def decorator_function(*nkwargs, **kwargs):

            if not g.current_user.can(permission):

                return forbidden('Insufficient permissions')

            return f(*nkwargs, **kwargs)

        return decorator_function

    return decorator

@wraps修饰器的作用是, 保持被修饰函数的__doc__和__name__属性。


七. 使用HTTPie测试Web服务

    测试Web服务必须使用HTTP客户端, 最常使用的在命令行中测试Web服务的客户端是curl, httpie, 后者命令更简洁, 可读性也更高, 我们使用后者。

1)安装httpie

&pip install httpie

2)运行web服务

3)测试web服务

GET请求可按如下方式发起:

...


因为这是第一页, 所以prev参数为None, 但是返回了获取下一页的url和总页数。


匿名用户可发送空邮件地址和密码来发起相同的请求:


下面这个命令发送POST请求以添加一篇新博客文章:

如此推测, request.json就是字典:

{'body': "I'm adding a post2 from the *command line*."}

而且视图函数返回语句:

    return jsonify(post.to_json()), 201, \
            {'location': url_for('api.get_post', id=post.id, _external=True)}

201是status_code,

{'location': url_for('api.get_post', id=post.id, _external=True)}是location首部。


要想使用认证令牌, 可向/api/v1.0/token 发送请求:


在接下来的1小时内, 这个令牌可用于访问API, 请求时要和空密码一起发送:


至此, Flasky的功能开发阶段就完全结束啦~~~

很显然, 下一步我们要部署Flasky。 在部署过程中, 我们会遇到新的挑战~











       


   












       




  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值