python web学习4-表单的登录和验证

本文详细介绍了在Flask应用中实现用户登录功能,包括Flask-Login的使用、密码安全处理、User模型的设计,以及邮箱验证过程中的令牌生成与邮件发送。此外,还探讨了如何结合邮箱确认来提升账户安全性。
摘要由CSDN通过智能技术生成


前言

随着进一步学习,我进入了一个比较大的应用开发阶段:社交博客。这个应用比较重要的一个部分,就是用户登录。之前也学习过这部分,但是那只是通过简单的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函数完成确认。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值