用 Flask 來寫個輕博客 (35) — 使用 Flask-RESTful 來構建 RESTful API 之四

目錄

前文列表

用 Flask 來寫個輕博客 (1) — 建立項目
用 Flask 來寫個輕博客 (2) — Hello World!
用 Flask 來寫個輕博客 (3) — (M)VC_鏈接 MySQL 和 SQLAlchemy
用 Flask 來寫個輕博客 (4) — (M)VC_建立數據模型和表
用 Flask 來寫個輕博客 (5) — (M)VC_SQLAlchemy 的 CRUD 詳解
用 Flask 來寫個輕博客 (6) — (M)VC_models 的關係(one to many)
用 Flask 來寫個輕博客 (7) — (M)VC_models 的關係(many to many)
用 Flask 來寫個輕博客 (8) — (M)VC_Alembic 管理數據庫結構的升級和降級
用 Flask 來寫個輕博客 (9) — M(V)C_Jinja 語法基礎快速概覽
用 Flask 來寫個輕博客 (10) — M(V)C_Jinja 經常使用過濾器與 Flask 特殊變量及方法
用 Flask 來寫個輕博客 (11) — M(V)C_建立視圖函數
用 Flask 來寫個輕博客 (12) — M(V)C_編寫和繼承 Jinja 模板
用 Flask 來寫個輕博客 (13) — M(V)C_WTForms 服務端表單檢驗
用 Flask 來寫個輕博客 (14) — M(V)C_實現項目首頁的模板
用 Flask 來寫個輕博客 (15) — M(V)C_實現博文頁面評論表單
用 Flask 來寫個輕博客 (16) — MV(C)_Flask Blueprint 藍圖
用 Flask 來寫個輕博客 (17) — MV(C)_應用藍圖來重構項目
用 Flask 來寫個輕博客 (18) — 使用工廠模式來生成應用對象
用 Flask 來寫個輕博客 (19) — 以 Bcrypt 密文存儲帳戶信息與實現用戶登錄表單
用 Flask 來寫個輕博客 (20) — 實現註冊表單與應用 reCAPTCHA 來實現驗證碼
用 Flask 來寫個輕博客 (21) — 結合 reCAPTCHA 驗證碼實現用戶註冊與登陸
用 Flask 來寫個輕博客 (22) — 實現博客文章的添加和編輯頁面
用 Flask 來寫個輕博客 (23) — 應用 OAuth 來實現 Facebook 第三方登陸
用 Flask 來寫個輕博客 (24) — 使用 Flask-Login 來保護應用安全
用 Flask 來寫個輕博客 (25) — 使用 Flask-Principal 實現角色權限功能
用 Flask 來寫個輕博客 (26) — 使用 Flask-Celery-Helper 實現異步任務
用 Flask 來寫個輕博客 (27) — 使用 Flask-Cache 實現網頁緩存加速
用 Flask 來寫個輕博客 (29) — 使用 Flask-Admin 實現後臺管理 SQLAlchemy
用 Flask 來寫個輕博客 (30) — 使用 Flask-Admin 加強文章管理功能
用 Flask 來寫個輕博客 (31) — 使用 Flask-Admin 實現 FileSystem 管理
用 Flask 來寫個輕博客 (32) — 使用 Flask-RESTful 來構建 RESTful API 之一
用 Flask 來寫個輕博客 (33) — 使用 Flask-RESTful 來構建 RESTful API 之二
用 Flask 來寫個輕博客 (34) — 使用 Flask-RESTful 來構建 RESTful API 之三 css

POST 請求

前三篇博文介紹了若是實現 GET 請求, 接下來繼續實現 POST 建立數據請求.python

  • 執行 Create 操做, 就確定少不了要向服務端傳入數據. 因此第一步固然就是定義解析器了.
    vim jmilkfansblog/controllers/flask_restful/parsers.py
from flask.ext.restful import reqparse

post_post_parser = reqparse.RequestParser()

post_post_parser.add_argument(
    'title',
    type=str,
    required=True,
    help='Title is required!')

post_post_parser.add_argument(
    'text',
    type=str,
    required=True,
    help='Text is required!')

post_post_parser.add_argument(
    'tags',
    type=str,
    action='append')

post_post_parser.add_argument(
    'token',
    type=str,
    required=True,
    help='Auth Token is required to create posts.')

