1. 程式人生 > >使用Flask實現使用者登陸認證的詳細過程

使用Flask實現使用者登陸認證的詳細過程

使用者認證的原理

在瞭解使用Flask來實現使用者認證之前,我們首先要明白使用者認證的原理。假設現在我們要自己去實現使用者認證,需要做哪些事情呢?

  1. 首先,使用者要能夠輸入使用者名稱和密碼,所以需要網頁和表單,用以實現使用者輸入和提交的過程。
  2. 使用者提交了使用者名稱和密碼,我們就需要比對使用者名稱,密碼是否正確,而要想比對,首先我們的系統中就要有儲存使用者名稱,密碼的地方,大多數後臺系統會通過資料庫來儲存,但是實際上我們也可以簡單的儲存到檔案當中。(為簡明起見,本文將使用者資訊儲存到json檔案當中)
  3. 登入之後,我們需要維持使用者登入狀態,以便使用者在訪問特定網頁的時候來判斷使用者是否已經登入,以及是否有許可權訪問改網頁。這就需要有維護一個會話來儲存使用者的登入狀態和使用者資訊。
  4. 從第三步我們也可以看出,如果我們的網頁需要許可權保護,那麼當請求到來的時候,我們就首先要檢查使用者的資訊,比如是否已經登入,是否有許可權等,如果檢查通過,那麼在response的時候就會將相應網頁回覆給請求的使用者,但是如果檢查不通過,那麼就需要返回錯誤資訊。
  5. 在第二步,我們知道要將使用者名稱和密碼儲存起來,但是如果只是簡單的用明文儲存使用者名稱和密碼,很容易被“有心人”盜取,從而造成使用者資訊洩露,那麼我們實際上應當將使用者資訊尤其是密碼做加密處理之後再儲存比較安全。
  6. 使用者登出

通過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.py
from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField, PasswordField
from wtforms.validators import DataRequired

# 定義的表單都需要繼承自FlaskForm
class 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.py

from werkzeug.security import generate_password_hash
from werkzeug.security import check_password_hash
from flask_login import UserMixin
import json
import uuid

# define profile.json constant, the file is used to
# save user name and password_hash
PROFILE_FILE = "profiles.json"

class User(UserMixin):
    def __init__(self, username):
        self.username = username
        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):
        password_hash = self.get_password_hash()
        if 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 LoginForm
from flask_wtf.csrf import CsrfProtect
from model import User
from flask_login import login_user, login_required
from flask_login import LoginManager, current_user
from flask_login import logout_user

app = Flask(__name__)

app.secret_key = os.urandom(24)

# use login manager to manage session
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'login'
login_manager.init_app(app=app)

# 這個callback函式用於reload User object,根據session中儲存的user id
@login_manager.user_loader
def load_user(user_id):
    return User.get(user_id)


# csrf protection
csrf = 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)
        if user.verify_password(password):
            login_user(user, remember=remember_me)
            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_required
def 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_required
def logout():
    logout_user()
    return redirect(url_for('login'))
# ...

至此,我們就實現了一個完整的登陸和登出的過程。

另外我們可能還需要其它輔助的功能,諸如傳送確認郵件,密碼重置,許可權分級管理等,這些功能都可以通過flask及其外掛來完成,這個大家可以自己探索下啦!



作者:geekpy
連結:https://www.jianshu.com/p/06bd93e21945
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。