基於Flask開發企業級REST API應用(二)

關於我
編程界的一名小小程序猿,目前在一個創業團隊任team lead,技術棧涉及Android、Python、Java和Go,這個也是咱們團隊的主要技術棧。
Github:github.com/hylinux1024
微信公衆號:angrycodemysql

本節開始項目的編碼實現。首先咱們來實現登陸註冊模塊的相關API。本項目咱們是使用先後端分離的模式,在實現登陸註冊功能以前,假設咱們的接口是開放的,那麼須要肯定接口校驗方案linux

0x00 接口校驗方案

咱們的目標是接口不能被抓包重複訪問,而且要對客戶端的可靠性進行驗證。git

  • 防重放攻擊可以使用參數有timestampnoncetokensign
  • 支持可信任的客戶端的請求則能夠考慮添加appkeyappsecret參數
公共參數

一、timestamp 時間戳github

單位毫秒,也能夠是秒,與服務端保持一致;
時間戳是無關時區的,因此客戶端與服務端的時間戳是能夠用來比較的;
若是客戶端與服務端時間戳相差比較大,則能夠考慮使用服務端時間進行校準;
時間戳的做用是,保證這個請求在必定時間內(例如60秒內)是有效的。有效期內的校驗就須要nonce參數redis

二、nonce 隨機數sql

由客戶端產生的隨機數,客戶端每次接口請求時須要保證它是不同的。
nonce的做用是保證timestamp有效期內的請求是不是合法的。
服務端接收到這個參數後,會將其保存在某個集合中。 服務端會檢測這個nonce是否在該集合中出現過,若是出現過說明該請求是不合法的。
每一個nonce的有效期設置跟timestamp參數有關,例如能夠設置爲60秒。數據庫

三、token 登陸態編程

須要登陸的接口則須要一個token參數。 服務端生成的token在有效期內有效,若是token過時則須要提示客戶端從新登陸。 token的生成規則可以使用隨機數json

token = md5(1024位的隨機數)
複製代碼

四、sign 簽名或校驗參數flask

msg = 除了timestamp、nonce、token、sign參數以外的其它排序後的參數列表和值列表 = sort(參數1=值1&參數2=值2&參數3=值3...)

sign = md5(msg+token+timestamp+nonce+salt) 

salt = 客戶端與服務端約定字符串
複製代碼

五、appkeyappsecret

服務端爲可信任的客戶端分配appkeyappsecret參數。可由隨機數或自定義的規則生成,要保證appkeyappsecret是對應的。
客戶端需保證appsecret不被泄露。
客戶端接口請求時只需帶上appkey參數。appsecret則添加到sign校驗參數的計算中

sign = md5(token+msg+timestamp+nonce+appsecret)
複製代碼

結合上面的參數,一個接口請求應該相似這樣

http://api.example.com/v1/login?phone=13499990000&timestamp=1564486841415&nonce=34C2AF&sign=e10adc3949ba59abbe56e057f20f883e&appkey=A23CE80D
複製代碼

服務端程序接收到請求後驗證流程應該是這樣的

  1. 經過appkey查詢到appsecret,若是查不到則返回出錯信息,不然繼續;
  2. 經過timestamp檢查nonce是否在有效時間內是的重複請求,若是是屢次重複請求,則返回出錯信息,不然繼續;
  3. 經過請求參數構造msg並計算sign,將此參數與請求中獲取到的參數進行對比,驗證成功後纔開始咱們的業務邏輯。

這樣咱們的一個簡單實用的接口驗證方案就出來了,固然可能還有其它一些好的想法,歡迎留言一塊兒探討學習。

0x01 show me the code

如今開始實現登陸註冊功能,相信這個模塊走通了,以後其它模塊也是依樣畫葫蘆。

先看下模塊

├── api
│   ├── __init__.py
│   └── auth.py
├── app.py
├── config.ini
├── datingtoday.sql
├── models.py
├── requirements.txt
├── test
└── venv
複製代碼

增長了一個api相關的文件包。 還有一個config.ini,主要用於配置數據庫等信息,而models.py文件是定義實體類的地方。

api/__init__.py
from flask import jsonify

def make_response_ok(data=None):
    resp = {'code': 0, 'msg': 'success'}
    if data:
        resp['data'] = data
    return jsonify(resp)

def make_response_error(code, msg):
    resp = {'code': code, 'msg': msg}
    return jsonify(resp)

def validsign(func):
    """ 驗證簽名 :param func: :return: """

    def decorator():
        params = request.form
        appkey = params.get('appkey')
        sign = params.get('sign')
        csign = signature(params)
        if not appkey:
            return make_response_error(300, 'appkey is none.')
        if csign != sign:
            return make_response_error(500, 'signature is error.')
        return func()

    return decorator
複製代碼

__init__.py中首先定義了兩個封裝統一的json數據結構的的方法,主要是用到flask中的jsonify函數,它能夠把一個對象轉成json

在前面咱們講了接口的驗證邏輯,這一部分對參數的校驗功能實際上是能夠通用的,因此對這個邏輯也進行了封裝成validsign方法。

不錯,這是一個裝飾器的定義。咱們但願在接口訪問的方法使用裝飾器,就能夠進行通用的接口校驗。

auth.py