NOTE 1: add_argument 的關鍵字參數 action='append' 指定了傳入的參數會轉換爲以字典爲元素的列表數據類型. 這是爲了便於建立 post.tags 對象.mysql

NOTE 2: 定義 token 參數是爲了後期的身份認證作準備sql

  • 在資源來 PostApi 中實現 post()方法
    vim jmilkfansblog/controllers/flask_restful/posts.py
class PostApi(Resource):
    """Restful API of posts resource."""
    ...
        def post(self, post_id=None):
        """Can be execute when receive HTTP Method `POST`. """

        if post_id:
            abort(400)
        else:
            args = parsers.post_post_parser.parse_args(strict=True)

            new_post = Post()
            new_post.title = args['title']
            new_post.date = datetime.datetime.now()
            new_post.text = args['text']
            new_post.user = user

            if args['tags']:
                for item in args['tags']:
                    tag = Tag.query.filter_by(name=item).first()
                    # If the tag already exist, append.
                    if tag:
                        new_post.tags.append(tag)
                    # If the tag not exist, create the new one.
                    # Will be write into DB with session do.
                    else:
                        new_tag = Tag()
                        new_tag.name = item
                        new_post.tags.append(new_tag)
        db.session.add(new_post)
        db.session.commit()
        return (new_post.id, 201)

NOTE 1: post() 返回了一個 Tuple 類型對象, 第二個元素會做爲 Response Hander 中的 HTTP status_int 狀態碼.數據庫

身份認證

須要注意的是, 對外開發的 RESTful API 必定要很是注重安全, 全部從外部對數據庫的寫入操做請求都必須進行身份認證.flask

身份認證的功能咱們仍然能夠由 Flask-Login 來支持, 但很明顯的, 這並不符合 REST 的無狀態約束. 因此咱們在這裏引入 Token 的概念, 外部請求若是但願經過 RESTful API 執行寫數據庫操做時, 必須攜帶用戶登陸信息, 經過身份認證以後, 再由服務端發放一段時間內有效的 Token. 身份認證在整個項目中也應看成爲一種資源來定義.vim

  • 首先仍是定義 auth 解析器來接受用戶信息
    vim jmilkfansblog/controllers/flask_restful/parsers.py
########################################################
# User's HTTP Request Parser
########################################################

user_post_parser = reqparse.RequestParser()

user_post_parser.add_argument(
    'username',
    type=str,
    required=True,
    help='Username is required!')

user_post_parser.add_argument(
    'password',
    type=str,
    required=True,
    help='Password is required!')

NOTE : auth 的解析器只須要用戶名和密碼兩個參數.api

  • 建立認證資源 auth 的資源類
    vim jmilkfansblog/controllers/flask_restful/auth.py
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer

from flask import abort, current_app
from flask.ext.restful import Resource

from jmilkfansblog.controllers.flask_restful import parsers
from jmilkfansblog.db.sqlalchemy.models import User


class AuthApi(Resource):
    """Restful api of Auth."""

    def post(self):
        """Can be execute when receive HTTP Method `POST`."""

        args = parsers.user_post_parser.parse_args()
        user = User.query.filter_by(username=args['username']).first()

        # Check the args['password'] whether as same as user.password.
        if user.check_password(args['password']):
            # serializer object will be saved the token period of time.
            serializer = Serializer(
                current_app.config['SECRET_KEY'],
                expires_in=600)
            return {'token': serializer.dumps({'id': user.id})}
        else:
            abort(401)

NOTE 1: Token 使用 Python 內建的 itsdangerous 庫來實現, itsdangerous.TimedJSONWebSignatureSerializer() 的第一個參數須要傳入 app 對象的私鑰, 該私鑰在以前的 Flask-WTForm 已經定義在 config.py 中了. 第二個參數定義了 Token 的有效時間.緩存

NOTE 2: 咱們使用 post() 方法來實現用戶身份驗證和發放 Token安全

  • 定義資源 auth 的路由
    vim jmilkfansblog/__init__.py
def create_app(object_name):
    ...
    restful_api.add_resource(
        AuthApi,
        '/api/auth',
        endpoint='restful_api_auth')
    restful_api.init_app(app)
  • 爲 User 對象實現 Token 驗證方法
    須要注意的是: 這個 Token 應該是有時限的, 若是永久生效則會很是危險. 因此咱們還須要對有時限的 Token 進行校驗, 肯定其沒有失效.
    vim jmilkfansblog/models.py
