1. 程式人生 > >flask-login原理詳解

flask-login原理詳解

 

最近發現專案中使用的flask-login中有些bug,直接使用官網的方式確實可以用,但僅僅是可以用,對於原理或解決問題沒有什麼幫助,最近通過檢視網上資料、分析原始碼、通過demo、從零開始總結了flask-login的使用方式及內部實現原理。

先說使用,安裝元件就不說了,太簡單。

安裝好了之後就是把元件註冊到flask中,這個簡單說下,具體flask如何註冊這些擴充套件的原理後續再補上,引用官網的說法:登入管理(login manager)包含了讓你的應用和 Flask-Login 協同工作的程式碼,比如怎樣從一個 ID 載入使用者,當用戶需要登入的時候跳轉到哪裡等等。具體註冊程式碼如下:

# encoding:utf-8
from flask.ext.login import LoginManager
app = Flask(__name__)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = "login" #配置如果需要登入調整的路由

註冊好了之後,就可以使用了,以下是路由函式的寫法,具體login_required、login_user的實現原理後面會再說

@app.route('/')
@login_required                                         
#進入首頁需要判斷使用者是否登入,沒有登入則跳轉到註冊時配置的路由 def index(): return render_template('index.html') @app.route('/login', methods = ['POST', 'GET']) def login(): if request.method == 'POST': req = request.get_json() username = req['username'] userpassword = req['userpassward']
if auth_user(username, userpassword): #判斷使用者名稱密碼是否正確,這裡隨便寫一個方法,寫死使用者名稱密碼就可以 login_user(User(14,'root', 'root')) #使用者名稱密碼驗證通過呼叫login_user把user註冊到請求上下文session中,這個session其實就是一個LocalStack return url_for('index') flash(u'無效的使用者名稱或密碼') return render_template('login.html')

還差一部分,建立models模組,用處兩個,一個用來定義User類,二用來註冊回撥函式,這個回撥函式通過user_id返回User例項。這裡只是想弄清楚login的原理所有沒用資料庫,儘量聚焦

from flask.ext.login import UserMixin
from app import login_manager

class User(UserMixin):
    __tablename__ = 'users'

    def __init__(self, id,password, username):
        '''
        :param id:
        :param username:
        '''
        self.id = id                    #這個屬性一定要有,否則自己要重寫get_id方法,不信自己去試下
        self.password = password
        self.username = username

    def __repr__(self):
        return '<User %r>' % self.username


@login_manager.user_loader
def load_user(user_id):
    print user_id
    return User(14, 'root', 'root')

ok,基本使用完成了,前端再寫兩個簡單頁面就可以index.html和login.html。程式碼如下,這裡主要理解後端流程,前端能用就可以

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>hello, {{ name }}</title>
</head>
<body>
<form name="myform">
  <label>使用者名稱:</label>
  <input type="text" name="fname"/>
  <br>
  <label>密碼:</label>
  <input type="text" name="address"/>
  <button type="button" onclick="login()">提交</button>
</form>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

<script>
  function login() {
    axios({
      method: 'post',
      url: '/login',
      data: {
        username: myform.fname.value,
        userpassward: myform.address.value
      }
    }).then(function (response) {
      location.href = response.data
    }).catch(function (error) {
      console.log(error);
    });
  }
  
</script>

</body>
</html>
login
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
ceshi
<button type="button" onclick="login()">提交</button>
</body>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
  {#  傳送使用者名稱、密碼#}
  function login() {
    axios({
      method: 'post',
      url: '/test',
      data: {
        username: 'c',
        userpassward: 'y'
      }
    }).then(function (response) {
      console.log(response.data);
    }).catch(function (error) {
      console.log(error);
    });
  }
</script>
</html>
index.html

具體使用過程梳理完成。

再看看其實現的原理,後端路由接收到前端請求後,會進入login_require裝飾器中,而此裝飾器用來判斷使用者鑑權情況,如下圖:

 

 那使用者登入鑑權的關鍵在裝飾器@login_required中,先看下整體的鑑權流程,再深入各個部分,下圖為鑑權的整體流程圖

此圖對應的程式碼

def login_required(func):
    @wraps(func)
    def decorated_view(*args, **kwargs):
        if current_app.login_manager._login_disabled:
            return func(*args, **kwargs)
        elif not current_user.is_authenticated:
            return current_app.login_manager.unauthorized()
        return func(*args, **kwargs)
    return decorated_view
login_required

流程解析

1. 判斷請求是否需要鑑權,current_app.login_manager._login_disabled,通常的命令get、post等請求都需要鑑權,此屬性為False。

2. 判斷當前使用者是否鑑權,current_user.is_authenticated。current_user--------從哪獲取?繼續分析程式碼

current_user = LocalProxy(lambda: _get_user())

current_user通過LoaclProxy建立了一個無名函式_get_user,為什麼用lambda:_get_user()而不直接使用_get_user?我也沒想明白,有牛人可以解釋一下。

LoaclProxy具體可以參考https://www.jianshu.com/p/3f38b777a621,說白了就是理解為把方法地址傳給變數,以後可以動態呼叫代理方法

獲取current_user通過_get_user()函式,先看下該函式的程式碼

def _get_user():
    if has_request_context() and not hasattr(_request_ctx_stack.top, 'user'):
        current_app.login_manager._load_user()

    return getattr(_request_ctx_stack.top, 'user', None)

解釋一下,首先需要知道請求上下文,程式碼中的_request_ctx_stack.top中是否有user這個變數,如果沒有則_load_user,如果有直接取這個user屬性返回給前面的說用來鑑權使用。

 _load_user會呼叫reload_user函式,

    def reload_user(self, user=None):
        ctx = _request_ctx_stack.top

        if user is None:
            user_id = session.get('user_id')
            if user_id is None:
                ctx.user = self.anonymous_user()
            else:
                if self.user_callback is None:
                    raise Exception(
                        "No user_loader has been installed for this "
                        "LoginManager. Add one with the "
                        "'LoginManager.user_loader' decorator.")
                user = self.user_callback(user_id)
                if user is None:
                    ctx.user = self.anonymous_user()
                else:
                    ctx.user = user
        else:
            ctx.user = user

該函式判斷_request_ctx_stack.top賦值user物件,物件可以傳入,也可以使用我們在使用過程中定義的def load_user(user_id),這個user_callback就是我們定義的load_user函式。

 獲取user如圖

登入時和重新請求時都會將user放入到棧中,每次請求後都出清理top中的user。放入top時會設定user_id到session中。