這一節的重點是實現登陸註冊和發短信接口,所以建立一個 auth.py的文件來寫跟受權登陸相關的接口,這樣有利於咱們組織代碼。
咱們知道要實現接口的訪問路徑的定義與方法直接的對應,是使用@route這個裝飾器。這裏咱們在一個新文件中定義咱們的接口,就須要用到Blueprint

A blueprint is an object that allows defining application functions without requiring an application object ahead of time. It uses the same decorators as Flask, but defers the need for an application by recording them for later registration.

說白了,它的做用跟@route差很少。

因爲咱們把登陸註冊看成一個接口來實現,即用戶經過短信進行登陸,後端會判斷該用戶是否爲新用戶,若是是新用戶則自動註冊。

0x02 短信接口

首先定義接口的訪問路徑爲

{host:port}/api/auth/sendsms

請求方法:POST
參數:phone
請求成功
{
    "code": 0,
    "data": {
        "code": "97532",
        "phone": "18922986865"
    },
    "msg": "success"
}
複製代碼

根據接口定義咱們會在auth.py中定義一個Blueprint對象用來映射咱們的訪問路徑和方法。

bp = Blueprint("auth", __name__, url_prefix='/api/auth')
複製代碼

短信接口的實現這裏會使用到redis,將請求到的短信驗證碼保存在redis中,並設置過時時間。而後登陸時,再進行驗證。

@bp.route("/sendsms", methods=['POST'], endpoint="sendsms")
@validsign
def send_sms():
    phone = request.form.get('phone')
    m = re.match(pattern_phone, phone)
    if not m:
        return make_response_error(300, 'phone number format error.')
    # 這裏須要修改成對接短信服務
    code = '97532'
    key = f'{phone}-{code}'
    r.set(key, code, 60)
    return make_response_ok({'phone': phone, 'code': code})
複製代碼

注意這裏的endpoint="sendsms"是必需設置,由於@validsign會修飾咱們的方法,每一個方法都是用一個通用的校驗,方法名稱會變成同樣的,因此若是不設置endpoint會致使url映射失敗。

0x03 登陸註冊接口

首先定義接口的訪問路徑爲

{host:port}/api/auth/login

請求方法:POST
參數:phone
參數:code
請求成功
{
    "code": 0,
    "data": {
        "expire_time": "2019-08-10 07:34:20",
        "token": "5bea89727e7553284f162d35c9926414",
        "user_id": 100784
    },
    "msg": "success"
}
複製代碼

執行登陸接口時,會先驗證redis中的驗證碼,而後查一下受權表user_auth看看是不是新用戶,最後返回用戶的登陸受權信息。

@bp.route("/login", methods=['POST'], endpoint='login')
@validsign
def login():
    phone = request.form.get('phone')
    code = request.form.get('code')
    key = f'{phone}-{code}'
    sms_code = r.get(key)
    if sms_code:
        sms_code = sms_code.decode()
    if code != sms_code:
        return make_response_error(503, 'sms code error')
    auth_info = UserAuth.query.filter_by(open_id=phone).first()
    if not auth_info:
        auth_info = register_by_phone(phone)
    else:
        auth_info = login_by_phone(auth_info)

    data = {'token': auth_info.token,
            'expired_time': auth_info.expired_time.strftime("%Y-%m-%d %H:%M:%S"),
            'user_id': auth_info.user_basic.id}

    r.set(f'auth_info_{auth_info.user_id}', str(data))
    return make_response_ok(data)
複製代碼

整體上邏輯仍是比較清晰的,最後咱們看一下app.py

from flask import Flask

from api import auth, config
from models import db

app = Flask(__name__)
# 將blueprint註冊到app中
app.register_blueprint(auth.bp)
# 配置app的config,將數據庫信息配置好
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config["SQLALCHEMY_DATABASE_URI"] = config['DATABASE']['uri']
# 最好生成一個secret_key
app.secret_key = '8c2c0b555e6e6cb01a5fd36dd981bcee'

db.init_app(app)

@app.route('/')
def hello_world():
    return 'Hello World!'


if __name__ == '__main__':
    app.run()
複製代碼

配置文件config.ini

# 配置數據庫連接
[DATABASE]
uri = mysql+pymysql://user:password@127.0.0.1:3306/datingtoday

# 配置appkey和secret
[APP]
appkey = 432ABZ
appsecret = 1AE32B09224
複製代碼

0x04 單元測試

因爲接口都須要動態計算校驗碼,因此單元測試是必需的。這裏我使用最簡單的方式,直接使用unittest模塊。

例如測試發短信的業務接口,首先生成一個隨機數nonce,而後計算校驗碼sign參數,最後調用flask中的post方法模擬接口請求。

def test_sendsms(self):
    import math
    nonce = math.floor(random.uniform(100000, 1000000))
    params = {'phone': '18922986865', 'appkey': '432ABZ', 'timestamp': datetime.now().timestamp(),
              'nonce': nonce}
    sign = signature(params)
    params['sign'] = sign

    respdata = self.app.post("/api/auth/sendsms", data=params)
    resp = respdata.json
    self.assertEqual(resp['code'], 0, respdata.data)
複製代碼

若是請求成功,就認爲經過測試。固然這裏的邏輯仍是比較簡單,但願小夥伴們留言討論。

0x05 項目地址

源碼地址:
github.com/hylinux1024…

Flask官方地址:
palletsprojects.com/p/flask/

注意本文會使用到mysqlredis數據庫,須要自行安裝。

相關文章
相關標籤/搜索