使用Flask實現用戶登陸認證的詳細過程
用戶認證的原理
在了解使用Flask來實現用戶認證之前,我們首先要明白用戶認證的原理。假設現在我們要自己去實現用戶認證,需要做哪些事情呢?
- 首先,用戶要能夠輸入用戶名和密碼,所以需要網頁和表單,用以實現用戶輸入和提交的過程。
- 用戶提交了用戶名和密碼,我們就需要比對用戶名,密碼是否正確,而要想比對,首先我們的系統中就要有存儲用戶名,密碼的地方,大多數後台系統會通過資料庫來存儲,但是實際上我們也可以簡單的存儲到文件當中。(為簡明起見,本文將用戶信息存儲到json文件當中)
- 登錄之後,我們需要維持用戶登錄狀態,以便用戶在訪問特定網頁的時候來判斷用戶是否已經登錄,以及是否有許可權訪問改網頁。這就需要有維護一個會話來保存用戶的登錄狀態和用戶信息。
- 從第三步我們也可以看出,如果我們的網頁需要許可權保護,那麼當請求到來的時候,我們就首先要檢查用戶的信息,比如是否已經登錄,是否有許可權等,如果檢查通過,那麼在response的時候就會將相應網頁回復給請求的用戶,但是如果檢查不通過,那麼就需要返回錯誤信息。
- 在第二步,我們知道要將用戶名和密碼存儲起來,但是如果只是簡單的用明文存儲用戶名和密碼,很容易被「有心人」盜取,從而造成用戶信息泄露,那麼我們實際上應當將用戶信息尤其是密碼做加密處理之後再存儲比較安全。
- 用戶登出
通過Flask以及相應的插件來實現登錄過程
接下來講述如何通過Flask框架以及相應的插件來實現整個登錄過程,需要用到的插件如下:
- flask-wtf
- wtf
- werkzeug
- flask_login
使用flask-wtf和wtf來實現表單功能
flask-wtf對wtf做了一些封裝,不過有些東西還是要直接用wtf,比如StringField等。flask-wtf和wtf主要是用於建立html中的元素和Python中的類的對應關係,通過在Python代碼中操作對應的類,對象等從而控制html中的元素。我們需要在python代碼中使用flask-wtf和wtf來定義前端頁面的表單(實際是定義一個表單類),再將對應的表單對象作為render_template函數的參數,傳遞給相應的template,之後Jinja模板引擎會將相應的template渲染成html文本,再作為http response返回給用戶。
定義表單類示例代碼:
# forms.pyfrom flask_wtf import FlaskFormfrom wtforms import StringField, BooleanField, PasswordFieldfrom wtforms.validators import DataRequired# 定義的表單都需要繼承自FlaskFormclass LoginForm(FlaskForm): # 域初始化時,第一個參數是設置label屬性的 username = StringField(User Name, validators=[DataRequired()]) password = PasswordField(Password, validators=[DataRequired()]) remember_me = BooleanField(remember me, default=False)
在wtf當中,每個域代表就是html中的元素,比如StringField代表的是<input type="text">元素,當然wtf的域還定義了一些特定功能,比如validators,可以通過validators來對這個域的數據做檢查,詳細請參考wtf教程。 對應的html模板可能如下login.html:
{% extends "layout.html" %}<html> <head> <title>Login Page</title> </head> <body> <form action="{{ url_for("login") }}" method="POST"> <p> User Name:<br> <input type="text" name="username" /><br> </p> <p> Password:</br> <input type="password" name="password" /><br> </p> <p> <input type="checkbox" name="remember_me"/>Remember Me </p> {{ form.csrf_token }} </form> </body></html>
這裡{{ form.csrf_token }}也可以使用{{ form.hidden_tag() }}來替換
同時我們也可以使用form去定義模板,跟直接用html標籤去定義效果是相同的,Jinja模板引擎會將對象、屬性轉化為對應的html標籤, 相對應的template,如下login.html:
<!-- 模板的語法應當符合Jinja語法 --><!-- extend from base layout -->{% extends "base.html" %}{% block content %} <h1>Sign In</h1> <form action="{{ url_for("login") }}" method="post" name="login"> {{ form.csrf_token }} <p> {{ form.username.label }}<br> {{ form.username(size=80) }}<br> </p> <p> {{ form.password.label }}<br> <!-- 我們可以傳遞input標籤的屬性,這裡傳遞的是size屬性 --> {{ form.password(size=80) }}<br> </p> <p>{{ form.remember_me }} Remember Me</p> <p><input type="submit" value="Sign In"></p> </form>{% endblock %}
現在我們需要在view中定義相應的路由,並將相應的登錄界面展示給用戶。 簡單起見,將view的相關路由定義放在主程序當中
# app.py@app.route(/login)def login(): form = LoginForm() return render_template(login.html, title="Sign In", form=form)
這裡簡單起見,當用戶請求/login路由時,直接返回login.html網頁,注意這裡的html網頁是經過Jinja模板引擎將相應的模板轉換後的html網頁。 至此,如果我們把以上代碼整合到flask當中,就應該能夠看到相應的登錄界面了,那麼當用戶提交之後,我們應當怎樣存儲呢?這裡我們暫時先不用資料庫這樣複雜的工具存儲,先簡單地存為文件。接下來就看下如何去存儲。
加密和存儲
我們可以首先定義一個User類,用於處理與用戶相關的操作,包括存儲和驗證等。
# models.pyfrom werkzeug.security import generate_password_hashfrom werkzeug.security import check_password_hashfrom flask_login import UserMixinimport jsonimport uuid# define profile.json constant, the file is used to# save user name and password_hashPROFILE_FILE = "profiles.json"class User(UserMixin): def __init__(self, username): self.username = username self.password_hash = self.get_password_hash() self.id = self.get_id() @property def password(self): raise AttributeError(password is not a readable attribute) @password.setter def password(self, password): """save user name, id and password hash to json file""" self.password_hash = generate_password_hash(password) with open(PROFILE_FILE, w+) as f: try: profiles = json.load(f) except ValueError: profiles = {} profiles[self.username] = [self.password_hash, self.id] f.write(json.dumps(profiles)) def verify_password(self, password): if self.password_hash is None: return False return check_password_hash(self.password_hash, password) def get_password_hash(self): """try to get password hash from file. :return password_hash: if the there is corresponding user in the file, return password hash. None: if there is no corresponding user, return None. """ try: with open(PROFILE_FILE) as f: user_profiles = json.load(f) user_info = user_profiles.get(self.username, None) if user_info is not None: return user_info[0] except IOError: return None except ValueError: return None return None def get_id(self): """get user id from profile file, if not exist, it will generate a uuid for the user. """ if self.username is not None: try: with open(PROFILE_FILE) as f: user_profiles = json.load(f) if self.username in user_profiles: return user_profiles[self.username][1] except IOError: pass except ValueError: pass return unicode(uuid.uuid4()) @staticmethod def get(user_id): """try to return user_id corresponding User object. This method is used by load_user callback function """ if not user_id: return None try: with open(PROFILE_FILE) as f: user_profiles = json.load(f) for user_name, profile in user_profiles.iteritems(): if profile[1] == user_id: return User(user_name) except: return None return None
- User類需要繼承flask-login中的UserMixin類,用於實現相應的用戶會話管理。
- 這裡我們是直接存儲用戶信息到一個json文件"profiles.json"
- 我們並不直接存儲密碼,而是存儲加密後的hash值,在這裡我們使用了werkzeug.security包中的generate_password_hash函數來進行加密,由於此函數默認使用了sha1演算法,並添加了長度為8的鹽值,所以還是相當安全的。一般用途的話也就夠用了。
- 驗證password的時候,我們需要使用werkzeug.security包中的check_password_hash函數來驗證密碼
- get_id是UserMixin類中就有的method,在這我們需要overwrite這個method。在json文件中沒有對應的user id時,可以使用uuid.uuid4()生成一個用戶唯一id
至此,我們就實現了第二步和第五步,接下來要看第三步,如何去維護一個session
維護用戶session
先看下代碼,這裡把相應代碼也放入到app.py當中
from forms import LoginFormfrom flask_wtf.csrf import CsrfProtectfrom model import Userfrom flask_login import login_user, login_requiredfrom flask_login import LoginManager, current_userfrom flask_login import logout_userapp = Flask(__name__)app.secret_key = os.urandom(24)# use login manager to manage sessionlogin_manager = LoginManager()login_manager.session_protection = stronglogin_manager.login_view = loginlogin_manager.init_app(app=app)# 這個callback函數用於reload User object,根據session中存儲的user id@login_manager.user_loaderdef load_user(user_id): return User.get(user_id)# csrf protectioncsrf = CsrfProtect()csrf.init_app(app)@app.route(/login)def login(): form = LoginForm() if form.validate_on_submit(): user_name = request.form.get(username, None) password = request.form.get(password, None) remember_me = request.form.get(remember_me, False) user = User(user_name, password) if user.verify_password(password): login_user(user) return redirect(request.args.get(next) or url_for(main)) return render_template(login.html, title="Sign In", form=form)
- 維護用戶的會話,關鍵就在這個LoginManager對象。
- 必須實現這個load_user callback函數,用以reload user object
- 當密碼驗證通過後,使用login_user()函數來登錄用戶,這時用戶在會話中的狀態就是登錄狀態了
受保護網頁
保護特定網頁,只需要對特定路由加一個裝飾器就可以,如下
# app.py# ...@app.route(/)@app.route(/main)@login_requireddef main(): return render_template( main.html, username=current_user.username)# ...
- current_user保存的就是當前用戶的信息,實質上是一個User對象,所以我們直接調用其屬性, 例如這裡我們要給模板傳一個username的參數,就可以直接用current_user.username
- 使用@login_required來標識改路由需要登錄用戶,非登錄用戶會被重定向到/login路由(這個就是由login_manager.login_view = login 語句來指定的)
用戶登出
# app.py# ...@app.route(/logout)@login_requireddef logout(): logout_user() return redirect(url_for(login))# ...
至此,我們就實現了一個完整的登陸和登出的過程。
另外我們可能還需要其它輔助的功能,諸如發送確認郵件,密碼重置,許可權分級管理等,這些功能都可以通過flask及其插件來完成,這個大家可以自己探索下啦!
推薦閱讀:
※八卦拍賣行:豹哥一時爽
※為什麼很多網站登錄失敗只提示「登錄失敗」或「帳號或密碼錯誤」,而沒有更詳細說明到底是賬號還是密碼錯誤?
※你的判斷沒錯,只是需要時間
※現在有哪些比較大的網站開通了賬號API,支持類似於使用新浪賬號登陸這樣的功能
※同一網站的多個賬號如何快速切換登錄?