---
# System prepended metadata

title: Python + Flask 虛擬美國股票交易網站 Part5 （使用Flask-Login管理用戶認證）
tags: [Flask, CS50, Python]

---

<style>

html, body, .ui-content {
    background-color: #333;
    color: #ddd;
}

body > .ui-infobar {
    display: none;
}

.ui-view-area > .ui-infobar {
    display: block;
}

.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
    color: #ddd;
}

.markdown-body h1,
.markdown-body h2 {
    border-bottom-color: #ffffff69;
}

.markdown-body h1 .octicon-link,
.markdown-body h2 .octicon-link,
.markdown-body h3 .octicon-link,
.markdown-body h4 .octicon-link,
.markdown-body h5 .octicon-link,
.markdown-body h6 .octicon-link {
    color: #fff;
}

.markdown-body img {
    background-color: transparent;
}

.ui-toc-dropdown .nav>.active:focus>a, .ui-toc-dropdown .nav>.active:hover>a, .ui-toc-dropdown .nav>.active>a {
    color: white;
    border-left: 2px solid white;
}

.expand-toggle:hover, 
.expand-toggle:focus, 
.back-to-top:hover, 
.back-to-top:focus, 
.go-to-bottom:hover, 
.go-to-bottom:focus {
    color: white;
}


.ui-toc-dropdown {
    background-color: #333;
}

.ui-toc-label.btn {
    background-color: #191919;
    color: white;
}

.ui-toc-dropdown .nav>li>a:focus, 
.ui-toc-dropdown .nav>li>a:hover {
    color: white;
    border-left: 1px solid white;
}

.markdown-body blockquote {
    color: #bcbcbc;
}

.markdown-body table tr {
    background-color: #5f5f5f;
}

.markdown-body table tr:nth-child(2n) {
    background-color: #4f4f4f;
}

.markdown-body code,
.markdown-body tt {
    color: #eee;
    background-color: rgba(230, 230, 230, 0.36);
}

a,
.open-files-container li.selected a {
    color: #5EB7E0;
}


</style>}
# Python + Flask 虛擬美國股票交易網站 Part5 （使用Flask-Login管理用戶認證）
###### tags: `CS50` `Python` `Flask`

## 前言
### Flask-Login為Flask提供了用戶session管理功能，可以輕鬆地處理登入登出等功能。


下表為Flask-Login要求用戶類實現的方法和屬性

| Class            | Introduction |
| ---------------- | ------------ |
| is_authenticated | 如果用戶已經通過認證，返回True, 否則返回False|
| is_active        | 如果允許用戶登錄，返回True, 否則返回False   |
| is_anonymous     | 如果當前用戶未登錄（匿名），返回True, 否則返回False |
| get_id()         | 以Unicode形式返回用戶的唯一標示符，返回True, 否則返回False |

# 

### 先安裝flask-login
```
$ pip3 install flask-login
```

### 然後在 vfinance/extensions.py 以及工廠函數中調用此方法

#### vfinance/extensions.py

```
from flask_login import LoginManager

...
login_manager = LoginManager()
```
#### vfinance/__init __.py
```
login_manager.init_app(app)
```
### 接著讓用戶類繼承Flask-Login提供的UserMixin類
#### vfinance/models.py
```
from flask_login import UserMixin

class User(db.Model, UserMixin):
    ...
```
UserMixin代表已經通過認證的用戶，所以is_authenticated和is_active會回傳True，而is_anonymous則會傳False。get_id()會查找用戶兌現的id屬性值作為id，也就是`Class User`中的`Primary key`

