文章目录
前言
随着进一步学习,我进入了一个比较大的应用开发阶段:社交博客。这个应用比较重要的一个部分,就是用户登录。之前也学习过这部分,但是那只是通过简单的html网页表单来实现,而今天要介绍的是在flask框架中实现用户登录,以及邮箱的验证。
一、登录
1. 引入Flask-Login
登录部分主要用到了一个扩展:Flask-Login。
首先在虚拟环境中安装一下:
pip install flask_login
之后在应用实例app创建后进行初始化:
# app/__init__.py
from flask_login import login_manager
# ...
app = Flask(__name__)
login = login_manager(app)
# ...
2. User模型
登录的思想简单来说就是,获取登录界面输入框中的值,与数据库中已知的值进行对比,结果一致就通过,否则不通过。数据库中与用户登录相关的模型就是User模型,这里存贮着所有的用户名和密码。这里主要介绍一下密码。
2.1 密码
众所周知,密码是不能明文显示的,否则将有很大的安全隐患。所以一般使用密码的哈希值保存密码。flask中实现密码哈希的包是Werkzeug,下面的例子演示了如何哈希密码:
>>> from werkzeug.security import generate_password_hash
>>> hash = generate_password_hash("cat")
>>> hash
'pbkdf2:sha256:150000$i9Ar5OCX$83eb7081b469c518d6053cadf32ae13d54a6688b44b6859d94cfa754581f6d23'
以及如何验证密码:
>>> from werkzeug.security import check_password_hash
>>> check_password_hash(hash, "cat")
True
>>> check_password_hash(hash, "dog")
False
因此User模型中要做出对应更改:
# app/models.py
from werkzeug.security import generate_password_hash, check_password_hash
# ...
class User(db.Model):
# ...
username = db.Column(db.String(64), validators=[DataRequired()])
password_hash = db.Column(db.String(128))
# ...
@property
def password(self):
raise AttributeArror('password is not a readable attribute')
# ...
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
@property装饰器使得User.password
会报错,保证密码无法被直接访问,进一步保证了数据的安全性。
2.2 UserMixin
Flask-Login这个扩展在用户模型上有一些属性和方法,必须的四项如下:
- is_authenticated: 是否通过登录认证,返回True或False。
- is_active: 如果用户账户是活跃的,那么这个属性是True,否则就是False。
- is_anonymous: 常规用户返回False,匿名用户返回True。
- get_id(): 返回用户的唯一id,返回结果是字符串。
Flask-Login使用UerMixin将它们整合起来。修改User模型:
# app/models.py
# ...
from flask_login import UserMixin
# ...
class User(UserMixin, db.Model):
# ...
2.3 加载用户
登录之后,整个应用中的user需要指向当前登录的这个用户,这样才能在用户导航到新页面时追踪到该用户。
flask中使用装饰器@login.user_loader
来为用户加载功能注册函数:
# app/models.py
from app import login
# ...
@login.user_loader
def load_user(id):
return User.query.get(int(id))
# ...
加载用户的这个函数实际上就是通过用户id直接返回用户实例。
3. 登录视图函数login
# app/auth/views.py
from flask_login import current_user, login_user
from app.models import User
# ...
@auth.route('/login', methods=['GET','POST']):
def login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = Login_Form()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid usename or password')
return redirect(url_for('auth.login'))
login_user(user, remember=form.remember_me.data)
return redirect(url_for('main.index'))
return render_template('auth/login.html', title='Sign In', form=form)
通过查资料和源码,我得知了flask中登录的浅层原理。
登录功能主要通过login_user()
这个函数实现。这个函数将user_id写入会话session,同时将user加入请求上下文的栈顶;
然后通过login_manager.py
中的另一个函数_load_user()
,获取栈顶的user。由于会话中已经有了session[user_id],其值用user_id表示,即user_id = session.get('user_id')
。还记得前面说的通过装饰器修饰的加载用户功能的吗?调用这个加载用户的函数,即通过load_user(user_id)
,最终将用户实例加载出来。
至此,登陆功能基本实现。
二、邮箱验证
平时注册某些网站,在注册完成之后都会在邮箱中收到一封确认邮件,点击邮件中的链接完成认证,才可以正常登录。那么这个功能在flask中是如何实现的呢?
1. 使用itsdangerous生成确认令牌
邮件中最简单的确认链接是http://www.example.com/auth/confirm/这种形式的URL,但是直接显示id的话可以人为修改,存在安全隐患,解决办法就是使用id生成一个令牌,且只有服务器可以生成。
下面演示如何使用itsdangerous包生成包含用户id的签名令牌:
(venv) flask shell
>>> from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
>>> s = Serializer(app.config['SECRET_KEY'], expires_in=3600)
>>> token = s.dumps({'confirm': 23})
>>> token
b'eyJhbGciOiJIUzUxMiIsImlhdCI6MTYxNTQzMDczMCwi...'
>>> data = s.loads(token)
>>> data
{'confirm': 23}
TimedJSONWebSignatureSerializer类会生成具有过期时间的令牌,接收一个秘钥作为参数,flask中使用SECRET_KEY设置。
因此需要对用户模型做如下改动,增加生成和校验令牌的功能:
# app/models.py
# ...
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from flask import curren_app
class User(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}).decode('utf-8')
def confirm(self, token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token.encode('utf-8'))
except:
return False
if data.get('confirm') != self.id:
return False
self.confirmed = True
db.session.add(self)
return True
2. 发送确认邮件
注册的路由是/register,注册完成之后要将用户重定向到/index,在重定向之前,应该发送一封确认邮件。
# app/auth/views.py
from ..email import send_mail
@auth.route('/register', methods=['GET','POST'])
def register():
form = RegistrationForm()
if from.validate_on_submit():
# ...
db.session.add(user)
# 由于确认令牌需要用到用户的id,所有必须commit()以生成id
db.session.commit()
token = user.generate_confirmation_token()
# 调用发送邮件的函数
send_email(参数们)
flash('A confirmation email has been sent to you by email.')
return redirect(url_for('main.index'))
return render_template('auth/register.html', form=form)
发送的邮件纯文本内容如下:
Dear {{ user.username }},
Welcome to Flasky!
To confirm your account please click on the following link:
{{ url_for('auth.confirm'), token=token, _external=True }}
Sincerely
_external=True保证生成的是绝对URL。
下面是邮件中auth.confirm对应的代码:
# app/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):
db.session.commit()
flash('You have confirmed your account. Thanks!')
else:
flash('The confirmation link is invalid or has expired.')
return redirect(url_for('main.index'))
login_required装饰器保证要先登录才能执行此函数;
如果用户已经确认过,直接重定向到首页;
如果用户没有确认过,执行User模型的confirm函数,进行用户的验证,并闪现消息。
最后还有一个需求:每次在用户登录之后,检查用户邮箱是否确认,如果未确认,只能显示一个页面,提示用户需要确认邮箱。这个功能可以通过flask提供的before_request钩子完成。
# app/auth/views.py
# ...
@auth.before_app_request
def before request():
if current_user.is_authenticated\
and not current_user.confirmed\
and request.blueprint != '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')
同时满足以下3个条件时,before_app_request会拦截请求:
- 用户已登录。
- 用户的账户未确认。
- 请求的URL不在身份验证蓝本中,也不是对静态文件的请求。
如果请求同时满足以上条件,会被重定向到/auth/unconfirmed路由,显示如下页面:
只有进入邮箱点击链接进行确认,再次登录才会正常进入首页。
三、总结
登录:首先login=login_manager(app)初始化Login-Manager,然后装饰器@login.user_loader注册加载用户函数,最后在登录的视图函数中使用login_user()加载用户对象实例,同时赋值给当前请求的current_user上下文变量。
邮箱验证:使用TimedJSONWebSignatureSerializer生成加密签名,传入用户ID生成确认令牌字符串。注册完成或者首次登陆,会发送一封含有确认令牌的邮件,点击链接,进入确认账户的路由’/confirm’,调用user的confirm函数完成确认。