本篇介紹 Session-base login 登入驗證,完整範例存放於 template-flask-login · GitHub,歡迎 Git Clone 使用~
Table
ㄧ. 環境設置
1. 安裝套件
使用 pip3 install -r requirements.txt
來安裝此次會需要的套件。
▍本次使用的 Flask 擴充套件清單如下:
- 資料庫使用 Flask-SQLAlchemy ORM 操作和連線,設定連線的資料庫是 SQLite
- 接收到 request 請求的序列化和驗證資料使用 Marshmallow 套件
- RESTful API 使用 Flask-RESTful 套件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# requirements.txt aniso8601==8.0.0 Click==7.0 Flask==1.1.1 Flask-RESTful==0.3.8 Flask-SQLAlchemy==2.4.1 itsdangerous==1.1.0 Jinja2==2.11.1 MarkupSafe==1.1.1 marshmallow==3.6.0 PyMySQL==0.9.3 python-dotenv==0.13.0 pytz==2020.1 six==1.14.0 SQLAlchemy==1.3.17 Werkzeug==1.0.0 |
2. 了解整體架構
架構上使用 Flask Application Factories (工廠模式),和 MVC (Model–view–controller)
- Model – 負責資料庫操作和儲存。
- View (Flask 內稱為 Templates) – 負責使用者介面設計。
- Controller (Flask 內稱為 View) – 負責對 Request / Response 處理,和負責與 Model 的資料溝通,並將資料串接到 View (Templates)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
├── app │ ├── __init__.py │ ├── config │ │ ├── config.py │ │ └── test.db │ ├── model │ │ └── user.py │ ├── templates │ │ ├── login.html │ │ └── signup.html │ └── view │ ├── abort_msg.py │ └── auth.py ├── main.py └── requirements.txt |
3. 了解 route 路徑
▍確認環境設立成功:
在終端機執行 flask run --reload
- 測試連線 http://127.0.0.1:5000/,如果得到
success
回應則代表套件都安裝完成! - 連線 http://127.0.0.1:5000/create_all,建立此次需要的 sqlite 測試資料庫,會被存放在
/app/config/test.db
位置。
▍其他相關路徑:
- 建立帳號:http://127.0.0.1:5000/auth/singup
- 登入帳號:http://127.0.0.1:5000/auth/login
- 登出帳號:http://127.0.0.1:5000/auth/logout
- 測試限制 member 權限網址: http://127.0.0.1:5000/normal_member
- 測試限制 admin 權限網址: http://127.0.0.1:5000/admin_member
二. 進入正題 – 實作 Flask Login 驗證
此次只會以會員註冊頁為範例講解,完整範例存放於 template-flask-login · GitHub,歡迎 Git Clone 使用~
1. 設定 註冊會員頁
位於 app/view/auth.py,Signup 完整程式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Signup(Resource): def post(self): try: # 資料驗證 user_data = users_schema.load(request.form, partial=True) # 註冊 new_user = UserModel(user_data) new_user.save_db() new_user.save_session() return {'msg': 'registration success'}, 200 except ValidationError as error: return {'errors': error.messages}, 400 except Exception as e: return {'errors': abort_msg(e)}, 500 def get(self): return make_response(render_template('signup.html')) api.add_resource(Signup, '/signup') |
首先我們看到第五行,我們使用 request.form
接收前端 Form 表單傳過來的使用者帳號和密碼,並且使用 Flask 擴充套件 Marshmallow 的 users_schema.load 語法進行資料驗證。
接下來可以看到第七行,將驗證過後的帳號密碼放入 UserModel(user_data)
,實例化新的使用者後存入 db new_user.save_db()
和設定使用者的 session new_user.save_session()
。
1 2 3 |
new_user = UserModel(user_data) # 實例化新使用者 new_user.save_db() # 將新使用者存入 db new_user.save_session() # 設定新使用者的 session |
如果是資料驗證錯誤 (密碼少於 6 碼、或缺少帳號/密碼欄位) 則會在 except ValidationError as error:
這邊觸發。
1 2 |
except ValidationError as error: return {'errors': error.messages}, 400 |
而如果是其他錯誤訊息則會在這邊觸發 except Exception as e:
,並且將錯誤訊息清理過後回覆給前端 {'errors': abort_msg(e)}
1 2 |
except Exception as e: return {'errors': abort_msg(e)}, 500 |
沒有發生錯誤,則結束並返回註冊成功 {'msg': 'registration success'}
訊息。
2. 設定 model
看到這邊大家心裡應該有不少問題,像是使用者密碼的 hash 處理?或資料驗證做了哪些事情?或存入 Session 都存了些什麼資料?
以上這些都將會在 model 裡面為大家講解,先來看一下 app/mdoel/user.py 的完整代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
class UserModel(db.Model): __tablename__ = 'user' uid = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(30), unique=True) password_hash = db.Column(db.String(255)) role = db.Column(db.String(10), default='normal') insert_time = db.Column(db.DateTime, default=datetime.now) update_time = db.Column(db.DateTime, onupdate=datetime.now, default=datetime.now) def __init__(self, user_data): self.name = user_data['name'] self.password = user_data['password'] @property def password(self): raise AttributeError('passowrd is not readabilty attribute') @password.setter def password(self, password): self.password_hash = generate_password_hash(password) def verify_password(self, password): return check_password_hash(self.password_hash, password) @classmethod def get_user(cls, name): return cls.query.filter_by(name=name).first() def save_db(self): db.session.add(self) db.session.commit() def save_session(self): session['username'] = self.name session['role'] = self.role session['uid'] = self.uid @staticmethod def remove_session(): session['username'] = '' session['role'] = '' session['uid'] = '' class UserSchema(Schema): uid = fields.Integer(dump_only=True) name = fields.String(required=True, validate=validate.Length(3)) password = fields.String(required=True, validate=validate.Length(6)) role = fields.String() insert_time = fields.DateTime() update_time = fields.DateTime() |
▍使用者密碼加密處理
可以看到第 16 行 @password.setter
這邊,如果要對 password 賦值的話,就會被 generate_password_hash(password)
加密,並且產生新的屬性 self.password_hash
再存入資料庫裡面。
回到剛剛 view 裡面 UserModel(user_data)
的這段實例化新使用者程式,在 Model 裡面已經做了加密的處理。
而如果對 view 裡面實例化後的新使用者,再次呼叫 password 的屬性話,則會產生 “passowrd is not readabilty attribute” 的錯誤訊息!要拿到 password 只能呼叫被加密過的密碼 password_hash。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class UserModel(db.Model): __tablename__ = 'user' uid = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(30), unique=True) password_hash = db.Column(db.String(255)) role = db.Column(db.String(10), default='normal') insert_time = db.Column(db.DateTime, default=datetime.now) update_time = db.Column(db.DateTime, onupdate=datetime.now, default=datetime.now) def __init__(self, user_data): self.name = user_data['name'] self.password = user_data['password'] @property def password(self): raise AttributeError('passowrd is not readabilty attribute') @password.setter def password(self, password): self.password_hash = generate_password_hash(password) def verify_password(self, password): return check_password_hash(self.password_hash, password) |
▍傳入資料驗證
我們在 model/user.py 內創建了 UserSchema 的類別並且繼承了 marshmallow 套件的 Schema,並且在此類別內制定好了欄位的名稱以及驗證內容,像是欄位 password 必須是 fields.String
型態,且輸入長度需要大於 6 validate.Length(6)
所以在 view 裡面 users_schema.load(request.form, partial=True)
只需呼叫 load 就可以很輕鬆地進行資料驗證了!
1 2 3 4 5 6 7 8 9 |
from marshmallow import Schema, fields, pre_load, validate class UserSchema(Schema): uid = fields.Integer(dump_only=True) name = fields.String(required=True, validate=validate.Length(3)) password = fields.String(required=True, validate=validate.Length(6)) role = fields.String() insert_time = fields.DateTime() update_time = fields.DateTime() |
▍Session 都存了些什麼資料
我們在 Session 中儲存了使用者的 username 和 role 以及 uid,而如果登出後只需要呼叫 remove_session() 就會將剛剛設定的 Session 清空囉!
1 2 3 4 5 6 7 8 9 10 |
def save_session(self): session['username'] = self.name session['role'] = self.role session['uid'] = self.uid @staticmethod def remove_session(): session['username'] = '' session['role'] = '' session['uid'] = '' |
3. 驗證使用者登入狀態
1 2 3 4 |
@app.route('/normal_member') @check_login('normal') def member_normal_page(): return 'ok' |
如果要像上面第二行 @check_login 可以使用裝飾詞來判斷是否登入的話,我們需要寫一個裝飾詞 。
首先會先去檢查使用者 Session 內有沒有 role session.get('role')
,如果沒有 role 則噴 401 權限不足錯誤訊息。
有 role 的話,則判斷裝飾詞傳入的 check_role,與 Session 內的 role 是否有資格登入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
def check_login(check_role): def decorator(func): def wrap(*args, **kw): user_role = session.get('role') if user_role == None or user_role == '': return abort(401) else: if check_role == 'admin' and check_role == user_role: return func(*args, **kw) if check_role == 'normal': return func(*args, **kw) else: return abort(401) wrap.__name__ = func.__name__ return wrap return decorator |
設定好後執行 flask run --reload
- 建立帳號:http://127.0.0.1:5000/auth/singup
- 測試限制 member 權限網址: http://127.0.0.1:5000/normal_member
- 測試限制 admin 權限網址: http://127.0.0.1:5000/admin_member
如果都沒有噴錯就代表成功囉!
三. 淺談 Session-based 驗證機制:
Session-based Authentication 認證機制是將使用者的 Session Information 存放在 Cookie 中,利用 HTTP request 和 response 所攜帶的 Cookie 做為使用者身份驗證的機制,也就是說 Server 端和 Client 端都必須儲存狀態資訊 (例如 Server 端必須將使用者資料存在 Session database 或 memory 中,而 Client 端也必須用 Cookie 儲存 Session Information),因此在使用 Session-based Authentication 通常會有以下缺點:
- 由於 Client 使用 Cookie 存放 Session Information,會需要處理 CSRF 攻擊 的防護。
- 當 Server 需要擴展 scalability 時,例如後端 Server 要從一台擴充成三台,需要煩惱每台之間的 Session 問題。
- Server Side Session 的使用情境下,當使用者每次發送請求時,都要使用 session_id 與資料庫交換資料,當同時使用者過多時,會佔據大量的伺服器資源。
下一篇我們將介紹 Token-based Authentication:【Flask教學】 Flask-JWT-Extended 實作
最後~
▍回顧本篇我們介紹了的 Flask login 內容:
- 環境設置
- 安裝套件
- 了解整體架構
- 了解 route 路徑
- 進入正題 – 實作 Flask Login 驗證機制
- 設定 註冊會員頁
- 設定 model
- 驗證使用者登入狀態
- 最後 – 淺談 Session-based 驗證機制優缺
關於 Flask 教學的延伸閱讀:
▍關於 Flask 教學系列目錄:
▍其他 Flask 相關教學:
- 【Flask教學系列】Flask 為甚麼需要 WSGI 與 Nginx
- 【Flask教學系列】實作 Flask CORS
- 【Flask教學系列】實作 Flask CSRF Protection
- 【Flask教學系列】實作 Dockerfile + nginx + ssl + Flask 教學 (附GitHub完整程式)
那 【Flask教學系列】實作 Flask Session-base login登入驗證機制 的介紹就到這邊告一個段落囉!有任何問題可以在以下留言~
有關 Max行銷誌的最新文章,都會發佈在 Max 的 Facebook 粉絲專頁,如果想看最新更新,還請您按讚或是追蹤唷!