用 Flask 來寫個輕部落格 (24) — 使用 Flask-Login 來保護應用安全
目錄
前文列表
擴充套件閱讀
使用者登入帳號
Web上的使用者登入功能應該是最基本的功能了,但這個功能可能並沒有你所想像的那麼簡單,這是一個關係到使用者安全的功能. 在現代這樣速度的計算速度下,用窮舉法破解賬戶的密碼會是一件很輕鬆的事。所以在設計使用者口令登入功能的時候需要注意下列幾點:
- 用正則表示式限制使用者輸入一些非常容易被破解的口令
- 密文儲存使用者的口令
- 不讓瀏覽器記住你的密碼
- 使用 HTTPS 在網上傳輸你的密碼
上述的手段都可以幫助我們的應用程式更好的保護使用者的賬戶資訊, 後兩點是否實現, 視乎於應用的安全級別是否嚴格, 我們在這裡僅實現前面兩點. 除此之外我們還要解決一個非常重要的問題, 就是使用者登入狀態的處理.
使用者登入狀態
因為 HTTP 是無狀態的協議,也就是說,這個協議是無法記錄使用者訪問狀態的,所以使用者的每次請求都是獨立的無關聯的. 而我們的網站都是設計成多個頁面的,所在頁面跳轉過程中我們需要知道使用者的狀態,尤其是使用者登入的狀態,這樣我們在頁面跳轉後我們才知道使用者是否有許可權來操作該頁面上的一些功能或是檢視一些資料。
所以,我們每個頁面都需要對使用者的身份進行認證。在這樣的應用場景下, 儲存使用者的登入狀態的功能就顯得非常重要了. 為了實現這一功能:
- 第一種方法, 用得最多的技術就是 session 和 cookie,我們會把使用者登入的資訊存放在客戶端的 cookie 裡,這樣,我們每個頁面都從這個 cookie 裡獲得使用者是否已經登入的資訊,從而達到記錄狀態,驗證使用者的目的.
- 第二種方法, 我們這裡會使用 Flask-Login 擴充套件是提供支撐.
NOTE: 兩種方法是不能夠共存的.
Flask-Login
Flask-Login 為 Flask 提供使用者 session 的管理機制。它可以處理 Login、Logout 和 session 等服務。
作用:
- 將使用者的 id 儲存在 session 中,方便用於 Login/Logout 等流程。
- 讓你能夠約束使用者 Login/Logout 的檢視
- 提供 remember me 功能
- 保護 cookies 不被篡改
使用 Flask-Login 來保護應用安全
- 安裝
pip install flask-login
pip freeze . requirements.txt
- 初始化 LoginManager 物件
vim extensions.py
from flask.ext.login import LoginManager
# Create the Flask-Login's instance
login_manager = LoginManager()
- 設定 LoginManager 物件的引數
vim extensions.py
# Setup the configuration for login manager.
# 1. Set the login page.
# 2. Set the more stronger auth-protection.
# 3. Show the information when you are logging.
# 4. Set the Login Messages type as `information`.
login_manager.login_view = "main.login"
login_manager.session_protection = "strong"
login_manager.login_message = "Please login to access this page."
login_manager.login_message_category = "info"
@login_manager.user_loader
def load_user(user_id):
"""Load the user's info."""
from models import User
return User.query.filter_by(id=user_id).first()
NOTE 1: login_view 指定了登入頁面的檢視函式
NOTE 2: session_protection 能夠更好的防止惡意使用者篡改 cookies, 當發現 cookies 被篡改時, 該使用者的 session 物件會被立即刪除, 導致強制重新登入.
NOTE 3: login_message 指定了提供使用者登入的文案
NOTE 4: login_category 指定了登入資訊的類別為 info
NOTE 5: 我們需要定義一個 LoginManager.user_loader
回撥函式,它的作用是在使用者登入並呼叫 login_user()
的時候, 根據 user_id 找到對應的 user, 如果沒有找到,返回None, 此時的 user_id 將會自動從 session 中移除, 若能找到 user ,則 user_id 會被繼續儲存.
- 修改 User models, 以更好支援 Flask-Login 的使用者狀態檢驗
class User(db.Model):
"""Represents Proected users."""
# Set the name for table
__tablename__ = 'users'
id = db.Column(db.String(45), primary_key=True)
username = db.Column(db.String(255))
password = db.Column(db.String(255))
# one to many: User ==> Post
# Establish contact with Post's ForeignKey: user_id
posts = db.relationship(
'Post',
backref='users',
lazy='dynamic')
roles = db.relationship(
'Role',
secondary=users_roles,
backref=db.backref('users', lazy='dynamic'))
def __init__(self, id, username, password):
self.id = id
self.username = username
self.password = self.set_password(password)
# Setup the default-role for user.
default = Role.query.filter_by(name="default").one()
self.roles.append(default)
def __repr__(self):
"""Define the string format for instance of User."""
return "<Model User `{}`>".format(self.username)
def set_password(self, password):
"""Convert the password to cryptograph via flask-bcrypt"""
return bcrypt.generate_password_hash(password)
def check_password(self, password):
return bcrypt.check_password_hash(self.password, password)
def is_authenticated(self):
"""Check the user whether logged in."""
# Check the User's instance whether Class AnonymousUserMixin's instance.
if isinstance(self, AnonymousUserMixin):
return False
else:
return True
def is_active():
"""Check the user whether pass the activation process."""
return True
def is_anonymous(self):
"""Check the user's login status whether is anonymous."""
if isinstance(self, AnonymousUserMixin):
return True
else:
return False
def get_id(self):
"""Get the user's uuid from database."""
return unicode(self.id)
NOTE 1: is_authenticated()
檢驗 User 的例項化物件是否登入了.
NOTE 2: is_active()
檢驗使用者是否通過某些驗證
NOTE 3: is_anonymous()
檢驗使用者是否為匿名使用者
NOTE 4: get_id()
返回 User 例項化物件的唯一標識 id
在完成這些準備之後, 我們可以將 Flask-Login 應用到 Login/Logout 的功能模組中.
- 在 LoginForm 加入 Remember Me 可選框
class LoginForm(Form):
"""Login Form"""
username = StringField('Usermame', [DataRequired(), Length(max=255)])
password = PasswordField('Password', [DataRequired()])
remember = BooleanField("Remember Me")
def validate(self):
"""Validator for check the account information."""
check_validata = super(LoginForm, self).validate()
# If validator no pass
if not check_validata:
return False
# Check the user whether exist.
user = User.query.filter_by(username=self.username.data).first()
if not user:
self.username.errors.append('Invalid username or password.')
return False
# Check the password whether right.
if not user.check_password(self.password.data):
self.password.errors.append('Invalid username or password.')
return False
return True
- jmilkfansblog.controllers.main.py
@main_blueprint.route('/login', methods=['GET', 'POST'])
@openid.loginhandler
def login():
"""View function for login.
Flask-OpenID will be receive the Authentication-information
from relay party.
"""
# Create the object for LoginForm
form = LoginForm()
# Create the object for OpenIDForm
openid_form = OpenIDForm()
# Send the request for login to relay party(URL).
if openid_form.validate_on_submit():
return openid.trg_login(
openid_form.openid_url.data,
ask_for=['nickname', 'email'],
ask_for_optional=['fullname'])
# Try to login the relay party failed.
openid_errors = openid.fetch_error()
if openid_errors:
flash(openid_errors, category="danger")
# Will be check the account whether rigjt.
if form.validate_on_submit():
# Using session to check the user's login status
# Add the user's name to cookie.
# session['username'] = form.username.data
user = User.query.filter_by(username=form.username.data).one()
# Using the Flask-Login to processing and check the login status for user
# Remember the user's login status.
login_user(user, remember=form.remember.data)
identity_changed.send(
current_app._get_current_object(),
identity=Identity(user.id))
flash("You have been logged in.", category="success")
return redirect(url_for('blog.home'))
return render_template('login.html',
form=form,
openid_form=openid_form)
NOTE 1: login_user()
能夠將已登入並通過 load_user()
的使用者對應的 User 物件, 儲存在 session 中, 所以該使用者在訪問不同的頁面的時候不需要重複登入.
NOTE 2: 如果希望應用記住使用者的登入狀態, 只需要為 login_user()
的形參 remember 傳入 True 實參就可以了.
- jmilkfansblog.controllers.main.py
@main_blueprint.route('/logout', methods=['GET', 'POST'])
def logout():
"""View function for logout."""
# Remove the username from the cookie.
# session.pop('username', None)
# Using the Flask-Login to processing and check the logout status for user.
logout_user()
identity_changed.send(
current_app._get_current_object(),
identity=AnonymousIdentity())
flash("You have been logged out.", category="success")
return redirect(url_for('main.login'))
NOTE 1 Logout 時, 使用 logout_user
來將使用者從 session 中刪除.
- jmilkfansblog.controllers.blog.py
如果我們希望網站的某些頁面不能夠被匿名使用者檢視, 並且跳轉到登入頁面時, 我們可以使用login_required
裝飾器
from flask.ext.login import login_required, current_user
@blog_blueprint.route('/new', methods=['GET', 'POST'])
@login_required
def new_post():
"""View function for new_port."""
form = PostForm()
# Ensure the user logged in.
# Flask-Login.current_user can be access current user.
if not current_user:
return redirect(url_for('main.login'))
# Will be execute when click the submit in the create a new post page.
if form.validate_on_submit():
new_post = Post(id=str(uuid4()), title=form.title.data)
new_post.text = form.text.data
new_post.publish_date = datetime.now()
new_post.users = current_user
db.session.add(new_post)
db.session.commit()
return redirect(url_for('blog.home'))
return render_template('new_post.html',
form=form)
引用了這個裝飾器之後, 當匿名使用者像建立文章時, 就會跳轉到登入頁面.
NOTE 1: Flask-Login 提供了一個代理物件 current_user 來訪問和表示當前登入的物件, 這個物件在檢視或模板中都是能夠被訪問的. 所以我們常在需要判斷使用者是否為當前使用者時使用(EG. 使用者登入後希望修改自己建立的文章).
小結
Flask-Login 為我們的應用提供了非常重要的功能, 讓應用清楚的知道了當前登入的使用者是誰和該使用者的登入狀態是怎麼樣的. 這就讓我們為不同的使用者設定特定的許可權和功能提供了可能. 如果沒有這個功能的話, 那麼所有登入的使用者都可以任意的使用應用的資源, 這是非常不合理的.