使用werkzeug 實現密碼散列html
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.pypython
#!/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)
建立用戶認證藍本git
app/auth/__init__.pygithub
#!/usr/bin/env python # -*- coding:utf-8 -*- from flask import Blueprint auth = Blueprint('auth',__name__) from . import views
app/auth/views.py 藍本路由和視圖函數flask
#!/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 註冊藍本bootstrap
from .auth import auth as auth_blueprint app.register_blueprint(auth_blueprint,url_prefix='/auth')
安裝flask-login插件cookie
pip install flask-loginsession
修改User模型,支持用戶登錄 app/models.pyapp
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_Logindom
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/3748e70d9f986b066328185bcf417d8b8205ed8chttps://github.com/Erick-LONG/flask_blog/commit/ce4a67d31f0d7d5f5c6719eebb193b2949c77b0chttps://github.com/Erick-LONG/flask_blog/commit/f89ef3dac3819129165be4d9b2a67a2bb092cf34