
图片来自 flask 官网(http://flask.pocoo.org/)
借着人工智能大数据的这股东风,Python 着实又火了一把。之所以用 “又” 字,是因为 Python 很早就走进了广大程序员的眼中。其简洁的语言,丰富而完整的社区,都是广大 Python 爱好者的福音。
本篇,主要从如下几方面来带你走进 Python 的世界。
- Python 语法简介
- Flask 框架简介
- Web 代码架构
- 从头完成一个完整的项目
Python 语法简介
Python 简介
Python 是著名的 “龟叔”Guido van Rossum 在 1989 年圣诞节期间,为了打发无聊的圣诞节而编写的一个编程语言。
下面最近 10 年最常用的 10 种编程语言的变化图:

图片来自 TIOBE 网站(https://www.tiobe.com/tiobe-index/)
Python 作为一个极易入门的语言,其简洁的语法绝对居功至伟。
一般来说,推荐使用 PyCharm 来编写 Python 程序,下面来看下最简单的 Hello World。

可以看到,Python 作为一个极易入门的语言,其简洁的语法绝对居功至伟。
再来看一个例子,来体会下 Python 的简洁。

可以看到,本里中用到了 Python 的注释、列表定义、函数定义和调用等。
因为本文主要还是关注于入门的实战知识,即基于 Flask 框架的鉴权系统的编写,所以对于 Python 的基础简介就到这里了,如果有同学还有疑问,或者对于 Python 的基础语法不是很理解,可以私下找我,一起交流。
Flask 简介
Flask 是一款轻量级的 Python Web 框架,据 Python 官方统计,其流行程度已经超过 Django 成为最流行的 Python Web 框架。首先 pip install Flask,然后就能愉快的体验啦。
下面我们换一种形式来 Hello World 一下:
from flask import Flask
App = Flask(__name__)
mylist = []
@App.route('/')
def index():
return "Hello World"
def myfunc():
for i in range(0, 10):
mylist.Append(i)
return mylist
if __name__ == "__main__":
App.run(debug=True)
现在运行这个脚本,就可以看到在本地的 5000 端口启动了一个服务,然后我们在浏览器中输入 “http://127.0.0.1:5000”,访问下试试吧,一个 Web 版本的 Hello World 就完成啦。

Web 页面为:

好,更多的 Python 和 Flask 使用方法,我们在实战中再详细介绍。
项目 Web 代码框架
结构解析
做一个项目,代码结构尤为重要,好的代码结构,可以让人一眼就能被你的项目所吸引,同时也能给后面接手你代码的人减轻工作量。本文使用的代码结构如下:

在项目目录的第一层,是 App 文件夹和其他公共库。
- App 文件夹,Flask 程序都保存在该文件夹内;
- config.py,用于存放项目的配置信息;
- guncorn,guncron 是一个高性能的 Python HTTP 代理服务器组件,该文件保存相关配置;
- manage.py,用于启动程序以及其他的程序任务;
- myweb.sqlite3,项目使用的数据库;
- requirements,本项目涉及的所有第三方库;
- run.sh,部署到 Linux 服务器时使用的启动脚本;
- sendemail.py,发送 Email 的工具;
- sendsms.py,发送短信的工具;
再来看 App 文件夹内部,包括 api_1_0,auth,main,static,templates,init.py,models.py。
-
api_1_0,其实本项目不涉及,可以作为以后项目扩展 API 接口使用;
-
auth 和 main,作为 Flask 项目的两个主要文件夹,分别存放鉴权代码和其他主逻辑代码;
分别包含如下代码模块:
views.py,存放路由函数,即处理逻辑;
form.py,存放页面表单;
__init__.py,创建蓝本,使用工厂函数;
error.py,定义页面 404 和 500 错误;
-
static,存放项目的图片、CSS 等文件;
-
templates 文件夹,存放 HTML 模板文件;
-
init.py 文件,定义工程函数;
-
models.py,存放数据库模型。
Flask 项目工作流程
我们先写一个简单的 demo,来理解下 Flask 项目的工作流程是怎样的。首先,先使用 pip install -r requirements 来安装所有的依赖包(文件包含内容见文尾),然后在项目顶级目录下创建 App 文件夹、config.py 和 manage.py 文件,在 App 文件夹中创建 main、templates 文件夹、models.py 和 __init**.py 文件,最后在 main 文件夹中创建 views.py、forms.py 和 _**_**init**.py 文件。
准备好以上之后,我们一起来动手吧。
在 config 文件中添加如下代码:
import os
basedir = os.path.abspath(os.path.dirname(__file__))
SECRET_KEY = "hardtoguess"
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'myweb.sqlite3')
设置一个加密参数,和一个数据库连接串。
在 main 的 init 文件中添加如下代码:
from flask import Blueprint
main = Blueprint('main', __name__)
from . import views
创建蓝本,通过实例化一个 Blueprint 类对象可以创建蓝本。这个构造函数有两个必须指定的参数: 蓝本的名字和蓝本所在的包或模块。
在 App 的 init 文件中添加如下代码:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
from flask_bootstrap import Bootstrap
import config
db = SQLAlchemy()
cors = CORS()
bootstrap = Bootstrap()
def create_App():
App = Flask(__name__)
App.config['SECRET_KEY'] = config.SECRET_KEY
App.config['SQLALCHEMY_DATABASE_URI'] = config.SQLALCHEMY_DATABASE_URI
App.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
db.init_App(App)
cors.init_App(App, supports_credentials=True)
bootstrap.init_App(App)
from .main import main as main_blueprint
App.register_blueprint(main_blueprint)
return App
引入 CORS() 来实现跨站请求伪造保护,引入 Bootstrap(),一个前端框架,通过 create_App() 来生成 App,然后通过 run() 函数运行程序。
下面开始定义数据库模型,在 models 文件添加代码:
from . import db
class TestTable(db.Model):
__tablename__ = 'test'
id = db.Column(db.Integer, primary_key=True)
testcolumn1 = db.Column(db.String(32))
testcolumn2 = db.Column(db.String(32))
定义了两个字符串类型的字段和一个整型的字段,稍后,我们通过命令在数据库中创建该表。
对应的 forms 文件,代码如下:
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
class TestForm(FlaskForm):
Column1 = StringField('Column1')
Column2 = StringField('Column2')
submit = SubmitField('Submit')
定义一个表单,该表单里会提交两个字段。
再来看看 views 文件,代码如下:
from App import db
from . import main
from flask import render_template, redirect, url_for
from ..models import TestTable
from .forms import TestForm
@main.route('/create', methods=['GET', 'POST'])
def createtable():
db.create_all()
return "创建数据库表成功"
@main.route('/drop', methods=['GET', 'POST'])
def droptable():
db.drop_all()
return "删除数据库表成功"
@main.route('/', methods=['GET', 'POST'])
def index():
form = TestForm()
print('test')
print(form)
if form.validate_on_submit():
data = TestTable(testcolumn1=form.Column1.data, testcolumn2=form.Column2.data)
db.session.add(data)
db.session.commit()
return redirect(url_for('main.index'))
return render_template('index.html', form=form)
这里我们定义了三个 URL,分别为 “/”,“/create” 和 “/drop”,“/” 会展示一个表单,如同我们定义的那样,而 “/create” 和 “/drop” 会分别创建和删除数据库表。
最后再编写页面模板,在 templates 目录下创建 index.html 文件,添加代码如下:
{% import "bootstrap/wtf.html" as wtf %}
<div class="container">
<h1>Test</h1>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
</div>
Flask-Bootstrap 提供了一个非常高端的辅助函数 quick_form(),用来快速渲染表单。
我们来看看效果
访问根 URL 效果:

访问 “/create” 效果:

让我们连接到数据库上看看表是否创建成功了:

可以看到确实正确创建了。
下面我们提交表单实验下

点击 Submit 按钮后,到数据库中查看

表单的信息也正确插入到数据库了。
剩下的 “/drop”,就留给你来验证啦。
好了,现在让我们来一起整理下 Flask 的工作流程:
- 如果涉及到数据库操作,就需要引入相应的 ORM 组件,例如 flask_sqlalchemy,并在 models 中定义表结构;
- 如果涉及到表单提交,需要引入 flask_cors 来做跨站伪造安全保护,并在 form 中定义表单结构;
- 在 views 中定义路由函数和后台处理逻辑,例如插入数据库等操作;
- 在 templates 中定义页面模板,用于前台展示,一般使用 flask_bootstrap 扩展库;
- 在项目级别的 init 文件中创建初始化函数,初始化各个组件,如 flask, flask_sqlalchemy,flask_bootstrap 等并注册蓝本;
- 在程序级别的 init 文件中创建蓝本;
- 运行 Flask 程序。
完整鉴权系统
相信通过上面的小 demo 和最后的总结,你已经大致了解了 Flask 的工作流程,下面我们就来一起完成这个鉴权系统的搭建。
Flask 的认证扩展
这里用到了 flask-login、Werkzeug 和 flask-github 三个第三方库:
flask-login:管理已登陆用户的用户会话 flask-github:一个集成 GitHub 鉴权的第三方库 Werkzeug:计算密码散列值并进行核对
这里先重点说下 Werkzeug 库,我们就使用该库来处理用户密码散列,即在数据库中,我们不存储用户的明文密码,而是存储密码的散列值。
Werkzeug 中的 security 模块能够很方便地实现密码散列值的计算。这一功能的实现只需要两个函数,分别用在注册用户和验证用户阶段。
generate_password_hash(password,method=pbkdf2:sha1,salt_length=8)_ _这个函数将原始密码作为输入,以字符串形式输出密码的散列值,输出的值可保存在用户数据库中。 method 和 salt_length 的默认值就能满足大多数需求。
check_password_hash(hash, password) 这个函数的参数是从数据库中取回的密码散列值和用户输入的密码。返回值为 True 表明密码正确。
让我们在实际应用中来深入理解 Werkzeug 的实现。
还记得最开始我们给出的代码结构嘛(可不是 demo 的代码结构哦),我们在 models 文件中加入如下数据模型:
from . import db
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from . import login_manager
import time
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from flask import current_App, request
import hashlib
class WebUser(UserMixin, db.Model):
__tablename__ = 'webuser'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.String(64), unique=True, index=True)
email = db.Column(db.String(64), unique=True, index=True)
username = db.Column(db.String(64), unique=True, index=True)
password_hash = db.Column(db.String(128))
@property
def password(self):
raise AttributeError('You can not read the password')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
if self.password_hash is not None:
return check_password_hash(self.password_hash, password)
计算密码散列值的函数通过名为 password 的只写属性实现。设定这个属性的值时,赋值方法会调用 Werkzeug 提供的 generate_password_hash() 函数,并把得到的结果赋值给 password_hash 字段。如果试图读取 password 属性的值,则会返回错误,原因很明显,因为生成散列值后就无法还原成原来的密码了。
verify_password 方 法 接 受 一 个 参 数( 即 密 码 ), 将其传给 Werkzeug 提供的 check_ password_hash() 函数,和存储在 WebUser 模型中的密码散列值进行比对。如果这个方法返回 True,就表明密码是正确的。
使用 Flask-Login 认证用户
Flask-Login 的 UserMix 类,提供了如下几种必须的方法:
is_authenticated(): 如果用户已经登录,必须返回 True,否则返回 False; is_active(): 如果允许用户登录,必须返回 True,否则返回 False。如果要禁用账户,可以返回 False; is_anonymous(): 对普通用户必须返回 False; get_id(): 必须返回用户的唯一标识符,使用 Unicode 编码字符串;
这样我们就可以在 WebUser 的实例中直接调用上面的这些方法了。接下来,将 Flask-Login 在程序的工厂函数中初始化,如下:
...
from flask_login import LoginManager
...
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'
def create_App(config_name):
App = Flask(__name__)
...
login_manager.init_App(App)
...
from .main import main as main_blueprint
App.register_blueprint(main_blueprint)
from .auth import auth as auth_blueprint
App.register_blueprint(auth_blueprint, url_prefix='/auth')
return App
login_view 属性设置登录页面的端点,即登陆的路由函数。 session_protection 设为'strong' 时,Flask-Login 会记录客户端 IP 地址和浏览器的用户代理信息,如果发现异动就登出用户。
最后 Flask-Login 要求程序实现一个回调函数,使用指定的标识符加载用户,我们在 models 中添加代码如下:
@login_manager.user_loader
def load_user(user_id):
return WebUser.query.get(int(user_id))
这里重点说下数据库的操作方法:
WebUser.query.all() 可以查询出数据库中 webuser 表的所有记录;
WebUser.query.get(int(user_id)) 会获取到 user_id 数值对应的记录信息; WebUser.query.filter_by(username=zhangsan).first() 会获取到用户名字段为 zhangsan 的记录信息。
本地登陆
保护路由装饰器 为了保护路由只让认证用户访问,Flask-Login 提供了一个 login_required 修饰器。被该修饰器修饰的函数路由,只允许登陆的用户访问。
添加登陆表单
呈现给用户的登录表单中包含一个用于输入电子邮件地址的文本字段、一个密码字段、一 个 “记住我” 复选框和提交按钮。 在 App/auth/forms.py 文件中,添加如下代码:
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField, IntegerField, FormField, FieldList
from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo
from ..models import WebUser
from wtforms import ValidationError
class LoginForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Length(1, 64), Email()])
password = PasswordField('Password', validators=[DataRequired()])
submit = SubmitField('Log In')
电子邮件字段用到了 WTForms 提供的 Length() 和 Email() 验证函数。PasswordField 类表示属性为 type="password" 的元素。BooleanField 类表示复选框。
对应的登陆页面模板保存在 templates/auth/login.html 中,它继承自 base.html 模板。
{% extends "bootstrap/base.html" %}
{% block title %}Flasky{% endblock %}
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">WebAuth</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
<ul class="nav navbar-nav">
<li><a href="/needconfirm">NeedConfirm</a></li>
</ul>
<ul class="nav navbar-nav">
<li><a href="/onlyadmin">OnlyAdmin</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Account<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="/manageuser">Manage User</a></li>
<li><a href="{{ url_for('main.user', username=current_user.username) }}">Profile</a></li>
<li><a href="{{ url_for('auth.logout') }}">Sign Out</a></li>
</ul>
</li>
{% else %}
<li><a href="{{ url_for('auth.login') }}">Sign In</a></li>
{% endif %}
</ul>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="container">
{% block page_content %}{% endblock %}
</div>
{% endblock %}
login.html 模板代码:
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Login{% endblock %}
{% block page_content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">×</button>
{{ message }}
</div>
{% endfor %}
</div>
<div class="page-header">
<h1>Login</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
模板语言: 使用 {{}} 来渲染变量,使用 {% if %} {% else %} {% endif %} 来做选择控制,使用 {% for %} {% endfor %} 来做循环控制。
用户登陆
在 App/auth/views.py 中添加登陆代码:
@auth.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = WebUser.query.filter_by(email=form.email.data).first()
if user is not None:
if user.verify_password(form.password.data):
login_user(user)
return redirect(request.args.get('next') or url_for('main.index'))
flash('Invalid username or password!')
return render_template('auth/login.html', form=form)
当请求类型是 GET 时,视图函数直接渲染模板,即显示表单。当表单在 POST 请求中提交时, Flask-WTF 中的 validate_on_submit() 函数会验证表单数据,然后尝试登入用户。
用户登出
用户登出就比较简单了,定义 logout 函数,调用 logout_user() 即可:
@auth.route('/logout')
@login_required
def logout():
logout_user()
if 'userid' in session:
session.pop('userid')
flash('You have logged out!')
return redirect(url_for('main.index'))
这样,登陆,登出功能就完成了!
我们来看下登陆页面

好像美观程度堪忧啊,我们这里到网上找一个美观些的登陆模板,来移植到我们的项目中。
模板效果如下,看起来还不错哦:

下面我们就来把这个页面和我们的后台逻辑关联到一起吧
首先在 auth/views.py 中添加新的登陆逻辑:
@auth.route('/loginnew', methods=['GET','POST'])
def login_new():
form = LoginForm()
if form.validate_on_submit():
user = WebUser.query.filter_by(email=form.email.data).first()
if user is not None and user.verify_password(form.password.data):
login_user(user)
return redirect(request.args.get('next') or url_for('main.index'))
flash('Invalid username or password.')
return render_template('login/login.html', form=form)
可以看到,我是准备把新的登陆模板放到 login 目录下了,在 templates 目录下新建 login 目录,并创建 login.html 文件,拷贝页面的 html 代码并稍作修改:
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block navbar %}{% endblock %}
{% block title %}Flask - Login{% endblock %}
{% block head %}
<link rel="shortcut icon" href="/static/icon/background-dragonv2.jpg">
<link rel="icon" href="/static/icon/background-dragonv2.jpg" type="image/x-icon">
<link rel="stylesheet" href="{{ url_for('static', filename='login/css/style.css') }}">
<link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet">
<link rel='stylesheet prefetch' href='https://fonts.googleapis.com/icon?family=Material+Icons'>
{% endblock %}
{% block header %}{% endblock %}
{% block cover %}{% endblock %}
{% block content %}
<body>
<div class="cotn_principal">
<div class="cont_centrar">
<div class="cont_login">
<div class="cont_info_log_sign_up">
<div class="col_md_login">
<div class="cont_ba_opcitiy">
<h2>LOGIN</h2>
<p>Lorem ipsum dolor sit amet, consectetur.</p>
<button class="btn_login" onClick="cambiar_login()">LOGIN</button>
</div>
</div>
<div class="col_md_sign_up">
<div class="cont_ba_opcitiy">
<h2>SIGN UP</h2>
<p>Lorem ipsum dolor sit amet, consectetur.</p>
<button class="btn_sign_up" onClick="cambiar_sign_up()">SIGN UP</button>
</div>
</div>
</div>
<div class="cont_back_info">
<div class="cont_img_back_grey"> <img src="/static/login/jiangshan.jpg" alt="" /> </div>
</div>
<div class="cont_forms" >
<div class="cont_img_back_"> <img src="/static/login/jiangshan.jpg" alt="" /> </div>
<div class="cont_form_login"> <a href="#" onClick="ocultar_login_sign_up()" ><i class="material-icons"></i></a>
<h2>LOGIN</h2>
<input name="email" type="text" id="email" placeholder="Email" />
<input name="password" type="password" id="password" placeholder="Password" />
<button type="submit" class="btn_login" onClick="myLogin()">LOGIN</button>
</div>
<div class="cont_form_sign_up"> <a href="#" onClick="ocultar_login_sign_up()"><i class="material-icons"></i></a>
<h2>SIGN UP</h2>
<input name="email1" type="text" id="email1" placeholder="Email" />
<input name="name1" type="text" id="name1" placeholder="User" />
<input name="password1" id="password1" type="password" placeholder="Password" />
<input name="cmpassword1" id="cmpassword1" type="password" placeholder="Confirm Password" />
<button type="submit" class="btn_sign_up" onClick="sign_up()">SIGN UP</button>
</div>
</div>
</div>
</div>
</div>
<script src="/static/login/js/index.js"></script>
<script>
function myLogin(){
var data = {};
data['email'] = $("#email").val();
data['password'] = $("#password").val();
$.ajax(
{
type:'POST',
url: '{{ url_for('auth.login_check') }}',
data: data,
{# dataType:'json',#}
success:function (data) {
if ( data == 'error' ){
alert("Invalid username or password.");
}else if (data == 'success'){
window.location.href="{{ url_for('main.index') }}";
}
},
error:function (xhr, type) {
}
}
);
}
function sign_up(){
var data = {};
data['email1'] = $("#email1").val();
data['password1'] = $("#password1").val();
data['name1'] = $("#name1").val();
$.ajax(
{
type:'POST',
url: '{{ url_for('auth.register_check') }}',
data: data,
{#dataType:'json',#}
success:function (data) {
if ( data == 'error' ){
alert("Email had been registed!");
}else if (data == 'success'){
window.location.href="{{ url_for('main.index') }}";
}
},
error:function (xhr, type) {
}
}
);
}
</script>
</body>
{% endblock %}
{% block footer %}{% endblock %}
这还个页面还是继承自 base 模板,同时使用 jQuery 来提交请求。 函数 myLogin() 就是登陆时提交数据的逻辑
function myLogin(){
var data = {};
data['email'] = $("#email").val();
data['password'] = $("#password").val();
$.ajax(
{
type:'POST',
url: '{{ url_for('auth.login_check') }}',
data: data,
{
success:function (data) {
if ( data == 'error' ){
alert("Invalid username or password.");
}else if (data == 'success'){
window.location.href="{{ url_for('main.index') }}";
}
},
error:function (xhr, type) {
}
}
);
}
首先获取到输入框的 email 和 password,然后想一个新的后台函数 login_check 提交数据,如果返回成功,则正常登陆,否则提示相应错误。
那么再来看看这个 login_check 函数的实现,在 auth/views.py 中添加代码:
@auth.route('/login-check', methods=['GET', 'POST'])
def login_check():
email = request.form.get('email', '')
password = request.form.get('password', '')
user = WebUser.query.filter_by(email=email).first()
if user is not None and user.verify_password(password):
login_user(user)
return "success"
else:
return "error"
接收前端传递的参数,如果鉴权成功,则调用 login_user 来登陆并返回成功消息给前台。
于是一个比较美观的登陆页面就完成了,至于注册的逻辑,我们后面完成。
第三方登陆
下面,我们在实现通过 GitHub 来鉴权登陆的功能。
OAuth 鉴权
简单来说,为一个网站添加第三方登录指的是提供通过其他第三方平台账号登入当前网站的功能。比如,使用 QQ、微信、新浪微博账号登录。对于某些网站,甚至可以仅提供社交账号登录的选项,这样网站本身就不需要管理用户账户等相关信息。对用户来说,使用第三方登录可以省去注册的步骤,更加方便和快捷。这里,我就是使用 GitHub 的 OAuth 认证来进行鉴权登陆。
这里首先需要在自己的 GitHub 上创建一个 OAuth 程序,非常简单,访问这个地址:https://github.com/settings/Applications/new,按照要求填写即可。
其中的 callback 需要填写一个回调函数,因为是本地调测,所以我们暂时配置为:http://127.0.0.1:5000/auth/callback/github。
创建好这个 OAuth 程序后,我们就会获得 Client ID(客户端 ID)和 Client Secret(客户端密钥),在后面调用 Github 的 API 时使用。
创建表结构
在 models 中创建对应的 GitHub 登陆用的数据库模型:
class ThirdOAuth(db.Model):
__tablename__ = 'thirdoauth'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.String(64), unique=True, index=True)
oauth_name = db.Column(db.String(128))
oauth_id = db.Column(db.String(128), unique=True, index=True)
oauth_access_token = db.Column(db.String(128), unique=True, index=True)
oauth_expires = db.Column(db.String(64), unique=True, index=True)
发送授权请求
flask-github 已经为我们封装好了,直接调用即可,在 auth/views.py 中添加代码:
@auth.route('/githublogin', methods=['GET', 'POST'])
def githublogin():
return github.authorize(scope='repo')
调用需要用到我们前面获得的客户端 ID 和密钥,我这里把相关信息写到了一个配置文件中,并在初始化 flask App 时加载: GITHUB_CLIENT_ID = os.environ.get(‘GCI’) GITHUB_CLIENT_SECRET = os.environ.get(‘GCS’)
初始化 App,在项目的 init.py 文件中添加:
def create_App(config_name):
App = Flask(__name__)
App.config.from_object(config[config_name])
config[config_name].init_App(App)
db.init_App(App)
cors.init_App(App, supports_credentials=True)
login_manager.init_App(App)
bootstrap.init_App(App)
github.init_App(App)
mail.init_App(App)
from .main import main as main_blueprint
App.register_blueprint(main_blueprint)
from .auth import auth as auth_blueprint
App.register_blueprint(auth_blueprint, url_prefix='/auth')
return App
获取 access 令牌
创建一个视图函数,定义正确的 URL 规则(这里的 URL 规则需要和 GitHub 上填写的 Callback URL 匹配),并为其附加一个 github.authorized_handler 装饰器。另外,这个函数要接受一个 access_token 参数,GitHub-Flask 会在授权请求结束后通过这个参数传入访问令牌。
获取令牌代码,在 auth/views.py 中添加代码 :
@auth.route('/callback/github')
@github.authorized_handler
def authorized(access_token):
if access_token is None:
flash('Login Failed!')
return redirect(url_for('main.index'))
response = github.get('user', access_token=access_token)
username = response['login']
u_id = response['id']
email = response['email']
avatar = response['avatar_url']
user = WebUser.query.filter_by(username=username).first()
if user is None:
user = WebUser(username=username, user_id=time.time(), confirmed=True)
db.session.add(user)
db.session.commit()
thirduser = ThirdOAuth(user_id=WebUser.query.filter_by(username=username).first().user_id,
oauth_name='github', oauth_access_token=access_token,
oauth_id=u_id)
db.session.add(thirduser)
db.session.commit()
login_user(user)
user.email = email
db.session.add(user)
db.session.commit()
session['userid'] = user.user_id
return render_template('index.html', avatar=avatar)
else:
thirduser = ThirdOAuth.query.filter_by(user_id=user.user_id).first()
thirduser.oauth_access_token = access_token
db.session.add(thirduser)
db.session.commit()
user.email = email
db.session.add(user)
db.session.commit()
login_user(user)
session['userid'] = user.user_id
return render_template('index.html', avatar=avatar)
首先检查用户是否存在,如果不存在,则向数据库 WebUser 和 ThirdOAuth 中分别添加记录,然后登陆。否则只向 ThirdOAuth 中添加记录。同时都会向 session 中增加一个名为 userid 的字段,作为第三方登陆的标识。
这样,一个简单的通过 GitHub 鉴权登陆的功能也完成了!
注册用户
发送邮件
注册用户需要发送邮件,所以这里先实现一个发送邮件的功能。直接使用 Python 自带的 smtplib 和 email 库,并通过多线程发送邮件。在 sendemail.py 中编写代码:
import smtplib
from email.mime.text import MIMEText
from email.header import Header
import os
from threading import Thread
def sendmail(to, subject, text):
mail_host = "smtp.gmail.com"
mail_user = os.environ.get('MAIL_USERNAME')
mail_pass = os.environ.get('MAIL_PASSWORD')
sender = os.environ.get('MAIL_USERNAME')
receivers = [to]
message = MIMEText(text, 'plain', 'utf-8')
message['From'] = Header('萝卜大杂烩', 'utf-8')
message['to'] = Header(to, 'utf-8')
subject = subject
message['Subject'] = Header(subject, 'utf-8')
smtpobj = smtplib.SMTP(mail_host, 25)
smtpobj.ehlo()
smtpobj.starttls()
smtpobj.login(mail_user, mail_pass)
thr = Thread(target=smtpobj.sendmail, args=[sender, receivers, message.as_string()])
thr.start()
return thr
同样的,把用户相关的敏感信息保存在环境变量中
生成 Token
这里使用 itsdangerous 来生成命令牌,它的 TimedJSONWebSignatureSerializer 类可以生成具有过期时间的 JSON Web 签名(JSON Web Signatures,JWS)。这个类的构造函数接收的参数是一个密钥,在 Flask 程序中可使用 SECRET_KEY 设置。
dumps() 方法为指定的数据生成一个加密签名,然后再对数据和签名进行序列化,生成令牌字符串。expires_in 参数设置令牌的过期时间,单位为秒。
为了解码令牌,序列化对象提供了 loads() 方法,其唯一的参数是令牌字符串。这个方法会检验签名和过期时间,如果通过,返回原始数据。如果提供给 loads() 方法的令牌不正 确或过期了,则抛出异常。
我们再来看下 WebUser 类,给它添加一个新的字段 confirmed,用来标识用户是否确认,再增加生成 Token 和确认函数:
class WebUser(UserMixin, db.Model):
...
confirmed = db.Column(db.Boolean, default=False)
...
def generate_confirmation_token(self, expiration=3600):
s = Serializer(current_App.config['SECRET_KEY'], expiration)
return s.dumps({'confirm': self.id})
def confirm(self, token):
s = Serializer(current_App.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return False
if data.get('confirm') != self.id:
return False
self.confirmed = True
db.session.add(self)
db.session.commit()
return True
生成 token 就是使用 itsdangerous 提供的函数,传入我们设置的 KEY 和期望的过期时间。 确认函数,同样使用 itsdangerous 来反译 token,并且比对 confirm 这个字典,如果一致,则更新数据库。
注册路由函数
在 auth/views.py 中,我这里定义了两个函数,一个是注册时不需要邮箱确认的,即默认设置 confirmed 字段为 True,另一个时注册时需要邮箱确认,保持 confirmed 字段默认值不变。
不需要邮箱确认:
@auth.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterForm()
if form.validate_on_submit():
user = WebUser(email=form.email.data,
username=form.username.data, password=form.password.data,
user_id=time.time(), confirmed=True)
db.session.add(user)
db.session.commit()
flash('You can login now.')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
需要邮箱确认:
@auth.route('/registerconfirm', methods=['GET', 'POST'])
def register_confirm():
form = RegisterForm()
if form.validate_on_submit():
user = WebUser(email=form.email.data,
username=form.username.data, password=form.password.data,
user_id=time.time())
db.session.add(user)
db.session.commit()
token = user.generate_confirmation_token()
text = render_template('auth/email/confirm.txt', user=user, token=token)
sendmail(user.email, 'Confirm Your Account', text)
flash('A Confirmation email has been sent to you by your registered email.')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
两者的差别就是是否更新 WebUser 的 confirmed 字段,和是否发送确认邮件。
而邮件所使用的发送模板定义在了 templates/auth/email/confirm.txt 中:
Dear {{ user.username }},
Welcome to Flask Webauth!
To confirm your account please click on the following link:
{{ url_for('auth.confirm', token=token, _external=True) }}
Sincerely,
The Flask Webauth Team
Note: replies to this email address are not monitored.
此处的 _external=True 是生成完整 URL 的意思。因为用户需要一个完整的 URL 来点击确认。
发出的邮件例子如下:

重新发送 confirm 邮件
如果用户没有在 token 超时时间内完成确认,而某些页面又是必须要确认才能访问的,这时用户就需要一个重新发送一个确认链接的入口来重新发送确认邮件,所以定义一个 resend confirm 函数,重新生成一个确认链接。因为这个是用户的主动行为,所以用 login_required 函数限制只有在用户登陆的情况下才可以使用。
在 auth/views.py 中添加代码:
@auth.route('/resendconfirm')
@login_required
def resend_confirmation():
token = current_user.generate_confirmation_token()
text = render_template('auth/email/confirm.txt', user=current_user, token=token)
sendmail(current_user.email, 'Confirm Your Account', text)
flash('A new confirmation email has been sent to you by your registered email')
return redirect(url_for('main.index'))
没有任何新的东西,就是调用类 WebUser 的方法 generate_confirmation_token() 来产生新的 Token 并发送给用户。
完成另一个版本的注册逻辑
还记得我们有一个比较美观的登陆注册页面,该页面里还包含一个用于注册的 jQuery 函数:
function sign_up(){
var data = {};
data['email1'] = $("#email1").val();
data['password1'] = $("#password1").val();
data['name1'] = $("#name1").val();
$.ajax(
{
type:'POST',
url: '{{ url_for('auth.register_check') }}',
data: data,
{
success:function (data) {
if ( data == 'error' ){
alert("Email had been registed!");
}else if (data == 'success'){
window.location.href="{{ url_for('main.index') }}";
}
},
error:function (xhr, type) {
}
}
);
}
我们同样需要定义一个新的函数 register_check 来处理相应的注册逻辑。
在 auth/views.py 中添加代码:
@auth.route('/register-check', methods=['GET', 'POST'])
def register_check():
email = request.form.get('email1', '')
password = request.form.get('password1', '')
name = request.form.get('name1', '')
user = WebUser.query.filter_by(email=email).first()
if user is None:
newuser = WebUser(email=email, username=name, password=password,
user_id=time.time(), confirmed=True)
db.session.add(newuser)
db.session.commit()
# token = newuser.generate_confirmation_token()
# text = render_template('auth/email/confirm.txt', user=newuser, token=token)
# sendmail(newuser.email, 'Confirm your Account', text)
return "success"
else:
return "error"
这里我们注释掉了发送 token 的代码,因为在向 WebUser 中添加数据时,已经设置 confirmed 字段为 True 了。如果你想用户注册后需要邮件确认,可以把这里的字段去掉,并去掉这三行注释。
定义非确认用户不可访问路由
Flask 有两个钩子函数,分别是 before_request 和 before_App_request 修饰器。
before_request:注册一个函数,在每次请求之前运行。 before_App_request:针对程序全局,每次请求之前运行。
这里使用 before_App_request 注册一个函数,用来区别对待不同的页面,在 auth/views.py 中添加代码:
@auth.before_App_request
def before_request():
if current_user.is_authenticated and not current_user.confirmed and request.endpoint == 'main.needconfirm':
return redirect(url_for('auth.unconfirmed'))
可以看到,只有当用户为登陆状态,且没有确认,并且 endpoint 为 main.needconfirm 时,before_App_request 才会拦截请求,并跳转到 auth.unconfirmed 页面。
在 main/views.py 中定义 needconfirm 函数如下:
@main.route('/needconfirm')
@login_required
def needconfirm():
return 'Good! Only confirmed users are allowed!'
在 auth/views.py 中定义 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')
然后这里的 unconfirmed 页面模板直接调用重新发送确认邮件的函数 resend_confirmation 即可:"{{url_for(‘auth.resend_confirmation’) }}"
自此,登陆注册的功能就完成了,下面我们来给用户增加些管理功能。主要为修改密码和重设密码。
用户管理
修改密码
这个功能相对简单一些,只要用户在登陆状态下,就可以展示一个表单,供用户修改密码
在 auth/forms.py 中增加修改密码表单如下:
class ChangePwdForm(FlaskForm):
oldpwd = PasswordField('Old Password', validators=[DataRequired()])
newpwd = PasswordField('New Password', validators=[DataRequired(),
EqualTo('newpwd2', message='两次输入的密码需要一致。')])
newpwd2 = PasswordField('Confirm New Password', validators=[DataRequired()])
submit = SubmitField('Change Password')
然后使用 WebUser 模型的 verity_password 方法来检测旧的密码是否正确,如果正确,则更新新密码,在 auth/views.py 中添加代码:
@auth.route('/changepwd', methods=['GET', 'POST'])
@login_required
def changepwd():
form = ChangePwdForm()
if form.validate_on_submit():
if current_user.verify_password(form.oldpwd.data):
current_user.password = form.newpwd.data
db.session.add(current_user)
db.session.commit()
flash('You have changed your password!')
return redirect(url_for('main.index'))
else:
flash('Invalid password')
return render_template('auth/changepwd.html', form=form)
重设密码
首先在 WebUser 模型中增加两个方法,分别用来产生新的 token 和重置密码:
def generate_reset_token(self, expiration=3600):
s = Serializer(current_App.config['SECRET_KEY'], expiration)
return s.dumps({'reset': self.id})
def reset_password(self, token, newpwd):
s = Serializer(current_App.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return False
if data.get('reset') != self.id:
return False
self.password = newpwd
db.session.add(self)
db.session.commit()
return True
然后定义两个表单,一个为重置密码请求表单,包含一个 Email 输入框,用来输入用户注册的邮箱;另一个为重置密码表单,如果在重设密码时输入的 Email 是错误的邮箱,则直接报错。
在 auth/forms.py 中添加代码:
ResetPwdEmailForm,只包含邮箱输入框和提交按钮
class ResetPwdEmailForm(FlaskForm):
email = StringField('Your Register Email', validators=[DataRequired(), Length(1, 64), Email()])
submit = SubmitField('Reset Password')
ResetPwdForm
class ResetPwdForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Length(1, 64), Email()])
newpwd = PasswordField('New Password', validators=[DataRequired(),
EqualTo('newpwd2', message='两次输入的密码需要一致。')])
newpwd2 = PasswordField('Confirm New Password', validators=[DataRequired()])
submit = SubmitField('Reset Password')
def validate_email(self, field):
if WebUser.query.filter_by(email=field.data).first() is None:
raise ValidationError('This Email is Invalid')
提供一个邮箱验证函数,如果用户输入的邮箱并不是当前用户信息中的邮箱,则返回错误。
最后,在 auth/views.py 中添加路由函数:
@auth.route('/resetpwdemail', methods=['GET', 'POST'])
def resetpwdemail():
form = ResetPwdEmailForm()
if form.validate_on_submit():
user = WebUser.query.filter_by(email=form.email.data).first()
if user:
token = user.generate_reset_token()
text = render_template('auth/email/resetpwdemail.txt', user=current_user, token=token)
sendmail(user.email, 'Reset Your Password', text)
flash('Have send a email to you')
return redirect(url_for('auth.login'))
else:
flash('This email is not registered')
return render_template('auth/resetpwdemail.html', form=form)
@auth.route('/resetpwd/<token>', methods=['GET', 'POST'])
def resetpwd(token):
form = ResetPwdForm()
if form.validate_on_submit():
user = WebUser.query.filter_by(email=form.email.data).first()
if user is None:
flash('Invalid Email')
return redirect(url_for('main.index'))
if user.reset_password(token, form.newpwd.data):
flash('Your password has been reset')
return redirect(url_for('auth.login'))
else:
flash('Reset Password Failed')
return redirect(url_for('main.index'))
return render_template('auth/resetpwd.html', form=form)
其中 resetpwdemail 函数是用来给需要重设密码的邮箱账号发送邮件的;resetpwd 函数是用来使用新的 Token 重设密码的。 只有当用户输入的 Email 有对应的用户时,才会向该 Email 发送修改密码的邮件。 当用户打开邮件里的重置密码链接后,还会校验用户输入的 Email 是否是正确的,如果是正确的,则更新密码。
角色管理
用户角色
我们简单的设计一个用户角色表,关联到 WebUser 表的 role_id 字段,而 WebUser 类新增 role_id 字段,作为 roles 表的外键,同时定义初始化角色的静态函数。
在 models.py 中添加代码:
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
users = db.relationship('WebUser', backref='role')
@staticmethod
def init_roles():
roles = ['User', 'Admin']
for r in roles:
role = Role.query.filter_by(name=r).first()
if role is None:
role = Role(name=r)
db.session.add(role)
db.session.commit()
现在先说说 init_roles 这个函数怎么用,这是一个类的静态函数,只要初始化了类,就可以直接调用。
我们先介绍一个有用的 Flask 插件 flask-script。
Flask-Script 是一个 Flask 扩展,为 Flask 程序添加了一个命令行解析器。Flask-Script 自带 了一组常用选项,而且还支持自定义命令。
我们在 manage.py 文件中使用该插件,现在修改后的文件内容为:
from App import create_App, db
from flask_script import Manager, Shell, Server
from App.models import WebUser, ThirdOAuth, Role
App = create_App('testing')
manager = Manager(App)
def make_shell_context():
return dict(App=App, db=db, WebUser=WebUser, Thirdoauth=ThirdOAuth, Role=Role)
manager.add_command("runserver", Server(use_debugger=True, host='0.0.0.0', port='9982'))
manager.add_command("shell", Shell(make_context=make_shell_context))
if __name__ == '__main__':
manager.run(default_command='runserver')
现在,我们相当于自定义了两个命令,runserver 和 shell,先来看看 shell 命令,当我们通过 python manage.py shell 来执行时,会自动初始化 App,db,和我们定义的 WebUser 等类。下面我们一起操作下:
执行 python manage.py shell

可以看到,我们进入了一个 shell 操作命令行,在这个命令行下,因为我们已经初始化了 App,db,WebUser 等,所以这里已经可以直接使用这些类。 根据前面学到的,可以调用 db.create_all() 来创建表结构,我们来试试

同时,我又调用了 Role 的静态函数,分别插入了角色和用户。
下面再执行 python manage.py runserver

这样,我们就在 0.0.0.0 的 9982 端口启动了服务,并且是 debug 状态的。0.0.0.0 就是说不仅仅是本机,外部网络的机器,只要能够访问到我们的电脑,都能够访问我们的服务。
好了,现在我们可以通过 init_roles 方法来初始化角色了,那么同理,我们也可以在 WebUser 类中定义静态函数,来初始化用户,在 models.py 中的 WebUser 类下添加代码:
@staticmethod
def insert_user():
users = {
'user1': ['user1@luobo.com', 'test1', 1],
'user2': ['user2@luobo.com', 'test2', 1],
'admin1': ['admin1@luobo.com', 'admin1', 2],
'admin2': ['admin2@luobo.com', 'admin2', 2]
}
for u in users:
user = WebUser.query.filter_by(username=u[0]).first()
if user is None:
user = WebUser(user_id=time.time(), username=u, email=users[u][0],
confirmed=True, role_id=users[u][2])
user.password = users[u][1]
db.session.add(user)
db.session.commit()
现在我们继续用户角色的功能实现。
在 WebUser 表类中,再增加一个判断是否为 admin 的方法,这样就可以在路由函数中通过该方法来判断用户是否为 admin 用户,从而确定权限。
def is_admin(self):
if self.role_id is 2:
return True
else:
return False
添加一个只有 admin 用户才能访问的测试页面,对于普通用户,则返回对应提示,在 main/views.py 中添加代码:
@main.route('/onlyadmin')
@login_required
def onlyadmin():
if current_user.is_admin():
return 'Good! Your are admin so you can see this page'
else:
return 'You are not admin user, Only admin user can access this page'
这样,在真实的场景下,当我们有些页面是只允许 admin 访问时,只需要通过 is_admin() 来做判断即可,甚至直接设置非 admin 用户,不展示某些页面。
blcok 用户
我们可以通过 Block 功能,来实现控制用户登陆的功能。在 WebUser 类中我们添加对应的字段 block_status 和方法 is_block,接下来,我们只需要在 login 函数中,于调用 login_user 函数前,添加一个判断,如果用户是 block 状态,则不允许登陆:
models.py
class WebUser(UserMixin, db.Model):
...
block_status = db.Column(db.Boolean, default=True)
...
def is_block(self):
if self.block_status:
return True
else:
return False
auth/views.py 的 login 函数中添加
if user.is_block() is False:
flash('You have been blocked, please contact admin')
return redirect(url_for('.login'))
添加一个用户列表页,展示所有的用户和 block 与 unblock 按钮,在 templates/userlist.html 中添加代码:
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}User List{% endblock %}
{% block page_content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">×</button>
{{ message }}
</div>
{% endfor %}
</div>
<div class="page-header">
<h1>用户列表</h1>
</div>
<div class="col-md-12">
{% for u in userlist %}
<p>
{{ u.username }}
<a class="btn btn-warning" href="{{ url_for('main.blockuser', username=u.username) }}">block</a>
<a class="btn btn-primary" href="{{ url_for('main.unblockuser', username=u.username) }}">unblock</a>
</p>
{% endfor %}
</div>
{% endblock %}
然后是 block 函数的设计,首先判断用户是否为 admin,然后判断是否在 block 当前登陆的用户,再然后判断用户是否已经处于 block 状态,最后再更新数据库,在 main/views.py 中添加代码如下:
@main.route('/blockuser/<username>', methods=['GET', 'POST'])
@login_required
def blockuser(username):
if current_user.is_admin():
user = WebUser.query.filter_by(username=username).first()
if user.username == current_user.username:
flash('Your can not block yourself')
return redirect(url_for('main.userlist'))
if user.is_block():
user.block_status = False
db.session.add(user)
db.session.commit()
flash('Have block this user')
return redirect(url_for('main.userlist'))
else:
flash('This user have been blocked')
return redirect(url_for('main.userlist'))
flash('Your have no permission to operate it')
return redirect(url_for('main.index'))
unblock 函数其实类似,同学们可以先自行实现下,然后再对照我的源码。
最后 block 页面如下:

短信集成
使用 twilio 发短信
我这里使用 twilio 提供的短信功能,它提供了一个免费的短信接口,让我们可以在完全 free 的状态下测试短信功能,同时也有对应的 python 库 twilio 来简化开发,只需要使用 pip install twilio 即可使用该公司提供的各种功能。有兴趣的同学可以移步官网查看: https://www.twilio.com/
发送短信模块
在 sendsms.py 中编写发送短信的逻辑,首先和前面产生确认邮件的 token 一样,这里也使用 itsdangerous 来加密 code:
from twilio.rest import Client
import os
import random
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from config import Config
def generate_code(expiration=3600):
random_num = ''.join(str(i) for i in random.sample(range(0, 9), 4))
s = Serializer(Config.SECRET_KEY, expiration)
return s.dumps({'code': random_num})
def decoding_code(code):
s = Serializer(Config.SECRET_KEY)
c = s.loads(code)
return c['code']
首先产生 code,因为 code 是通过 URL 参数传播的,所以又增加了加密和解密的过程
然后是发送短信的代码,查看官网就可以获得,很简单,不多说了:
def sendsms(code, num):
account_sid = os.environ.get('A_SID')
auth_token = os.environ.get('A_TK')
mytwilio_num = os.environ.get('T_NUM')
client = Client(account_sid, auth_token)
message = client.messages.create(
from_=mytwilio_num,
body=code,
to=num)
集成短信功能到系统
在 auth/forms.py 中新增两个表单, 一个是发送 verify code,一个是验证 verify code:
class LoginSMSCodeForm(FlaskForm):
phonenumber = IntegerField('Your Phone Number', validators=[DataRequired()])
submit = SubmitField('Send validate code')
class LoginSMSForm(FlaskForm):
validatacode = IntegerField('Enter validate code')
submit = SubmitField('Login')
再在 auth/views.py 中新增两个路由,login_codesend 和 login_codeverify 分,别处理两个表单的提交逻辑。 login_codesend 函数,会向输入的手机号发送 verify code,并把产生的 code 传给 login_codeverify 函数,用于比较。
login_codesend 函数:
@auth.route('/logincodesend/', methods=['GET', 'POST'])
def login_codesend():
codeform = LoginSMSCodeForm()
if codeform.validate_on_submit():
num = codeform.phonenumber.data
user = UserPhone.query.filter_by(phone=num).first()
if user is None:
user = UserPhone(phone=num)
db.session.add(user)
db.session.commit()
code = generate_code()
decod_code = decoding_code(code)
sendsms(decod_code, '+86' + str(num))
flash('Have send a validate code to your phone')
return redirect(url_for('.login_codeverify', code=code, num=num))
return render_template('auth/logincodesend.html', form=codeform)
向用户输入的号码发送短信,同时把相应的信息存入数据库中。
login_codeverify 函数:
@auth.route('/logincodeverify/<code>/<num>', methods=['GET', 'POST'])
def login_codeverify(code, num):
form = LoginSMSForm()
code = decoding_code(code)
if form.validate_on_submit():
if str(form.validatacode.data) == code:
webuser = WebUser.query.filter_by(phone=num).first()
if webuser is None:
webuser = WebUser(username=num, phone=num, user_id=time.time(), confirmed=True)
db.session.add(webuser)
db.session.commit()
phoneuser = UserPhone.query.filter_by(phone=num).first()
if phoneuser:
phoneuser.user_id = webuser.user_id
db.session.add(phoneuser)
db.session.commit()
login_user(webuser)
return redirect(url_for('main.index'))
flash('Invalid verify code')
return redirect(url_for('.login_codeverify', code=code, num=num))
return render_template('auth/logincodeverify.html', form=form)
比较拿到的 verify code 和用户输入的 code,如果一样,则登陆成功。 两个页面也比较简单,直接快速渲染 Form 即可。
最后我们更新登陆页面代码如下:
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Login{% endblock %}
{% block page_content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">×</button>
{{ message }}
</div>
{% endfor %}
</div>
<div class="page-header">
<h1>Login</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
<div class="col-md-12">
<a class="btn btn-primary" href="{{ url_for('auth.githublogin') }}">Login with GitHub</a>
</div>
<div class="col-md-12">
<a class="btn btn-primary" href="{{ url_for('auth.login_codesend') }}">Login with SMS</a>
</div>
<div class="col-md-12">
<a href="{{ url_for('auth.resetpwdemail') }}">
忘记密码,重置
</a>
</div>
<div class="col-md-12">
<a href="{{ url_for('auth.register') }}">
点击此处注册,不需要确认邮箱
</a>
</div>
<div class="col-md-12">
<a href="{{ url_for('auth.register_confirm') }}">
点击此处注册,需要确认邮箱
</a>
</div>
{% endblock %}
我们添加了重置密码的入口,同时也增加了通过 GitHub 和 SMS 登陆的入口。 我们也把注册是否需要邮件确认区分开来。

用户资料
gravatar 头像
使用业界流行的 gravatar,它可以把用户的 Email 和头像关联起来,快速生成用户头像图片,其中 Email 地址需要经过 md5 加密处理。 可以使用查询字符串的形式,在 url 中传入参数,来获取头像图片。
参数说明:
| 参数名 |
说明 |
| s |
图片大小,单位为像素 |
| r |
图片级别,可选值有 “g”,“pg”,“r” 和 “x” |
| d |
没有注册 Gavatar 服务的用户使用的默认图片生成方式,例如 “identicon” |
| fd |
强制使用默认头像 |
一个请求 URL 例子: http://secure.gravatar.com/avatar/ffd5c7d7bfcc8605aaa2d259e2590112?s=128&d=identicon&r=g
大家可以输入到浏览器中看看返回的图片是什么。
添加生成头像方法
在 WebUser 类中,添加生成头像的方法函数
def gravatar(self, size=100, default='identicon', rating='g'):
if request.is_secure:
url = 'https://secure.gravatar.com/avatar'
else:
url = 'http://secure.gravatar.com/avatar'
email = self.email or 'test@luobo.com'
hash = self.avatar_hash or hashlib.md5(
email.lower().encode('utf-8')).hexdigest()
avatar = '{url}/{hash}?s={size}&d={default}&r={rating}'.format(
url=url, hash=hash, size=size, default=default, rating=rating
)
return avatar
丰富用户资料字段
为 WebUser 新增三个字段,以丰富用户资料
nickname = db.Column(db.String(64))
about_me = db.Column(db.Text())
avatar_hash = db.Column(db.String(32))
添加路由函数
添加展示用户 profile 的函数,这里对使用 GitHub 登陆的用户做了判断,优先展示 GitHub 的头像和用户名。在 main/views.py 中添加代码:
@main.route('/user/<username>', methods=['GET', 'POST'])
@login_required
def user(username):
user = WebUser.query.filter_by(username=username).first()
if user is None:
abort(404)
if 'userid' in session:
thirduser = ThirdOAuth.query.filter_by(user_id=session['userid']).first()
if thirduser:
response = github.get('user', access_token=thirduser.oauth_access_token)
avatar = response['avatar_url']
gituser = response['login']
return render_template('user.html', user=user, avatar=avatar, gituser=gituser)
return render_template('user.html', user=user, avatar=None, gituser=None)
对于 user.html 模板页面,同样,首先判断是否 gituser,如果 gituser 不为 None,则展示 git 的用户名,同时也会展示用户自己设置的 nickname
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Profile{% endblock %}
{% block page_content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">×</button>
{{ message }}
</div>
{% endfor %}
</div>
<div class="page-header">
{% if avatar %}
<img style="-webkit-user-select: none;" src="{{ avatar }}" />
{% else %}
<img class="img-rounded profile-thumbnail" src="{{ user.gravatar(size=128) }}">
{% endif %}
<div class="profile-header">
<h1>
{% if gituser %}
{{ gituser }}
{% elif current_user.username %}
{{ user.username }}
{% endif %}
</h1>
{% if user.nickname %}
<p>昵称: {{ user.nickname }}</p>
{% endif %}
{% if user.email %}
<p>邮箱: <a href="mailto:{{ user.email }}">{{ user.email }}</a> </p>
{% endif %}
{% if user.about_me %}
<p>个人介绍: {{ user.about_me }}</p>
{% endif %}
<p>
{% if user == current_user %}
<a class="btn btn-default" href="{{ url_for('main.edit_profile') }}">Edit Profile</a>
{% endif %}
</p>
</div>
</div>
{% endblock %}
编辑用户资料
在 main/forms.py 中定义表单,包括 username,nickname 和 about_me,用于提供给用户做修改
class EditProfileForm(FlaskForm):
username = StringField('Username', validators=[Length(0, 64)])
nickname = StringField('Nickname', validators=[Length(0, 64)])
about_me = TextAreaField('About me')
submit = SubmitField('Submit')
然后在路由函数中判断,如果 username 存在且不是当前用户时,不能修改 username,如果修改的 username 是唯一,那么更新数据库。在 main/views.py 中添加代码:
@main.route('/edit-profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
form = EditProfileForm()
if form.validate_on_submit():
username = form.username.data
if WebUser.query.filter_by(username=username).first() and username != current_user.username:
flash('This username has been used')
return redirect(url_for('main.edit_profile'))
current_user.username = form.username.data
current_user.nickname = form.nickname.data
current_user.about_me = form.about_me.data
db.session.add(current_user)
db.session.commit()
flash('Your profile has been updated')
return redirect(url_for('.user', username=current_user.username))
form.username.data = current_user.username
form.nickname.data = current_user.nickname
form.about_me.data = current_user.about_me
return render_template('edit_profile.html', form=form)
至此,我们的用户鉴权系统就基本完成了。当然,同学们可能也看到了,由于个人的能力有限,很多代码还是需要优化的,同时还有一些功能很棒,但是没有提供,例如:用户修改 Email 的功能,基于其他第三方系统的登陆这等,这些就当作作业,由感兴趣的你来完成吧。
总结
现在,我们再来总结下系统的整体功能
- 用户本地鉴权登陆; 2. 用户第三方鉴权登陆(目前实现基于 GitHub ) ; 3. 用户注册,需要或不需要邮箱确认; 4. 用户角色管理,包括 block 用户 ; 5. 用户管理,修改和重置密码等; 6. 用户资料管理,获取头像等; 7. 集成短信发送功能,其实不仅局限于注册,可以把发短信功能扩展到其他场景。
以上,就是本次 Chat 的全部功能实现,当然由于笔者水平有限,文中难免存在错误,还望大家能不吝指出,每天进步一小步,我们终会成为大神!
给出 GitHub 源码,仅供大家参考:
https://github.com/zhouwei713/flask-webauth
强烈建议大家把所有代码都手动写一遍,切不可直接复制粘贴,这两种方式,你获得的提升是完全不一样的!
同时,为了文章的可读性,在第三部分,除了代码,我并没有加入过多的其他展示图片,所以我这里提供了一个演示的地址,供大家学习: https://luobodazahui.top 我还初始化了一些用户,大家可以登陆上去,体验下相关功能。

最后提供一些免费的学习资料,感兴趣的同学可以自取: https://pan.baidu.com/s/1kjUsJ8XnmNhLeIIzyEMXvA 提取码: 4x5s https://pan.baidu.com/s/1oc6mONu068y3V_ulaw-BSQ 提取码: ef6f https://pan.baidu.com/s/1ht2KgazmD_DYjw3HBqkFYw 提取码: m9t3
附录
在最后的最后,我们再来说说如何部署我们的系统。
相信同学们还记得,我们代码里是有这两个文件的:
gunicorn,run.sh
其中 gunicorn 就是作为 web 容器的,那么什么是 web 容器呢,从百度百科上看到定义:
web 容器是一种服务程序,在服务器一个端口就有一个提供相应服务的程序,而这个程序就是处理从客户端发出的请求,如 JAVA 中的 Tomcat 容器,ASP 的 IIS 或 PWS 都是这样的容器。一个服务器可以有多个容器。
那么 python 常用的容器就是 gunicorn 了,而我们项目里的 gunicorn 文件就是对应的配置文件。
内容如下: from gevent import monkey monkey.patch_all() import multiprocessing debug = True loglevel = 'debug' bind = '0.0.0.0:5005' logfile = '/home/log/debug.log' workers = multiprocessing.cpu_count() * 2 + 1 worker_class = 'gevent'
其实大部分还是很好理解的,只解释个别参数,具体的配置,大家可以查看相关文档。
bind = '0.0.0.0:5005' #绑定到 5005 端口 workers = multiprocessing.cpu_count() _ 2 + 1 # 用于处理工作进程的数量,为正整数,默认为 1。worker 推荐的数量为当前的 CPU 个数_2 + 1 worker_class = 'gevent' # 要使用的工作模式,这里使用协程模式
然后就是部署到 Linux 服务器了,使用一个简单的启动脚本,如 run.sh 所写:
/usr/bin/gunicorn -D -c /home/webauth/gunicorn manage:App
这样,就在服务器本地的 5005 端口启动了我们的 Web 服务了。当然还有部署反向代理,如:Nginx 等,就不在本文的讨论范围了,有兴趣的同学可以私下一起找我交流。
最后,因为笔者的表达能力所限,可能有些功能或者用法并没有说的很清楚,所以这里我创建了一个供大家讨论的 QQ 群,大家有疑问可以提出,一起讨论,我也会在里面随时回答大家的疑惑,期待我们一同进步! QQ 群 :617870323
本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。
https://gitbook.cn/books/5c6df95292e9e173a2f99f2f/index.html
#1
图片来自 flask 官网(http://flask.pocoo.org/)
借着人工智能大数据的这股东风,Python 着实又火了一把。之所以用 “又” 字,是因为 Python 很早就走进了广大程序员的眼中。其简洁的语言,丰富而完整的社区,都是广大 Python 爱好者的福音。
本篇,主要从如下几方面来带你走进 Python 的世界。
Python 语法简介
Python 简介
Python 是著名的 “龟叔”Guido van Rossum 在 1989 年圣诞节期间,为了打发无聊的圣诞节而编写的一个编程语言。
下面最近 10 年最常用的 10 种编程语言的变化图:
图片来自 TIOBE 网站(https://www.tiobe.com/tiobe-index/)
Python 作为一个极易入门的语言,其简洁的语法绝对居功至伟。
一般来说,推荐使用 PyCharm 来编写 Python 程序,下面来看下最简单的 Hello World。
可以看到,Python 作为一个极易入门的语言,其简洁的语法绝对居功至伟。
再来看一个例子,来体会下 Python 的简洁。
可以看到,本里中用到了 Python 的注释、列表定义、函数定义和调用等。
因为本文主要还是关注于入门的实战知识,即基于 Flask 框架的鉴权系统的编写,所以对于 Python 的基础简介就到这里了,如果有同学还有疑问,或者对于 Python 的基础语法不是很理解,可以私下找我,一起交流。
Flask 简介
Flask 是一款轻量级的 Python Web 框架,据 Python 官方统计,其流行程度已经超过 Django 成为最流行的 Python Web 框架。首先 pip install Flask,然后就能愉快的体验啦。
下面我们换一种形式来 Hello World 一下:
现在运行这个脚本,就可以看到在本地的 5000 端口启动了一个服务,然后我们在浏览器中输入 “http://127.0.0.1:5000”,访问下试试吧,一个 Web 版本的 Hello World 就完成啦。
Web 页面为:
好,更多的 Python 和 Flask 使用方法,我们在实战中再详细介绍。
项目 Web 代码框架
结构解析
做一个项目,代码结构尤为重要,好的代码结构,可以让人一眼就能被你的项目所吸引,同时也能给后面接手你代码的人减轻工作量。本文使用的代码结构如下:
在项目目录的第一层,是 App 文件夹和其他公共库。
再来看 App 文件夹内部,包括 api_1_0,auth,main,static,templates,init.py,models.py。
Flask 项目工作流程
我们先写一个简单的 demo,来理解下 Flask 项目的工作流程是怎样的。首先,先使用 pip install -r requirements 来安装所有的依赖包(文件包含内容见文尾),然后在项目顶级目录下创建 App 文件夹、config.py 和 manage.py 文件,在 App 文件夹中创建 main、templates 文件夹、models.py 和 __init**.py 文件,最后在 main 文件夹中创建 views.py、forms.py 和 _**_**init**.py 文件。
准备好以上之后,我们一起来动手吧。
在 config 文件中添加如下代码:
在 main 的 init 文件中添加如下代码:
在 App 的 init 文件中添加如下代码:
下面开始定义数据库模型,在 models 文件添加代码:
对应的 forms 文件,代码如下:
再来看看 views 文件,代码如下:
最后再编写页面模板,在 templates 目录下创建 index.html 文件,添加代码如下:
我们来看看效果
访问根 URL 效果:
访问 “/create” 效果:
让我们连接到数据库上看看表是否创建成功了:
可以看到确实正确创建了。
下面我们提交表单实验下
点击 Submit 按钮后,到数据库中查看
表单的信息也正确插入到数据库了。
剩下的 “/drop”,就留给你来验证啦。
好了,现在让我们来一起整理下 Flask 的工作流程:
完整鉴权系统
相信通过上面的小 demo 和最后的总结,你已经大致了解了 Flask 的工作流程,下面我们就来一起完成这个鉴权系统的搭建。
Flask 的认证扩展
这里用到了 flask-login、Werkzeug 和 flask-github 三个第三方库:
这里先重点说下 Werkzeug 库,我们就使用该库来处理用户密码散列,即在数据库中,我们不存储用户的明文密码,而是存储密码的散列值。
Werkzeug 中的 security 模块能够很方便地实现密码散列值的计算。这一功能的实现只需要两个函数,分别用在注册用户和验证用户阶段。
generate_password_hash(password,method=pbkdf2:sha1,salt_length=8)_ _这个函数将原始密码作为输入,以字符串形式输出密码的散列值,输出的值可保存在用户数据库中。 method 和 salt_length 的默认值就能满足大多数需求。
check_password_hash(hash, password) 这个函数的参数是从数据库中取回的密码散列值和用户输入的密码。返回值为 True 表明密码正确。
让我们在实际应用中来深入理解 Werkzeug 的实现。
还记得最开始我们给出的代码结构嘛(可不是 demo 的代码结构哦),我们在 models 文件中加入如下数据模型:
计算密码散列值的函数通过名为 password 的只写属性实现。设定这个属性的值时,赋值方法会调用 Werkzeug 提供的 generate_password_hash() 函数,并把得到的结果赋值给 password_hash 字段。如果试图读取 password 属性的值,则会返回错误,原因很明显,因为生成散列值后就无法还原成原来的密码了。
verify_password 方 法 接 受 一 个 参 数( 即 密 码 ), 将其传给 Werkzeug 提供的 check_ password_hash() 函数,和存储在 WebUser 模型中的密码散列值进行比对。如果这个方法返回 True,就表明密码是正确的。
使用 Flask-Login 认证用户
Flask-Login 的 UserMix 类,提供了如下几种必须的方法:
这样我们就可以在 WebUser 的实例中直接调用上面的这些方法了。接下来,将 Flask-Login 在程序的工厂函数中初始化,如下:
最后 Flask-Login 要求程序实现一个回调函数,使用指定的标识符加载用户,我们在 models 中添加代码如下:
本地登陆
添加登陆表单
呈现给用户的登录表单中包含一个用于输入电子邮件地址的文本字段、一个密码字段、一 个 “记住我” 复选框和提交按钮。 在 App/auth/forms.py 文件中,添加如下代码:
对应的登陆页面模板保存在 templates/auth/login.html 中,它继承自 base.html 模板。
login.html 模板代码:
用户登陆
在 App/auth/views.py 中添加登陆代码:
用户登出
用户登出就比较简单了,定义 logout 函数,调用 logout_user() 即可:
这样,登陆,登出功能就完成了!
我们来看下登陆页面
好像美观程度堪忧啊,我们这里到网上找一个美观些的登陆模板,来移植到我们的项目中。
模板效果如下,看起来还不错哦:
下面我们就来把这个页面和我们的后台逻辑关联到一起吧
首先在 auth/views.py 中添加新的登陆逻辑:
可以看到,我是准备把新的登陆模板放到 login 目录下了,在 templates 目录下新建 login 目录,并创建 login.html 文件,拷贝页面的 html 代码并稍作修改:
那么再来看看这个 login_check 函数的实现,在 auth/views.py 中添加代码:
于是一个比较美观的登陆页面就完成了,至于注册的逻辑,我们后面完成。
第三方登陆
下面,我们在实现通过 GitHub 来鉴权登陆的功能。
OAuth 鉴权
简单来说,为一个网站添加第三方登录指的是提供通过其他第三方平台账号登入当前网站的功能。比如,使用 QQ、微信、新浪微博账号登录。对于某些网站,甚至可以仅提供社交账号登录的选项,这样网站本身就不需要管理用户账户等相关信息。对用户来说,使用第三方登录可以省去注册的步骤,更加方便和快捷。这里,我就是使用 GitHub 的 OAuth 认证来进行鉴权登陆。
这里首先需要在自己的 GitHub 上创建一个 OAuth 程序,非常简单,访问这个地址:https://github.com/settings/Applications/new,按照要求填写即可。
其中的 callback 需要填写一个回调函数,因为是本地调测,所以我们暂时配置为:http://127.0.0.1:5000/auth/callback/github。
创建好这个 OAuth 程序后,我们就会获得 Client ID(客户端 ID)和 Client Secret(客户端密钥),在后面调用 Github 的 API 时使用。
创建表结构
在 models 中创建对应的 GitHub 登陆用的数据库模型:
发送授权请求
flask-github 已经为我们封装好了,直接调用即可,在 auth/views.py 中添加代码:
初始化 App,在项目的 init.py 文件中添加:
获取 access 令牌
创建一个视图函数,定义正确的 URL 规则(这里的 URL 规则需要和 GitHub 上填写的 Callback URL 匹配),并为其附加一个 github.authorized_handler 装饰器。另外,这个函数要接受一个 access_token 参数,GitHub-Flask 会在授权请求结束后通过这个参数传入访问令牌。
获取令牌代码,在 auth/views.py 中添加代码 :
这样,一个简单的通过 GitHub 鉴权登陆的功能也完成了!
注册用户
发送邮件
注册用户需要发送邮件,所以这里先实现一个发送邮件的功能。直接使用 Python 自带的 smtplib 和 email 库,并通过多线程发送邮件。在 sendemail.py 中编写代码:
生成 Token
这里使用 itsdangerous 来生成命令牌,它的 TimedJSONWebSignatureSerializer 类可以生成具有过期时间的 JSON Web 签名(JSON Web Signatures,JWS)。这个类的构造函数接收的参数是一个密钥,在 Flask 程序中可使用 SECRET_KEY 设置。
dumps() 方法为指定的数据生成一个加密签名,然后再对数据和签名进行序列化,生成令牌字符串。expires_in 参数设置令牌的过期时间,单位为秒。
为了解码令牌,序列化对象提供了 loads() 方法,其唯一的参数是令牌字符串。这个方法会检验签名和过期时间,如果通过,返回原始数据。如果提供给 loads() 方法的令牌不正 确或过期了,则抛出异常。
我们再来看下 WebUser 类,给它添加一个新的字段 confirmed,用来标识用户是否确认,再增加生成 Token 和确认函数:
注册路由函数
在 auth/views.py 中,我这里定义了两个函数,一个是注册时不需要邮箱确认的,即默认设置 confirmed 字段为 True,另一个时注册时需要邮箱确认,保持 confirmed 字段默认值不变。
不需要邮箱确认:
需要邮箱确认:
而邮件所使用的发送模板定义在了 templates/auth/email/confirm.txt 中:
发出的邮件例子如下:
重新发送 confirm 邮件
如果用户没有在 token 超时时间内完成确认,而某些页面又是必须要确认才能访问的,这时用户就需要一个重新发送一个确认链接的入口来重新发送确认邮件,所以定义一个 resend confirm 函数,重新生成一个确认链接。因为这个是用户的主动行为,所以用 login_required 函数限制只有在用户登陆的情况下才可以使用。
在 auth/views.py 中添加代码:
完成另一个版本的注册逻辑
还记得我们有一个比较美观的登陆注册页面,该页面里还包含一个用于注册的 jQuery 函数:
在 auth/views.py 中添加代码:
定义非确认用户不可访问路由
Flask 有两个钩子函数,分别是 before_request 和 before_App_request 修饰器。
这里使用 before_App_request 注册一个函数,用来区别对待不同的页面,在 auth/views.py 中添加代码:
在 main/views.py 中定义 needconfirm 函数如下:
在 auth/views.py 中定义 unconfirmed 函数如下:
自此,登陆注册的功能就完成了,下面我们来给用户增加些管理功能。主要为修改密码和重设密码。
用户管理
修改密码
这个功能相对简单一些,只要用户在登陆状态下,就可以展示一个表单,供用户修改密码
在 auth/forms.py 中增加修改密码表单如下:
然后使用 WebUser 模型的 verity_password 方法来检测旧的密码是否正确,如果正确,则更新新密码,在 auth/views.py 中添加代码:
重设密码
首先在 WebUser 模型中增加两个方法,分别用来产生新的 token 和重置密码:
然后定义两个表单,一个为重置密码请求表单,包含一个 Email 输入框,用来输入用户注册的邮箱;另一个为重置密码表单,如果在重设密码时输入的 Email 是错误的邮箱,则直接报错。
在 auth/forms.py 中添加代码:
ResetPwdEmailForm,只包含邮箱输入框和提交按钮
ResetPwdForm
最后,在 auth/views.py 中添加路由函数:
角色管理
用户角色
我们简单的设计一个用户角色表,关联到 WebUser 表的 role_id 字段,而 WebUser 类新增 role_id 字段,作为 roles 表的外键,同时定义初始化角色的静态函数。
在 models.py 中添加代码:
现在先说说 init_roles 这个函数怎么用,这是一个类的静态函数,只要初始化了类,就可以直接调用。
我们先介绍一个有用的 Flask 插件 flask-script。
我们在 manage.py 文件中使用该插件,现在修改后的文件内容为:
执行 python manage.py shell
下面再执行 python manage.py runserver
好了,现在我们可以通过 init_roles 方法来初始化角色了,那么同理,我们也可以在 WebUser 类中定义静态函数,来初始化用户,在 models.py 中的 WebUser 类下添加代码:
现在我们继续用户角色的功能实现。
在 WebUser 表类中,再增加一个判断是否为 admin 的方法,这样就可以在路由函数中通过该方法来判断用户是否为 admin 用户,从而确定权限。
添加一个只有 admin 用户才能访问的测试页面,对于普通用户,则返回对应提示,在 main/views.py 中添加代码:
blcok 用户
我们可以通过 Block 功能,来实现控制用户登陆的功能。在 WebUser 类中我们添加对应的字段 block_status 和方法 is_block,接下来,我们只需要在 login 函数中,于调用 login_user 函数前,添加一个判断,如果用户是 block 状态,则不允许登陆:
models.py
auth/views.py 的 login 函数中添加
添加一个用户列表页,展示所有的用户和 block 与 unblock 按钮,在 templates/userlist.html 中添加代码:
然后是 block 函数的设计,首先判断用户是否为 admin,然后判断是否在 block 当前登陆的用户,再然后判断用户是否已经处于 block 状态,最后再更新数据库,在 main/views.py 中添加代码如下:
最后 block 页面如下:
短信集成
使用 twilio 发短信
我这里使用 twilio 提供的短信功能,它提供了一个免费的短信接口,让我们可以在完全 free 的状态下测试短信功能,同时也有对应的 python 库 twilio 来简化开发,只需要使用 pip install twilio 即可使用该公司提供的各种功能。有兴趣的同学可以移步官网查看: https://www.twilio.com/
发送短信模块
在 sendsms.py 中编写发送短信的逻辑,首先和前面产生确认邮件的 token 一样,这里也使用 itsdangerous 来加密 code:
然后是发送短信的代码,查看官网就可以获得,很简单,不多说了:
集成短信功能到系统
在 auth/forms.py 中新增两个表单, 一个是发送 verify code,一个是验证 verify code:
再在 auth/views.py 中新增两个路由,login_codesend 和 login_codeverify 分,别处理两个表单的提交逻辑。 login_codesend 函数,会向输入的手机号发送 verify code,并把产生的 code 传给 login_codeverify 函数,用于比较。
login_codesend 函数:
login_codeverify 函数:
最后我们更新登陆页面代码如下:
用户资料
gravatar 头像
使用业界流行的 gravatar,它可以把用户的 Email 和头像关联起来,快速生成用户头像图片,其中 Email 地址需要经过 md5 加密处理。 可以使用查询字符串的形式,在 url 中传入参数,来获取头像图片。
参数说明:
一个请求 URL 例子: http://secure.gravatar.com/avatar/ffd5c7d7bfcc8605aaa2d259e2590112?s=128&d=identicon&r=g
大家可以输入到浏览器中看看返回的图片是什么。
添加生成头像方法
在 WebUser 类中,添加生成头像的方法函数
丰富用户资料字段
为 WebUser 新增三个字段,以丰富用户资料
添加路由函数
添加展示用户 profile 的函数,这里对使用 GitHub 登陆的用户做了判断,优先展示 GitHub 的头像和用户名。在 main/views.py 中添加代码:
对于 user.html 模板页面,同样,首先判断是否 gituser,如果 gituser 不为 None,则展示 git 的用户名,同时也会展示用户自己设置的 nickname
编辑用户资料
在 main/forms.py 中定义表单,包括 username,nickname 和 about_me,用于提供给用户做修改
然后在路由函数中判断,如果 username 存在且不是当前用户时,不能修改 username,如果修改的 username 是唯一,那么更新数据库。在 main/views.py 中添加代码:
至此,我们的用户鉴权系统就基本完成了。当然,同学们可能也看到了,由于个人的能力有限,很多代码还是需要优化的,同时还有一些功能很棒,但是没有提供,例如:用户修改 Email 的功能,基于其他第三方系统的登陆这等,这些就当作作业,由感兴趣的你来完成吧。
总结
现在,我们再来总结下系统的整体功能
以上,就是本次 Chat 的全部功能实现,当然由于笔者水平有限,文中难免存在错误,还望大家能不吝指出,每天进步一小步,我们终会成为大神!
给出 GitHub 源码,仅供大家参考:
https://github.com/zhouwei713/flask-webauth
强烈建议大家把所有代码都手动写一遍,切不可直接复制粘贴,这两种方式,你获得的提升是完全不一样的!
同时,为了文章的可读性,在第三部分,除了代码,我并没有加入过多的其他展示图片,所以我这里提供了一个演示的地址,供大家学习: https://luobodazahui.top 我还初始化了一些用户,大家可以登陆上去,体验下相关功能。
最后提供一些免费的学习资料,感兴趣的同学可以自取: https://pan.baidu.com/s/1kjUsJ8XnmNhLeIIzyEMXvA 提取码: 4x5s https://pan.baidu.com/s/1oc6mONu068y3V_ulaw-BSQ 提取码: ef6f https://pan.baidu.com/s/1ht2KgazmD_DYjw3HBqkFYw 提取码: m9t3
附录
在最后的最后,我们再来说说如何部署我们的系统。
相信同学们还记得,我们代码里是有这两个文件的:
其中 gunicorn 就是作为 web 容器的,那么什么是 web 容器呢,从百度百科上看到定义:
那么 python 常用的容器就是 gunicorn 了,而我们项目里的 gunicorn 文件就是对应的配置文件。
其实大部分还是很好理解的,只解释个别参数,具体的配置,大家可以查看相关文档。
然后就是部署到 Linux 服务器了,使用一个简单的启动脚本,如 run.sh 所写:
这样,就在服务器本地的 5005 端口启动了我们的 Web 服务了。当然还有部署反向代理,如:Nginx 等,就不在本文的讨论范围了,有兴趣的同学可以私下一起找我交流。
最后,因为笔者的表达能力所限,可能有些功能或者用法并没有说的很清楚,所以这里我创建了一个供大家讨论的 QQ 群,大家有疑问可以提出,一起讨论,我也会在里面随时回答大家的疑惑,期待我们一同进步! QQ 群 :617870323
本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。
https://gitbook.cn/books/5c6df95292e9e173a2f99f2f/index.html
#1