day102:MoFang:後端完成對簡訊驗證碼的校驗&基於celery完成非同步簡訊傳送&flask_jwt_extended&使用者登入的API介面

Poke發表於2020-12-03

目錄

1.使用者註冊

  1.後端完成對簡訊驗證碼的校驗

  2.基於celery實現簡訊非同步傳送

2.使用者登入

  1.jwt登入驗證:flask_jwt_extended

  2.服務端提供使用者登入的API介面

1.使用者註冊

1.後端完成對簡訊驗證碼的校驗

application.apps.users.marshmallow,程式碼:

from marshmallow import Schema,fields,validate,validates,ValidationError
from message import ErrorMessage as Message
from .models import User,db
class MobileSchema(Schema):
    ...

from marshmallow_sqlalchemy import SQLAlchemyAutoSchema,auto_field
from marshmallow import post_load,pre_load,validates_schema
from application import redis
class UserSchema(SQLAlchemyAutoSchema):
    ...

    @validates_schema
    def validate(self,data, **kwargs):
        ....

        '''校驗簡訊驗證碼'''
        # 1. 從redis中提取驗證碼
        redis_sms_code = redis.get("sms_%s" % data["mobile"])
        if redis_sms_code is None:
            raise ValidationError(message=Message.sms_code_expired,field_name="sms_code")
        redis_sms_code = redis_sms_code.decode()
        
        #2. 從客戶端提交的資料data中提取驗證碼
        sms_code = data["sms_code"]
        
        #3. 字串比較,如果失敗,則丟擲異常,否則,直接刪除驗證碼
        if sms_code != redis_sms_code:
            raise ValidationError(message=Message.sms_code_error, field_name="sms_code")
        redis.delete("sms_%s" % data["mobile"])

        return data

2.基於celery實現簡訊非同步傳送

1.安裝celery

pip install celery==4.4.0

2.celery主程式檔案:main.py

在專案根目錄下建立mycelery目錄,同時建立celery啟動主程式檔案main.py,程式碼:

from __future__ import absolute_import
from celery import Celery
from application import init_app

# 初始化celery物件
app = Celery("flask")

# 初始化flask 
flask_app = init_app("application.settings.dev").app

# 載入配置
app.config_from_object("mycelery.config")

# 自動註冊任務
app.autodiscover_tasks(["mycelery.sms"])

3.celery配置檔案:config.py

配置檔案,mycelery.config,程式碼:

# 任務佇列地址
broker_url = 'redis://127.0.0.1:6379/15'

# 結果佇列地址
result_backend = "redis://127.0.0.1:6379/14"

4.建立任務模組:sms/tasks.py

在mycelery下建立任務模組包sms,並建立tasks.py任務模組檔案,

同時,在任務執行過程中, 基於監聽器和任務bind屬性對失敗任務進行記錄和重新嘗試執行. 程式碼:

import json
from application import redis
from flask import current_app
from ronglian_sms_sdk import SmsSDK
from mycelery.main import app,flask_app