> 使用FlaskLogin登入/登出某个用户非常简单，只需要在视图函数中调用FlaskLogin提供的login_user（）或logout_user（）函数，并传入要登入/登出的用户类对象。在这两个函数背后，FlaskLogin使用Flask的session对象将用户的id值存储到用户浏览器的cookie中（名为user_id），这时表示用户被登入。相对来说，登出则意味着在用户浏览器的cookie中删除这个值。默认情况下，关闭浏览器时，通过Flask的session对象存储在客户端的sessioncookie会被删除，所以用户会登出。另外，FlaskLogin还支持记住登录状态，通过在login_user（）中将remember参数设为True即可实现。这时FlaskLogin会在用户浏览器中创建一个名为remember_token的cookie，当通过session设置的user_idcookie因为用户关闭浏览器而失效时，它会重新恢复user_idcookie的值。
[李辉 - Flask Web开发实战 helloflask](http://helloflask.com/book/1/)  


## 獲取當前用戶
使用Flask-Login提供的current_user對象，表示當前用戶。調用時會傳回與當欠用戶對應的用戶模型類對象。因為session中只會儲存登錄用戶的id，所以為了讓他返回對應的用戶對象，要設置一個用戶加載函數。這個函數要用login_manager.user_loader的裝飾器，接受用戶id作為參數，傳回對應的用戶對象

#### vfinance/extension.py
```
...
@login_manager.user_loader
def load_user(user_id):
    from bluelog.models import Admin
    user = Admin.query.get(int(user_id))
    return user
```

設置後，調用current_user時，Flask-Login會調用用戶加載函數並傳回對應的用戶對象。如果當前用戶已經登錄，會返回Admin類實例；如果用戶未登錄，current_user默認會返回Flask-Login內建的AnonymousUserMixin類對象，它的is_authenticated和is_active屬性會返回False，而is_anonymous屬性會返回True。 

最後可以通過對current_user對象調用is_authenticated等屬性來判斷當前用戶的認證狀態，它也已經註冊到了模板上下文中，因此可以在模板中根據用戶狀態渲染不同的內容。

例如：

```
<small>　

    {%if current_user.is_authenticated%}　　　
        # 如果用戶已經登入就顯示登出按鈕
        <ahref="{{url_for('auth.logout',next=request.full_path)}}">Logout</a>　　
        
    {% else %}　　　
        # 如果用戶沒有已經登入就顯示登入按鈕
        <ahref="{{url_for('auth.login',next=request.full_path)}}">Login</a>　　
        
    {% endif %}

</small>


```

## 註冊/登入/登出 vfinance/blueprints/auth.py 

### 登入
```
import os
from flask import Blueprint, render_template, flash, redirect, url_for
from flask_login import login_user, logout_user, login_required, current_user

from vfinance.extensions import db
from vfinance.forms import LoginForm, RegisterForm
from vfinance.models import User
from vfinance.utils import redirect_back
from sqlalchemy.exc import IntegrityError

@auth_bp.route("/login", methods = ["GET", "POST"])
def login():
    form = LoginForm()
    if current_user.is_authenticated:
        # flash('You are already loging','success')
        return redirect(url_for("home.index"))
    
    if form.validate_on_submit():
        username = form.username.data
        password = form.password.data
        remember = form.remember.data
        users = User.query.all()
        
        for user in users:
            if username in user.username:
                if user.validate_password(password):
                    login_user(user,remember)
                    flash('Welcome Back '+user.username, 'info')
                    return redirect_back()
                # flash("Invalid username or password.", 'warning')
                else:
                    flash("Invalid username or password.", 'warning')
```


#### vfinance/templates/auth/login.html
```
{% extends 'base.html' %}
{% from 'bootstrap/form.html' import render_form %}

{% block title %}Login{% endblock %}

{% block content %}
    <div class="container h-100">
        <div class="row h-100 page-header justify-content-center align-items-center">
            <h1>Log in</h1>
        </div>
        <div class="row h-100 justify-content-center align-items-center">
            {{ render_form(form, extra_classes='col-6') }}
        </div>
    </div>
{% endblock %}

{% block footer %}{% endblock %}

```


### 註冊
```
@auth_bp.route("/register", methods = ["GET", "POST"])
def register():
    form = RegisterForm()
    
    if form.validate_on_submit():
        username = form.username.data
        password = form.password.data
        cash = 1000000
        user = User(
            username = username,
            cash = cash,
            position = 0
        )
        user.set_password(password)
        db.session.add(user)
        try:
            db.session.commit()
            flash("Thanks for registering! Please login!", 'success' )
            
        except IntegrityError:
            flash("Account already exists", 'danger')
            db.session.rollback()
            return redirect_back()
        return redirect(url_for('home.index'))
        

    return render_template("auth/register.html", form = form)

```
#### #### vfinance/templates/auth/register.html
```
{% extends "base.html" %}
{% from 'bootstrap/form.html' import render_form %}

{% block title %}
    Register
{% endblock title %}

{% block content%}
    <div class="container h-100">
        <div class="row h-100 page-header justify-content-center align-items-center">
            <h1>Register</h1>
        </div>
        <div class="row h-100 justify-content-center align-items-center">
            {{ render_form(form, extra_classes='col-6') }}
        </div>
    </div>  

    
{% endblock %}

{% block footer %}
    
{% endblock footer %}
```

### 登出
```
@auth_bp.route('/logout')
@login_required
def logout():
    logout_user()
    flash('Logout success.', 'info')
    return redirect_back()
```

## 視圖函數保護

程式許多操作需要登錄後才能進行，因此要將那些需要登入後才能使用的funciton保護起來，如果用戶在未登錄狀態下訪問了某個需要登錄才能使用的資源，程式會重新定位到登入頁面

視圖保護可以用Flask-Login提供的login_required裝飾器，例如登出函數：
```
from flask_login import login_required

@auth_bp.route('/logout')
@login_required
def logout():
    logout_user()
    flash('Logout success.', 'info')
    return redirect_back()
```

但在這之前還需要在extension.py腳本中使用login_manager對象的login_view屬性設置登錄試圖的端點值：
```
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Helloooo'
login_manager.login_message_category = 'warning'
```

這樣一來當在未登錄狀態下訪問需要登錄的頁面時，程式就會自動導到登錄頁面，並顯示提示消息。