...

class User(db.Model):
    """Represents Proected users."""
    ...
    @staticmethod
    @cache.memoize(60)
    def verify_auth_token(token):
        """Validate the token whether is night."""

        serializer = Serializer(
            current_app.config['SECRET_KEY'])
        try:
            # serializer object already has tokens in itself and wait for 
            # compare with token from HTTP Request /api/posts Method `POST`.
            data = serializer.loads(token)
        except SignatureExpired:
            return None
        except BadSignature:
            return None

        user = User.query.filter_by(id=data['id']).first()
        return user

NOTE: 使用 itsdangerous.TimedJSONWebSignatureSerializer.loads() 來進行 Token 驗證, 若是驗證失敗的話, 咱們直接返回 None

  • 如今咱們擁有了生成 Token 和驗證 Token 的支撐, 最後咱們在 PostApi.post() 中加入 Token 驗證機制.

    vim jmilkfansblog/controllers/flask_restful/posts.py

def post(self, post_id=None):
        """Can be execute when receive HTTP Method `POST`. """

        if post_id:
            abort(400)
        else:
            args = parsers.post_post_parser.parse_args(strict=True)

            # Validate the user identity via token(/api/auth POST).
            # Will be create the post(/api/posts POST), if pass with validate token.
            user = User.verify_auth_token(args['token'])
            if not user:
                abort(401)
        ...

測試

  • 不加 Token 的 POST 請求
(env) jmilkfan@JmilkFan-Devstack:/opt/JmilkFan-s-Blog$ curl -d "title=Just Test POST" -d "text=Hello world" -d "tags=Python" http://localhost:8089/api/posts
{
    "message": {
        "token": "Auth Token is required to create posts."
    }
}
  • 建立 Token
(env) jmilkfan@JmilkFan-Devstack:/opt/JmilkFan-s-Blog$ curl -d "username=<username>" -d "password=<password>" http://localhost:8089/api/auth{
    "token": "eyJhbGciOiJIUzI1NiIsImV4cCI6MTQ4MzM1MjkwNSwiaWF0IjoxNDgzMzUyMzA1fQ.eyJpZCI6IjY1Y2I5NzkyLWI4NzYtNDllNy1iMmM1LTQ2NDY4NjI0MTk5ZSJ9.hYpczUEZUalgzutyyIViheBd_jnnCmegvp4sazHIEoA"
}
  • 加 Token 的 POST 請求
(env) jmilkfan@JmilkFan-Devstack:/opt/JmilkFan-s-Blog$ curl -d "title=Just Test" -d "text=Hello" -d "tags=Python" -d "token=eyJhbGciOiJIUzI1NiIsImV4cCI6MTQ4MzM1MjkwNSwiaWF0IjoxNDgzMzUyMzA1fQ.eyJpZCI6IjY1Y2I5NzkyLWI4NzYtNDllNy1iMmM1LTQ2NDY4NjI0MTk5ZSJ9.hYpczUEZUalgzutyyIViheBd_jnnCmegvp4sazHIEoA" http://localhost:8089/api/posts
"1746b650-bcab-436b-82ab-7411e252b576"

數據庫記錄 :

mysql> select * from posts; +--------------------------------------+-----------+-----------------+---------------------+--------------------------------------+
| id | title | text | publish_date | user_id | +--------------------------------------+-----------+-----------------+---------------------+--------------------------------------+
| 1746b650-bcab-436b-82ab-7411e252b576 | Just Test | Hello           | NULL                | 65cb9792-b876-49e7-b2c5-46468624199e |
| 1af8f334-c9ac-4eba-bdca-4dda597aba70 | 333333333 | <p>22222</p>
 | 2016-12-17 22:39:16 | 65cb9792-b876-49e7-b2c5-46468624199e |
| 29bab6a0-6a0f-48f1-a088-6c271cebe906 | 222222    | <p>222222</p>
 | 2016-12-27 22:35:00 | 65cb9792-b876-49e7-b2c5-46468624199e |
| 9c25d00e-49a7-4369-ac83-c0aca046ba73 | Just Test | Hello | NULL | 65cb9792-b876-49e7-b2c5-46468624199e | +--------------------------------------+-----------+-----------------+---------------------+--------------------------------------+
相關文章
相關標籤/搜索