flask token認證


在前后端分離的項目中,我們現在多半會使用token認證機制實現登錄權限驗證。

token通常會給一個過期時間,這樣即使token泄露了,危害期也只是在有效時間內,超過這個有效時間,token過期了,就需要重新生成一個新的token。

如何生成token呢?

1、創建用戶數據庫,本文會使用flask-SQLAlchemy(ORM)去管理數據庫:

  首先創建一個用戶模型:包括了用戶昵稱,賬號(郵箱或者電話號碼等),密碼及擁有的權限

 1 class User(Base):
 2     id = Column(Integer, primary_key=True)
 3     nickname = Column(String(30), nullable=False)
 4     account = Column(String(30), nullable=False)
 5     _password = Column("password", String(100), nullable=False)
 6     auth = Column(SmallInteger, default=1)
 7 
 8     @property
 9     def password(self):
10         return self._password
11 
12     @password.setter
13     def password(self, row):
14         self._password = generate_password_hash(row)
15 
16     @staticmethod
17     def register_by_email(nickname, account, password):
18         with db.auto_commit():
19             user = User()
20             user.nickname = nickname
21             user.account = account
22             user.password = password
23             db.session.add(user)
24 
25     @staticmethod
26     def checkUser(email, password):
27         # 驗證用戶名是否存在
28         user = User.query.filter_by(account=email).first_or_404()
29         res = user.checkPassword(password)
30         if not res:
31             raise AuthFailed()
32         scope = "adminScope" if user.auth=="2" else "scope"
33         return {"uid":user.id, "scope":scope}
34 
35     def checkPassword(self, raw):
36         if not self._password:
37             return False
38         # check_password_hash將raw加密后和_password比較
39         p = generate_password_hash(raw)
40         print(p==self._password)
41         return check_password_hash(self._password, raw)
42 
43     def delete(self):
44         self.status = "0"
View Code

  由於安全原因,數據庫的密碼是一定不能明文保存的,所以此處將用戶名進行了加密

  本文使用的werkzeug.security下面的generate_password_hash()對密碼進行的加密,我們定義了password.setter方法,當在設置密碼時,會調用generate_password_hash(password)加密密碼,並將其賦值給_password

  當驗證密碼時,會調用werkzeug.security下面的check_password_hash(hashpwd, raw) 對用戶傳遞過來的密碼和加密后的密碼進行比對,如果正確返回True

2、注冊

  當前端傳遞過來用戶名,密碼時進行注冊時,我們需要對用戶名和密碼進行如下基本驗證

    1)非空性及長度等基本校驗

    2)用戶名是否已經存在

  郵箱注冊form:

class EmailRegisterForm(RegisterForm):
    nickname = StringField(validators=[DataRequired(), length(3,30)])
    account = StringField(validators=[DataRequired(message="account can not be blank"), length(
        min=3, max=32, message="account length wrong"), Email(message="format wrong")])
    password = StringField(validators=[DataRequired()])

    def validate_account(self, value):
        user = User.query.filter_by(account=value.data).first()
        if user:
            raise ParamsError(msg = "用戶已存在")

 

當驗證成功后,會調用我們在User模型下面定義的register_by_email() 方法進行注冊。

@api.router("/register", methods=["POST"])
def register():
    data = request.json
    form = RegisterForm(data=data).validate_for_api()
    promise = {
        ClientType.REGISTER_EMAIL:_register_by_email,
        ClientType.REGISTER_MOBILE:_register_by_mobile()
    }
    promise[form.type.data]()
    return Success()


def _register_by_email():
    form = EmailRegisterForm(data=request.json).validate_for_api()
    nickname = form.nickname.data
    account = form.account.data
    password = form.password.data
    User.register_by_email(nickname, account,password)

現在我們使用postman發送一條注冊請求

 

 

如果用戶已經存在,會返回400

 

3、登錄,生成token

  生成token的方式有很多種,如產生一個固定長度的隨機字符串,和用戶名密碼及過期時間一起存儲在數據庫中,這樣token就是一個普通的字符串,可以方便的和其他字符串驗證比較並可以檢查是否過期

  比較復雜一點的做法就是,不要將token存儲在數據庫,而是使用數字簽名作為token,這樣做的好處是經過用戶數字簽名的token是可以防止篡改的。

  flask使用與數字簽名類似的方法去實現加密的token,我們可以直接使用itsdangerous庫去實現。

  生成token,需要用到itsdangerous下面的TimedJSONWebSignatureSerializer

  首先我們實例化一個Serializer,並將我們的秘鑰SECRET_KEY和過期時間作為參數,返回一個TimedJSONWebSignatureSerializer類型對象

  然后調用TimedJSONWebSignatureSerializer對象的dumps方法,將我們想要寫入到token中的信息以字典形式傳遞進去即可。

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer

