在先後端分離的項目中,咱們如今多半會使用token認證機制實現登陸權限驗證。前端
token一般會給一個過時時間,這樣即便token泄露了,危害期也只是在有效時間內,超過這個有效時間,token過時了,就須要從新生成一個新的token。node
如何生成token呢?數據庫
一、建立用戶數據庫,本文會使用flask-SQLAlchemy(ORM)去管理數據庫:json
首先建立一個用戶模型:包括了用戶暱稱,帳號(郵箱或者電話號碼等),密碼及擁有的權限flask
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"
因爲安全緣由,數據庫的密碼是必定不能明文保存的,因此此處將用戶名進行了加密後端
本文使用的werkzeug.security下面的generate_password_hash()對密碼進行的加密,咱們定義了password.setter方法,當在設置密碼時,會調用generate_password_hash(password)加密密碼,並將其賦值給_passwordapi
當驗證密碼時,會調用werkzeug.security下面的check_password_hash(hashpwd, raw) 對用戶傳遞過來的密碼和加密後的密碼進行比對,若是正確返回Truepromise
二、註冊安全
當前端傳遞過來用戶名,密碼時進行註冊時,咱們須要對用戶名和密碼進行以下基本驗證session
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
三、登陸,生成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)
四、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()