@app.task(name="send_sms",bind=True)
def send_sms(self,mobile,sms_code):
    """傳送簡訊"""
    try:
        with flask_app.app_context(): 
            sdk = SmsSDK(
                current_app.config.get("SMS_ACCOUNT_ID"),
                current_app.config.get("SMS_ACCOUNT_TOKEN"),
                current_app.config.get("SMS_APP_ID")
            )
            ret = sdk.sendMessage(
                current_app.config.get("SMS_TEMPLATE_ID"),
                mobile,
                (sms_code, current_app.config.get("SMS_EXPIRE_TIME") // 60)
            )
            result = json.loads(ret)

            if result["statusCode"] == "000000":
                pipe = redis.pipeline()
                pipe.multi()  # 開啟事務
                # 儲存簡訊記錄到redis中
                pipe.setex("sms_%s" % mobile, current_app.config.get("SMS_EXPIRE_TIME"), sms_code)
                # 進行冷卻倒數計時
                pipe.setex("int_%s" % mobile, current_app.config.get("SMS_INTERVAL_TIME"), "_")
                
                pipe.execute()  # 提交事務
            else:
                current_app.log.error("簡訊傳送失敗!\r\n%s" % ret)
                raise Exception
    except Exception as exc:
        # 重新嘗試執行失敗任務
        print(self.request.retries) # 本次執行的次數
        self.retry(exc=exc, countdown=3, max_retries=5)

"""基於監聽器完成任務監聽"""
from celery.app.task import Task
class SMSTask(Task):
    def on_success(self, retval, task_id, args, kwargs):
        print( '任務執行成功!')
        return super().on_success(retval, task_id, args, kwargs)

    def on_failure(self, exc, task_id, args, kwargs, einfo):
        print('任務執行失敗!%s' % self.request.retries)
        # 重新嘗試執行失敗任務,時間間隔:3秒,最大嘗試次數:5次
        self.retry(exc=exc, countdown=3, max_retries=5)
        return super().on_failure(exc, task_id, args, kwargs, einfo)

    def after_return(self, status, retval, task_id, args, kwargs, einfo):
        print('this is after return')
        return super().after_return(status, retval, task_id, args, kwargs, einfo)

    def on_retry(self, exc, task_id, args, kwargs, einfo):
        print('this is retry')
        return super().on_retry(exc, task_id, args, kwargs, einfo)

5.flask專案呼叫非同步任務傳送簡訊

flask專案呼叫非同步任務傳送簡訊,application.apps.home.views,程式碼:

@jsonrpc.method(name="Home.sms")
def sms(mobile):
    """傳送簡訊驗證碼"""
    # 驗證手機
    if not re.match("^1[3-9]\d{9}$",mobile):
        return {"errno": status.CODE_VALIDATE_ERROR, "errmsg": message.mobile_format_error}

    # 簡訊傳送冷卻時間
    ret = redis.get("int_%s" % mobile)
    if ret is not None:
        return {"errno": status.CODE_INTERVAL_TIME, "errmsg": message.sms_interval_time}

    # 生成驗證碼
    sms_code = "%06d" % random.randint(0,999999)
    
    try:
        # 非同步傳送簡訊 ******
        from mycelery.sms.tasks import send_sms
        send_sms.delay(mobile=mobile, sms_code=sms_code)
        # 返回結果
        return {"errno":status.CODE_OK, "errmsg": message.sms_is_send}
    except Exception as e:
        return {"errno": status.CODE_SMS_ERROR, "errmsg": message.sms_send_error}

6.執行celery

在第一個終端執行celery

主程式終端下啟動: celery -A mycelery.main worker -l info
排程器終端下啟動: celery -A mycelery.main beat

再開一個終端,輸入如下指令

python manage.py shell
>>> from mycelery.sms.tasks import send_sms
>>> send_sms.delay(mobile="13928836666",sms_code="123456")

2.使用者登入

1.jwt登入驗證:flask_jwt_extended

1.flask_jwt_extended簡介

當前我們開發的專案屬於前後端分離,而目前最適合我們使用的認證方式就是jwt token認證。

在flask中,我們可以通過flask_jwt_extended模組來快速實現jwt使用者登入認證。

注意:

  1. flask_jwt_extended的作者開發當前模組主要適用於flask的普通檢視方法的。其認證方式主要通過裝飾器來完成。而我們當前所有服務端介面都改造成了jsonrpc規範介面,所以我們在使用過程中,需要對部分原始碼進行調整才能正常使用。

  2. 事實上,在我們當前使用的flask_jsonrpc也提供了使用者登陸認證功能,但是這個功能是依靠使用者賬戶username和密碼password來實現。如果我們基於當前這種方式,也可以實現jwt登陸認證,只是相對於上面的flask_jwt_extended模組而言,要補充的程式碼會更多,所以在此,我們放棄這塊功能的使用。

2.模組安裝

pip install flask-jwt-extended

官網文件:https://flask-jwt-extended.readthedocs.io/en/latest/

配置說明:https://flask-jwt-extended.readthedocs.io/en/latest/options/

3.初始化

在魔方專案中對模組進行初始化,application/__init__.py,程式碼:

 

import os,sys
# 引入flask_jwt_extended模組
from flask_jwt_extended import JWTManager

# jwt認證模組例項化
jwt = JWTManager()

def init_app(config_path):
    """全域性初始化"""
   
    # jwt初始化
    jwt.init_app(app)

    return manager

4.jwt相關配置

配置檔案,application.settings.dev,程式碼:

from . import InitConfig
class Config(InitConfig):
    """專案開發環境下的配置"""
    ......

    # jwt 相關配置
    # 加密演算法,預設: HS256
    JWT_ALGORITHM  = "HS256"
    # 祕鑰,預設是flask配置中的SECRET_KEY
    JWT_SECRET_KEY = "y58Rsqzmts6VCBRHes1Sf2DHdGJaGqPMi6GYpBS4CKyCdi42KLSs9TQVTauZMLMw"
    # token令牌有效期,單位: 秒/s,預設: datetime.timedelta(minutes=15) 或者 15 * 60
    JWT_ACCESS_TOKEN_EXPIRES = 60
    # refresh重新整理令牌有效期,單位: 秒/s,預設:datetime.timedelta(days=30) 或者 30*24*60*60
    JWT_REFRESH_TOKEN_EXPIRES = 30*24*60*60
    # 設定通過哪種方式傳遞jwt,預設是http請求頭,也可以是query_string,json,cookies
    JWT_TOKEN_LOCATION = "headers"
    # 當通過http請求頭傳遞jwt時,請求頭引數名稱設定,預設值: Authorization
    JWT_HEADER_NAME="Authorization"
    # 當通過http請求頭傳遞jwt時,令牌的字首。
    # 預設值為 "Bearer",例如:Authorization: Bearer <JWT>
    JWT_HEADER_TYPE="jwt"

5.編寫登入檢視函式(和jwt部分相關的)

application.apps.users.views,程式碼:

 

from application import jsonrpc,db
from .marshmallow import MobileSchema,UserSchema
from marshmallow import ValidationError
from message import ErrorMessage as Message
from status import APIStatus as status
......

from flask_jwt_extended import create_access_token,create_refresh_token,jwt_required,get_jwt_identity,jwt_refresh_token_required
from flask import jsonify,json

@jsonrpc.method("User.login")
def login(account,password):
    """根據使用者登入資訊生成token"""
    # 1. todo 根據賬戶資訊和密碼獲取使用者
    
    # 2. 生成jwt token
    access_token = create_access_token(identity=account)
    refresh_token = create_refresh_token(identity=account)
    return "ok"

@jsonrpc.method("User.info")
@jwt_required # 驗證jwt
def info():
    """獲取使用者資訊"""
    user_data = json.loads(get_jwt_identity()) # get_jwt_identity 用於獲取載荷中的資料
    return "ok"

@jsonrpc.method("User.refresh")
@jwt_refresh_token_required
def refresh():
    """重新獲取新的認證令牌token"""
    current_user = get_jwt_identity()
    # 重新生成token
    access_token = create_access_token(identity=current_user)
    return access_token

6.修改jwt原始碼

裝飾器jwt_required就是用於獲取客戶端提交的資料中的jwt的方法,這裡,我們需要進行2處調整。以方便它更好的展示錯誤資訊。

flask_jwt_extended/view_decorators.py,程式碼:

from jwt.exceptions import ExpiredSignatureError
from flask_jwt_extended.exceptions import InvalidHeaderError
from message import ErrorMessage as message
from status import APIStatus as status
def jwt_required(fn):
@wraps(fn) def wrapper(*args, **kwargs): try: verify_jwt_in_request() except NoAuthorizationError: return {"errno":status.CODE_NO_AUTHORIZATION,"errmsg":message.no_authorization} except ExpiredSignatureError: return {"errno":status.CODE_SIGNATURE_EXPIRED,"errmsg":message.authorization_has_expired} except InvalidHeaderError: return {"errno":status.CODE_INVALID_AUTHORIZATION,"errmsg":message.authorization_is_invalid} return fn(*args, **kwargs) return wrapper

當前檔案,另一個驗證函式jwt_refresh_token_required,程式碼:

def jwt_refresh_token_required(fn):

    @wraps(fn)
    def wrapper(*args, **kwargs):
        try:
            verify_jwt_refresh_token_in_request()
        except NoAuthorizationError:
            return {"errno":status.CODE_NO_AUTHORIZATION,"errmsg":message.no_authorization}
        except ExpiredSignatureError:
            return {"errno":status.CODE_SIGNATURE_EXPIRED,"errmsg":message.authorization_has_expired}
        except InvalidHeaderError:
            return {"errno":status.CODE_INVALID_AUTHORIZATION,"errmsg":message.authorization_is_invalid}
        return fn(*args, **kwargs)
    return wrapper

2.服務端提供使用者登入的API介面

application.apps.users.views,檢視實現並完成登陸介面,程式碼:

from flask_jwt_extended import create_access_token,create_refresh_token,jwt_required,get_jwt_identity,jwt_refresh_token_required
from flask import jsonify,json
from sqlalchemy import or_
from .models import User
from message import ErrorMessage as message
from status import APIStatus as status
@jsonrpc.method("User.login")
def login(account,password):
    """根據使用者登入資訊生成token"""
    # 1. 根據賬戶資訊和密碼獲取使用者
    if len(account) < 1:
        return {"errno":status.CODE_NO_ACCOUNT,"errmsg":message.account_no_data}
    user = User.query.filter(or_(
        User.mobile==account,
        User.email==account,
        User.name==account
    )).first()

    # 檢測使用者是否存在
    if user is None: 
        return {"errno": status.CODE_NO_USER,"errmsg":message.user_not_exists}

    # 驗證密碼
    if not user.check_password(password): 
        return {"errno": status.CODE_PASSWORD_ERROR, "errmsg":message.password_error}

    # 2. 生成jwt token
    access_token = create_access_token(identity=user.id)
    refresh_token = create_refresh_token(identity=user.id)

    return {"access_token": access_token,"refresh_token":refresh_token}

@jsonrpc.method("User.info")
@jwt_required # 驗證jwt
def info():
    """獲取使用者資訊"""
    user_data = json.loads(get_jwt_identity()) # get_jwt_identity 用於獲取載荷中的資料
    print(user_data)
    return "ok"

@jsonrpc.method("User.refresh")
@jwt_refresh_token_required
def refresh():
    """重新獲取新的認證令牌token"""
    current_user = get_jwt_identity()
    # 重新生成token
    access_token = create_access_token(identity=current_user)
    return access_token

 

相關文章