用 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
前三篇博文介紹了若是實現 GET 請求, 接下來繼續實現 POST 建立數據請求.python
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
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
########################################################
# 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
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安全
def create_app(object_name):
...
restful_api.add_resource(
AuthApi,
'/api/auth',
endpoint='restful_api_auth')
restful_api.init_app(app)
...
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)
...
(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."
}
}
(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"
}
(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 | +--------------------------------------+-----------+-----------------+---------------------+--------------------------------------+