def generate_auth_token(uid, type, scope, expiration=7200):
    serializer = Serializer(current_app.config["SECRET_KEY"], expires_in=expiration)
    token = serializer.dumps({"uid":uid, "type":type.value, "scope":scope})
    return token

  當前端傳遞用戶名,密碼到服務端時,服務端校驗用戶存在並且密碼正確時候,就會調用generate_auth_token函數,生成token

  值得注意的一點是,這里生成的token是二進制的,所以我們在返回給前端時,需要將二進制解碼token.decode("ascii")

  后面用戶在訪問需要登錄才能訪問的的接口時,就不需要再登錄,只需要將token傳遞過來即可。

  1)驗證用戶名是否存在,此方法作為靜態方法放在User模型下

    @staticmethod
    def checkUser(email, password):
        user = User.query.filter_by(account=email).first_or_404()
        res = user.checkPassword(password)
        if not res:
            raise AuthFailed()
        scope = "adminScope" if user.auth=="2" else "scope"
        return {"uid":user.id, "scope":scope}

  2)校驗密碼是否匹配  

    def checkPassword(self, raw):
        if not self._password:
            return False
        # check_password_hash將raw加密后和_password比較
        return check_password_hash(self._password, raw)

  3)校驗通過后,調用generate_auth_token方法生成token

@api.router("/", methods=["POST"])
def get_token():
    data = request.json
    form = EmailLoginForm(data=data).validate_for_api()
    type = form.type.data
    promise = {
        ClientType.REGISTER_EMAIL:User.checkUser
    }
    identify = promise[ClientType(type)](form.account.data, form.password.data)
    expiration = current_app.config["EXPIRATION"]
    token = generate_auth_token(identify["uid"], type,identify["scope"], expiration)
    r = {
        "token":token.decode("ascii")
    }
    return jsonify(r)

 

4、token認證

  如用戶想要獲取用戶信息,這個是要登錄后才能訪問的接口,我們可以使用一個裝飾器 @auth.login_required 保護,即表示只有正常登錄的用戶才可以訪問

  這個裝飾器用到了flask_httpauth庫下面的HTTPBasicAuth

  auth = HTTPBasicAuth

  HTTP Basic Authentication 協議沒有具體要求必須使用用戶名密碼進行驗證,HTTP頭可以使用兩個字段去傳輸認證信息,對於token,我們只需要將token作為用戶名傳遞過去即可,密碼字段可以不填

  @auth.verify_password將作為@auth.login_required的中校驗密碼的回調函數被調用。

  我們前面生成token的時候,用到了我們自定義了SECRET_KEY加密,同樣解密也需要使用我們的秘鑰SECRET_KEY,加密調用的是serializer.dumps(),解密對應的需要使用serializer.loads()

  調用serializer.loads(token)時,如果捕捉到下面兩個錯誤:

    BadSignature:簽名錯誤,簽名可能被篡改

    SignatureExpired:簽名已過期

  表示驗證token失敗,直接拋出自定義異常,如果沒有捕捉到錯誤,表示,驗證通過。可以從中取得前面加密的用戶信息,並將信息保存在g變量中,留做他用。

  這里的g變量和request一樣,都是代理模式的實現,而且是線程隔離的,所以也不用擔心多個請求線程導致數據錯亂。

@auth.verify_password
def check_authorization(token, pwd):
    user_info = check_auth_token(token)
    if not user_info:
        return False
    else:
        g.user = user_info
        return True

def check_auth_token(token):
    serialzer = Serializer(current_app.config["SECRET_KEY"])
    try:
        s = serialzer.loads(token)
    except BadSignature:
        raise AuthFailed(msg="token is invalid", error_code=1004)
    except SignatureExpired:
        raise AuthFailed(msg="token is expired", error_code=1004)
    uid = s["uid"]
    type = s["type"]
    scope = s["scope"]
    return user(uid, type, scope)

 

【補充】

  某些要求比較嚴謹的驗證,還可以將設備mac地址等信息加入都token中

  獲取設備ip mac等信息的方法:

    import socket

    host_name = socket.gethostname()

    ip = socket.gethostbyname(host_name)

    import uuid

    mac = uuid.UUID(int=uuid.getnode()).hex[-12:].upper()

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM