Flask 學習 七 用戶認證


使用werkzeug 實現密碼散列

from werkzeug.security import generate_password_hash,check_password_hash

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True,index=True)
    role_id=db.Column(db.Integer,db.ForeignKey('roles.id'))
    password_hash=db.Column(db.String(128))
    @property
    def password(self):
        raise AttributeError('密碼不是一個可讀屬性') #只寫屬性
    @password.setter
    def password(self,password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self,password):
        return check_password_hash(self.password_hash,password)

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

密碼散列化測試tests/test_url_model.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import unittest
from app.models import User
class UserModelTestCase(unittest.TestCase):
    def test_password_setter(self):
        u = User(password='cat')
        self.assertTrue(u.password_hash is not None)
    def test_no_password_getter(self):
        u = User(password='cat')
        with self.assertRaises(AttributeError):
            u.password
    def test_password_verification(self):
        u = User(password='cat')
        self.assertTrue(u.verify_password('cat'))
        self.assertFalse(u.verify_password('dog'))
    def test_password_salts_are_random(self):
        u =User(password='cat')
        u2=User(password='cat')
        self.assertTrue(u.password_hash != u2.password_hash)
View Code

創建用戶認證藍本

app/auth/__init__.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from flask import Blueprint
auth = Blueprint('auth',__name__)
from . import views

app/auth/views.py 藍本路由和視圖函數

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from flask import render_template
from . import auth

@auth.route('/login')
def login():
    return render_template('auth/login.html')

app/__init__.py 注冊藍本

from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint,url_prefix='/auth')

安裝flask-login插件

pip install flask-login

修改User模型,支持用戶登陸 app/models.py

from flask_login import UserMixin

class User(UserMixin,db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    email=db.Column(db.String(64),unique=True,index=True)
    username = db.Column(db.String(64), unique=True,index=True)
    role_id=db.Column(db.Integer,db.ForeignKey('roles.id'))
    password_hash=db.Column(db.String(128))

app/__init__.py 初始化Flask_Login

from flask_login import LoginManager
#初始化Flask-Login
login_manager=LoginManager()
login_manager.session_protection='strong' # 記錄客戶端ip,用戶代理信息,發現異動,登出用戶,可以設置不同等級None,'basic','strong'
login_manager.login_view='auth.login' # 設置登錄頁面端點,藍本的名字也要加到前面

def create_app(config_name):
    #....
    login_manager.init_app(app) #初始化登陸
    #....

app/models.py 加載用戶的回掉函數

from . import login_manager

# 加載用戶的回調函數
@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))
# 加載用戶回掉函數,接收以Unicode字符串表示的用戶標識符,如果能找到這個用戶必須返回用戶對象,否則返回None

保護路由

為了只讓認證的用戶訪問,未認證的用戶會攔截請求,發往登陸頁面

#保護路由
@app.route('/secret')
@login_required
def secret():
    return '只有認證后的用戶才能登陸'

添加登陸表單

app/auth/forms.py

from flask_wtf import FlaskForm
from wtforms import StringField,PasswordField,BooleanField,SubmitField
from wtforms.validators import DataRequired,Length,Email

class LoginForm(FlaskForm):
    email=StringField('郵箱',validators=[DataRequired(),Length(1,64),Email()])
    password=PasswordField('密碼',validators=[DataRequired()])
    remember_me=BooleanField('保持登陸')
    submit = SubmitField('登陸')

auth/login.html  用wtf.quick_form()渲染表單

{% extends 'base.html' %}
{% import'bootstrap/wtf.html' as wtf %}
{% block title %}Flask-登陸{% endblock %}
{% block page_content %}
    <div class="page-header">
    <h1>登陸</h1>
    </div>
    <div class="col-md-4">
        {{ wtf.quick_form(form) }}
    </div>
{% endblock %}

base.html  # current_user.is_authenticated 如果是匿名用戶登陸is_authenticated返回False,這個方法可以判斷當前用戶是否登陸

<ul class="nav navbar-nav navbar-right">
            {% if current_user.is_authenticated %}
             <li><a href="{{ url_for('auth.logout') }}">退出登陸</a></li>
            {% else %}
            <li><a href="{{ url_for('auth.login') }}">立即登陸</a></li>
            {% endif %}
</ul>

登入用戶

app/auth/views.py #登陸路由

from flask import render_template,redirect,request,url_for,flash
from flask_login import login_user,login_required,logout_user,current_user
from . import auth
from ..models import User
from .forms import LoginForm

@auth.route('/login',methods=['get','post'])
def login():
    form = LoginForm()
    if form.validate_on_submit(): # 驗證表單數據
        user = User.query.filter_by(email=form.email.data).first()
        if user is not None and user.verify_password(form.password.data): # verify_password會驗證表單數據
            login_user(user,form.remember_me.data) # 如果為True則為用戶生成長期有效的cookies
            return redirect(request.args.get('next') or url_for('main.index'))# next保存原地址(從request.args字典中讀取)
        flash('用戶名或密碼無效')
    return render_template('auth/login.html',form=form)

更新登陸模板auth/login.html

{% extends 'base.html' %}
{% import'bootstrap/wtf.html' as wtf %}
{% block title %}Flask-登陸{% endblock %}
{% block page_content %}
    <div class="page-header">
    <h1>登陸</h1>
    </div>
    <div class="col-md-4">
        {{ wtf.quick_form(form) }}
    </div>
{% endblock %}

登出用戶

auth/views.py

from flask_login import login_user,login_required,logout_user,current_user

@auth.route('/logout')
@login_required
def logout():
    logout_user()
    flash('你已經退出')
    return redirect(url_for('main.index'))

測試登陸 index.html

<h1>Hello,
        {% if current_user.is_authenticated %}
            {{ current_user.username }}
        {% else %}
            訪客
        {% endif %}!
</h1>

注冊新用戶

添加用戶注冊表單

from flask_wtf import FlaskForm
from wtforms import StringField,PasswordField,BooleanField,SubmitField
from wtforms.validators import DataRequired,Length,Email,Regexp,EqualTo
from ..models import User

class RegistrationForm(FlaskForm):
    email = StringField('郵箱', validators=[DataRequired(), Length(1, 64), Email()])
    username= StringField('用戶名', validators=[DataRequired(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$',0,'用戶名必須是字母,數字,點號,下划線')])
    password = PasswordField('密碼', validators=[DataRequired(),EqualTo('password2',message='密碼不一致')])
    password2 =  PasswordField('再次輸入密碼', validators=[DataRequired()])
    submit = SubmitField('注冊')
    def validate_email(self,filed):
        if User.query.filter_by(email=filed.data).first():
            raise ValidationError('郵箱已被注冊')
    def validate_username(self,filed):
        if User.query.filter_by(username=filed.data).first():
            raise ValidationError('用戶名已被使用')

auth/register.html

{% extends 'base.html' %}
{% import'bootstrap/wtf.html' as wtf %}
{% block title %}Flask-注冊{% endblock %}
{% block page_content %}
    <div class="page-header">
    <h1>注冊</h1>
    </div>
    <div class="col-md-4">
        {{ wtf.quick_form(form) }}
    </div>
{% endblock %}

auth/login.html 添加鏈接到注冊頁面

 <p>新用戶?<a href="{{ url_for('auth.register') }}">點擊這里注冊</a></p>

app/auth/views.py

@auth.route('/register',methods=['get','post'])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        user=User(email = form.email.data,
                  username=form.username.data,
                  password=form.password.data)
        db.session.add(user)
        flash('你現在已經登陸了')
        return redirect(url_for('auth.login'))
    return render_template('auth/register.html',form=form)

確認賬戶

使用isdangerous生成確認令牌

app/models.py 確認用戶賬戶

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from flask import current_app
from . import db
class User(UserMixin,db.Model):
    # ...
    confirmed = db.Column(db.Boolean,default=False)
    def generate_confirmation_token(self,expiration=3600):  #生成令牌,有效期1小時
        s = Serializer(current_app.config['SECRET_KEY'],expiration)
        return s.dumps({'confirm':self.id})
    def confirm(self,token):  # 檢驗令牌 如果通過把confirmed字段設為True
        s=Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token)
        except:
            return False
        if data.get('confirm') !=self.id: # 檢查令牌id是否存儲是已登錄用戶進行匹配
            return False
        self.confirmed=True
        db.session.add(self)
        return True

發送確認郵件

auth/views.py

from ..email import send_mail

@auth.route('/register',methods=['get','post'])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        user=User(email = form.email.data,
                  username=form.username.data,
                  password=form.password.data)
        db.session.add(user)
        db.session.commit()
        token = user.generate_confirmation_token()
        send_mail(user.email,'確認你的賬戶','auth/email/confirm',user=user,token=token)
        flash('我們已經給你發送了一封確認郵件')
        return redirect(url_for('main.index'))
    return render_template('auth/register.html',form=form)

auth/email/cinfirm.txt

親愛的,{{ user.username }}
歡迎來到 Flasky
為了確認您的賬戶,請點擊以下鏈接:

{{ url_for('auth.confirm',token=token,_external=True) }} #返回絕對路徑

Flasky 團隊

auth/views.py

from flask_login import current_user
@auth.route('/confirm/<token>')
@login_required # 保護這個路由,用戶需要先登錄
def confirm(token):
    if current_user.confirmed:
        return redirect(url_for('main.index'))
    if current_user.confirm(token):
        flash('你已經確認了你的賬戶,謝謝')
    else:
        flash('確認鏈接失效或過期')
    return redirect(url_for('main.index'))

app/auth/views.py 使用before_app_request來全局使用處理程序中未確認的用戶

@auth.before_app_request # 如果返回響應或重定向,會直接發送至客戶端,不會調用請求視圖函數
def before_request():
    if current_user.is_authenticated\
            and not current_user.confirmed \
            and request.endpoint[:5] != 'auth.'\   #訪問路由獲取權限
            and request.endpoint != 'static':
        return redirect(url_for('auth.unconfirmed'))

@auth.route('/unconfirmed')
def unconfirmed():
    if current_user.is_anonymous or current_user.confirmed:
        return redirect(url_for('main.index'))
    return render_template('auth/unconfirmed.html')

auth/views.py 重新發送確認郵件

@auth.route('/confirm')
@login_required
def resend_confirmation():
    token=current_user.generate_confirmation_token()
    send_mail(current_user.email,'確認你的賬戶','auth/email/confirm',user=current_user,token=token)
    flash('一封新的確認郵件已經發送到您的郵箱')
    return redirect(url_for('main.index'))

管理賬戶:

修改密碼  重設密碼 修改電子郵件地址

https://github.com/Erick-LONG/flask_blog/commit/3748e70d9f986b066328185bcf417d8b8205ed8c

https://github.com/Erick-LONG/flask_blog/commit/ce4a67d31f0d7d5f5c6719eebb193b2949c77b0c

https://github.com/Erick-LONG/flask_blog/commit/f89ef3dac3819129165be4d9b2a67a2bb092cf34

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM