Flask Web DEvelopment翻译7
大部分程序需要追踪用户身份。当用户连接到程序,通过一系列步骤使自己的身份被识别,这就是用户验证。一旦程序知道这个用户是谁,它就能提供定制化的体验。 大部分验证方法要求用户提供一个身份标识(用户名称或者邮件地址)和一个密码。本章将创建一个完整的身份认证系统。
Flask认证扩展优秀的Python认证包有很多,但它们都没有单蹦完成所有的任务的。本章的用户认证解决方案联合使用了多个包共同完成这一任务。下面是我们要用的包清单:
Flask-Login: 登陆后用户会话管理Werkzeug: 密码加密和密码验证itsdangerous: 加密安全令牌的生成和验证除了特定的认证包之外,下面是我们要用到的常规目的的一些扩展。
Flask-Mail: 发送验证邮件Flask-Bootstrap:HTML模板Flask-WTF: web表单密码加密在设计开发web程序过程中,存储在数据库的用户信息的安全性往往被忽视。如果攻击者黑进服务器,访问了你的用户数据库,你就有用户安全的风险。并且这种风险要比你想象的要大得多。这是众所周知的一个事实:大部分用户在多个网站上使用同样的密码,所以即使你没有存储任何敏感信息,只需获得你数据库里的用户密码,攻击者就可以访问其他网站上你这些用户的帐户了。(译注:奇葩如此。当年CSDN这样的大网站竟然都是明文存储密码。在2011年其密码数据库泄露后,众多网站纷纷中招——用户们都是一个密码|甚至一套名码走天下。
) 安全存储用户密码的关键就是不要存储明文密码,你应该存储它的hash(译注:哈希|散列
)值。密码哈希函数获取输入的明文密码后对其一次或多次加密转换,其结果是一个新的字符串序列,跟原文密码毫不相似。哈希密码可以与明文密码(译注:用户输入
)进行校验,因为哈希函数是可重复的:同样的输入一定会得到同样的结果输出。
使用Werkzeug哈希密码密码哈希是一个复杂的任务,很难确保没有疏漏。我不建议你自己实现加密验证这套方案,应该采用社区广泛认可的库。如果有兴趣学习加密验证,你可以去读读这些文章:Salted Password Hashing-Doing it Right
Werkzeug的安全模块通过两个函数方便地实现了密码哈希加密,分别用于注册和验证阶段:
generate_password_hash(password, method=pbkdf2:sha1, salt_length=8) :这个函数接收纯文本密码并返回经过哈希加密的一个字符串,该返回值可以放心的存入数据库。method和salt_length两个参数的默认值基本可以满足大部分用例需要了。(译注:当然,这两个参数可以指定别的值,从而带来更高的安全性。总的来说随着cpu技术进步,暴力破解<穷尽所有组合进行逐一猜测>所需要的时间也在相应缩短——理论上所有密码都可以被猜出来,当然所需要的时间可能无法接受——所以很对定长的用户密码,提高加密的复杂性/更长的salt/更多次的hash,使破解需要的时间尽可能长,是另一种安全保护考虑。
)check_password_hash(hash, password) : 这个函数分别从数据库获得散列后的密码,从用户获取输入的明文密码,进行比较。一旦这两个密码一致就返回True。例子8-1展示了第五章编写的User模型中密码加密部分的变更:
Example 8-1. app/models.py: Password hashing in User modelfrom werkzeug.security import generate_password_hash, check_password_hashclass User(db.Model): # ... password_hash=db.Column(db.String(128)) @property def password(self): raise AttributeError('password is not a readable attribute') @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)
密码哈希函数通过User类的password只写属性实现的。如果试图读取password属性,将返回一个错误。设置该属性时,setter方法会呼叫werkzeug的generate_password_hash()函数并把返回结果写到类字段password_hash。原始密码 一旦哈希就无法恢复(<small>译注:你只能比对哈希值而不能从哈希值还原原始密码</small>) verify_password方法获取用户输入的密码,并传递给werkzeug的check_pasword_hash()函数来验证是否与保存在User模型中的已经哈希过的密码一致。如果这个方法返回True,密码就是正确的。
密码哈希功能已经完成,你可以在shell中进行测试:
(venv) $ python manage.py shell>>> u=User()>>> u.password='cat'>>> u.password_hash'pbkdf2:sha1:1000$duxMk0OF$4735b293e397d6eeaf650aaf490fd9091f928bed'>>> u.verify_password('cat')True>>> u.verify_password('dog')False>>> u2=User()>>> u2.password='cat'>>> u2.password_hash'pbkdf2:sha1:1000$UjvnGeTP$875e28eb0874f44101d6b332442218f66975ee89'
注意,虽然使用了同一个密码,用户u和u2的哈希密码值完全不同。为了确认这一功能将来也能正常工作,我们把上面的测试写成一个单元测试以便于能重复测试。例子8-2展示了tests包中的一个新模块,它有三个新测试来检测User模型的最近更改:
Example 8-2. tests/test_user_model.py: Password hashing testsimport unittestfrom app.models import Userclass 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)
创建身份认证蓝图我们在第七章介绍了蓝图:把程序创建移动到工厂函数中后,定义全局路由。用户验证相关的路由可以添加到auth蓝图。要保持良好的代码结构,针对不同的程序功能集合设置不同的蓝图就是很好的办法。 auth蓝图保存在同名的python包里。蓝图包构造函数创建蓝图对象并从一个views.py模块中导入路由。请看来例子8-3:
Example 8-3. app/auth/__init__.py: Blueprint creationfrom flask import Blueprintauth=Blueprint('auth', __name__)from . import views
app/auth/views.py
模块,如例子8-4所示,导入了蓝图和验证相关的的路由定义(译注:使用装饰器@auth.route()
)。现在已经添加了一个'/login'路由,来显示同名的占位符模板。
Example 8-4. app/auth/views.py: Blueprint routes and view functionsfrom flask import render_templatefrom . import auth@auth.route('/login')def login(): return render_template('auth/login.html')
传给render_template()的模板文件被保存在auth文件夹。这个文件夹必须在app/templates
中创建,因为Flask默认所有模板都在这里。把蓝图模板保存在各自同名文件夹,避免了和main了蓝图或未来其他蓝图的模板名称冲突。
提醒:经过配置,蓝图也可以拥有自己独立的模板文件夹(<small>译注:不在app/templates中?</small>)。当配置了多个模板文件夹之后,render_template()函数将先搜索程序指定的模板文件夹,再搜索蓝图指定的模板文件夹。
auth蓝图需要添加到create_app()工厂函数中的程序,如例子8-5:
Example 8-5. app/__init__.py: Blueprint attachmentdef create_app(config_name):# ... from .auth import auth as auth_blueprint app.register_blueprint(auth_blueprint, url_prefix='/auth') return app
蓝图注册中的url_prefix参数是可选的。如果启用,在这个蓝图中定义的路由都会使用这个前缀(此处是/auth)注册。例如,/login路由会被注册成/auth/login,在开发服务器上的完整路径就是http://localhost:5000/auth/login
使用Flask-login实现用户验证当用户登录进入程序,必须记录下他们的验证授权状态,这样即使转到别的页面也可以记住已登录的用户。Flask-Login是个小巧但非常有用的扩展,它并没有绑定某个特定的验证方式,只是限于用户验证管理这方面(译注:不限制于某种验证方法(账号密码方式,openID,或其他),只负责管理用户是否可以验证通过,以及记住用户会话,退出系统??
)。 开始之前,我们需要在虚拟环境中安装该扩展:
(venv) $ pip install flask-login
准备登录用的用户模型为了配合User模型一起工作,Flask-Login扩展需要模型实现几个方法。如表8-1:
Table 8-1. Flask-Login user methodsMethod Descriptionis_authenticated() Must return True if the user has login credentials or False otherwise.is_active() Must return True if the user is allowed to log in or False otherwise. A False return value can be used for disabled accounts.is_anonymous() Must always return False for regular users.get_id() Must return a unique identifier for the user, encoded as a Unicode string.
这四个方法可以在模型类中直接以方法的形式实现,但我们有个更简单的备选——Flask-Login提供了一个UserMixin类,它已经默认实现了这些方法足以满足大部分应用场景。更新后的用户模型如里子8-6所示:
Example 8-6. app/models.py: Updates to the User model to support user loginsfrom flask.ext.login import UserMixinclass 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) password_hash=db.Column(db.String(128)) role_id=db.Column(db.Integer, db.ForeignKey('roles.id'))
注意,email字段也被添加上了。在这个程序里,用户使用email来登录系统,相比较用户名,一般很少有人会忘了自己邮件地址。 Flask-Login在工厂函数中进行初始化,如例子8-7所示:
Example 8-7. app/__init__.py: Flask-Login initializationfrom flask.ext.login import LoginManagerlogin_manager=LoginManager()login_manager.session_protection='strong'login_manager.login_view='auth.login'def create_app(config_name): # ... login_manager.init_app(app) # ...
Login_Manager对象的属性session_protection可以被设置成None,basic或者strong,来提供不同的安全等级对付用户会话攻击。如果设置为strong,Flask-Login将持续追踪客户端ip地址和浏览器标识,一旦发现变动就会要求重新登录。 login_view属性设置了登录页面的端点('auth.login')。你可能记着,login路由是在蓝图里的,这就需要添加蓝图名为前缀。 最终,Flask-Login要求程序设置一个回调函数,用来载入用户,作为身份识别。如例子8-8所示:
Example 8-8. app/models.py: User loader callback functionfrom . import login_manager@login_manager.user_loaderdef load_user(user_id): return User.query.get(int(user_id))
载入用户的回调函数接收一个Unicode字符串格式的用户标识id。如果该标识有效,回调函数的返回值则肯定是一个用户对象,否则返回None而不会报错。
保护路由为了保护路由只允许已登录用户访问,Flask-Login提供了login_required装饰器。用例如下:
from flask.ext.login import login_required@app.route('/secret')@login_requireddef secret(): return 'Only authenticated users are allowed!'
如果一个未经验证的用户试图访问该路由,Flask-Login就会中断请求,把用户转到登录页面。
添加登录表单将要显示给用户的登录表单页面,包含一个文本字段用来输入email地址,一个密码字段,一个"remember me"选框和一个提交按钮。例子8-9显示了Flask-WTF类:
Example 8-9. app/auth/forms.py: Login formfrom flask.ext.wtf import Formfrom wtforms import StringField, PasswordField, BooleanField, SubmitFieldfrom wtforms.validators import Required, Emailclass LoginForm(Form): email=StringField('Email', validators=[Required(), Length(1, 64),Email()]) password=PasswordField('Password', validators=[Required()]) remember_me=BooleanField('Keep me logged in') submit=SubmitField('Log In')
email字段利用了WTFForms提供的Length(),Email()验证器。PassowrdField类将显示为type="passowrd"的一个<input>元素。BooleanField类则显示为一个选择框。 与登录页面关联的模板是auth/login.html。这个模板只需要使用Flask-Bootstrap的wtf.quick_form()宏来显示表单即可。图8-1显示了浏览器中的登录表单:
Paste_Image.png在base.html模板中的导航条使用了jinja2的条件语句,根据当前用户的登录状态来显示“登录”或“退出”链接。这个条件语句显示在例子8-10:
Example 8-10. app/templates/base.html: Sign In and Sign Out navigation bar links<ul class="bcb3-e002-bb5b-14d1 nav navbar-nav navbar-right"> {% if current_user.is_authenticated() %} <li><a href="{{ url_for('auth.logout') }}">Sign Out</a></li> {% else %} <li><a href="{{ url_for('auth.login') }}">Sign In</a></li> {% endif %}</ul>
条件语句中的current_user变量由Flask-login定义,在视图函数和模板中自动生效。这个变量包含了当前已登录的用户或者一个匿名用户对象——如果未登录的话。匿名用户对象给is_authenticated()方法的响应是False,所以很方便就能知道当前用户是否已经登录。
用户登录视图函数login()的实现如例子8-11:
Example 8-11. app/auth/views.py: Sign In routefrom flask import render_template, redirect, request, url_for, flashfrom flask.ext.login import login_userfrom . import authfrom ..models import Userfrom .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): login_user(user, form.remember_me.data) return redirect(request.args.get('next') or url_for('main.index')) flash('Invalid username or password.') return render_template('auth/login.html', form=form)
视图函数创建了一个LoginForm对象,并像第四章中那样使用了这个简单表单。当这个请求的类型是GET,视图函数就只渲染这个模板——依次显示表单。如果这个表单被提交——请求类型是POST,Flask-WTF的validate_on_submit()函数就验证表单变量,然后试图登录用户。 为了登录用户,这个函数开始利用表单提供的email从数据库中加载用户。如果指定email的用户存在,就传递表单的password值给verify_password方法调用之。如果密码有效,Flask-login的login_user()函数就把用户登入系统,并察看表单的“remember me”的布尔值。如果该值为False,用户会话将在浏览器关闭的时候过期失效,用户下次就必须重新登录。如果值为True,就会给浏览器设置一个长期的cookie,用以恢复用户会话。(译注:由于cookie被加密存储在客户端,这样下次用户打开网站时浏览器就会读取cookie自动把用户登录进系统。
) 根据我们在第四章讨论过的POST/Redirect/GET模式,POST请求提交登录信息并以重定向为结束,但也有两个可能的URL跳转方向。如果是阻止为验证用户访问某URL而显示登录表单,Flask_login会把这一URL保存在“next”查询字符串参数中,你可以从request.args字典中访问。如果next查询字符串参数无不可用,就以重定向到home页面取代之。(译注:即,如果登录页面有历史url--你在登录前试图访问的地址,成功登录后就自动转向该url;反之,跳转到首页。
)如果用户提供的email或密码无效,就会设置闪现消息并重新向用户显示登录表单。
提醒:在生产服务器上,登录路由必须设置为基于安全http(
译注:https??
),保证提交给服务器的表单数据经过加密传输。没有安全http的话,登录信息在传输时会被拦截——这样在服务器上加密密码的所有努力统统白费了。
登录模板需要更新来显示表单。如例子8-12所示:
Example 8-12. app/templates/auth/login.html: Render login form{% extends "base.html" %}{% import "bootstrap/wtf.html" as wtf %}{% block title %}Flasky - Login{% endblock %}{% block page_content %}<div class="e002-bb5b-14d1-b7db page-header"> <h1>Login</h1></div><div class="bb5b-14d1-b7db-f2b0 col-md-4"> {{ wtf.quick_form(form) }}</div>{% endblock %}
退出登录退出路由的实现 如例子8-13所示:
Example 8-13. app/auth/views.py: Sign Out routefrom flask.ext.login import logout_user, login_required@auth.route('/logout')@login_requireddef logout(): logout_user() flash('You have been logged out.') return redirect(url_for('main.index'))
Flask-login调用了logout_user()函数来移除并重设用户会话 ,来退出登录状态。退出操作 设置了一个闪现消息确认完成,并重定向到首页。
测试登录为了测试登录函数正常工作,应该更新首页代码来显示已登录的用户名欢迎信息。模板中的欢迎信息段如例子8-14:
Example 8-14. app/templates/index.html: Greet the logged-in userHello,{% if current_user.is_authenticated() %}{{ current_user.username }}{% else %}Stranger{% endif %}!
在这个模板里,我们再次使用了current_user.is_authenticated()来判断用户是否登录成功。 由于我们还没有创建用户注册功能,现在只能先从shell中添加一个新用户:
(venv) $ python manage.py shell>>> u=User(email='john@example.com', username='john', password='cat')>>> db.session.add(u)>>> db.session.commit()
上面注册的用户登录后,将在首页显示对他的欢迎信息,如图8-2所示
Paste_Image.png用户注册要成为程序的一个正式用户,必须向系统注册自己信息才能登录。在登录页面上添加一个链接,供用户跳转到注册页面,来输入email地址,用户名和密码来实现注册。
添加注册表单注册表单用于注册页面,来供用户输入email地址,用户名和密码。如例子8-15:
Example 8-15. app/auth/forms.py: User registration formfrom flask.ext.wtf import Formfrom wtforms import StringField, PasswordField, BooleanField, SubmitFieldfrom wtforms.validators import Required, Length, Email, Regexp, EqualTofrom wtforms import ValidationErrorfrom ..models import Userclass RegistrationForm(Form): email=StringField('Email', validators=[Required(), Length(1, 64),Email()]) username=StringField('Username', validators=[Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,'Usernames must have only letters, numbers, dots or underscores')]) password=PasswordField('Password',validators=[Required(), EqualTo('password2', message='Passwords must match.')]) password2=PasswordField('Confirm password', validators=[Required()]) submit=SubmitField('Register') def validate_email(self, field): if User.query.filter_by(email=field.data).first(): raise ValidationError('Email already registered.') def validate_username(self, field): if User.query.filter_by(username=field.data).first(): raise ValidationError('Username already in use.')
这个表单使用了WTForms内置验证器Regexp来确保username字段只包含字母、数字、下划线和点号。这个验证器在表达式后面的两个参数分别是正则表达式标志位(原文the regular expression flags
<small> 不明白。</small>查看WTForm文档:*flags* – The regexp flags to use, for example re.IGNORECASE. Ignored if regex is not a string.意思是像re.ignorecase<忽略大小写>的话,如果regex是非字符串就忽略,不执行验证???更迷糊了
)和供验证失败显示的错误信息。 为了安全,要求输入两次密码。因此要保证这两次输入的内容一致,就得使用WTForms的另外一个验证器EqualTo进行验证。这个验证器被添加到密码字段,把另外一个字段名作为参数。 表单还有两个自定义的验证器(作为表单的方法实现)。一旦表单定义了一个方法,其格式是前缀validate_+字段名,那么除了常规验证器之外这个方法也会被调用。本例中,自定义的email和username验证器可以确保这两个值不会与数据库中已有值重复。如验证失败,自定义验证器将抛出一个带有文本错误信息为参数的ValidationError错误。 显示模板就是/tmplates/auth/register.html
。跟登录(login)模板一样,它也通过wft.quick_form()
来显示表单。注册页面如图8-3:
注册页面需要从登录页面链接过来,以便于没有系统帐户的用户能找到注册地方。更改如例子8-16所示:
Example 8-16. app/templates/auth/login.html: Link to the registration page<p> New user? <a href="{{ url_for('auth.register') }}">Click here to register </a></p>
注册新用户对用户注册的处理没有什么奇特之处。当提交注册表并验证通过后,一个新帐户就被添加到数据库里。完成这一任务的视图函数如例子8-17所示:
Example 8-17. app/auth/views.py: User registration route@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('You can now login.') return redirect(url_for('auth.login')) return render_template('auth/register.html', form=form)
帐户确认对于某些类型的程序来说,确认用户信息真实有效非常重要。一个最常见的要求是可以通过用户的邮件联系该用户。 要验证email地址,程序只需在用户注册完成后立即向用户的email发送一封确认邮件。用户的新帐户被初始化为未确认,直到根据邮件中的指示操作完成才会正式启用。帐户确认过程一般是点击一个特定的包含确认令牌的URL链接。
使用itsdangerous生成确认令牌最简单的帐户确认链接就是一个包含在email中的http://www.example.com/auth/confirm/<id>
格式的URL,此处的id就是用户在数据库中的主键id的数值。当用户点击链接,视图函数就会处理该路由,把id作为确认参数来更新用户的确认状态。 但这绝对不是一个安全的处理,任何用户只要能识别出确认链接的格式就能通过URL来发送随机数字确认任意帐户。所以,用一个包含同样但加密了的信息的令牌来替换url中的id就更安全些。 如可能你还记得我们在第四章用户会话中的那些讨论,Flask使用了加密签名的cookies来保护用户会话不受攻击。这些安全cookie都是使用itsdangerouse包来加密的。同样,我们可以把这一思路用于确认令牌。 下面是一个简短的shell会话,展示了itsdangerous如何生成包含用户id的安全令牌:
(venv) $ python manage.py shell>>> from manage import app>>> from itsdangerous import TimedJSONWebSignatureSerializer as Serializer>>> s=Serializer(app.config['SECRET_KEY'], expires_in=3600)>>> token=s.dumps({ 'confirm': 23 })>>> token'eyJhbGciOiJIUzI1NiIsImV4cCI6MTM4MTcxODU1OCwiaWF0IjoxMzgxNzE0OTU4fQ.ey ...'>>> data=s.loads(token)>>> data{u'confirm': 23}
itsdangerous提供了多种令牌生成方式。其中,TimedJSONWebSignatureSerializer类会生成带有超时限制的JSON Web签名(JWS,包含了时间戳。)。这个类的构造函数需要一个加密密匙(key)作为参数,你可以使用Flask程序配置中的SECRET_KEY。
发送确认邮件当前/register路由在完成添加用户之后就会重定向到/index。在重定向之前,这个路由需要发送确认邮件。变更如例子8-19所示:
Example 8-19. app/auth/views.py: Registration route with confirmation emailfrom ..email import send_email@auth.route('/register', methods=['GET', 'POST'])def register(): form=RegistrationForm() if form.validate_on_submit(): # ... db.session.add(user) db.session.commit() token=user.generate_confirmation_token() send_email(user.email, 'Confirm Your Account','auth/email/confirm', user=user, token=token) 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)
注意,即使程序配置中设置了请求结束时自动提交,也必须显式调用db.session.commit()
。这是因为只有提交到数据库后,新用户才会取得id,这样生成确认令牌时才有id可用——不能延迟提交(译注:程序配置是在请求结束后再提交,在这里明显是晚了:Config类中 SQLALCHEMY_COMMIT_ON_TEARDOWN=True
)。 认证蓝图使用的email模板存储在templates/auth/email
文件夹,与其他模板分离。就行第六章讨论的那样,每个email有两个正文模板——纯文本和富文本格式。例子8-20展示了确认邮件模板的纯文本格式,你可以在我的Github库里找到HTML版本的模板。
Example 8-20. app/auth/templates/auth/email/confirm.txt: Text body of confirmation emailDear {{ user.username }},Welcome to Flasky!To confirm your account please click on the following link:{{ url_for('auth.confirm', token=token, _external=True) }}Sincerely,The Flasky TeamNote: replies to this email address are not monitored.
默认情况下,url_for()会生成一个相对路径的url,所以,像url_for('auth.conrim',token='abc' )将返回字符串'/auth/confirm/abc'。这个对于通过邮件来发送的地址来说当然是无效的——在程序内部使用相对地址没有问题,是因为浏览器会利用当前页面的主机名和端口把它转换成绝对地址。但通过email发送出的url可是没有转换需要的上下文的。所以_external=True
参数就用上了,这样url_for()就可以生成一个包含协议名称(http://或https://
)以及主机名,端口信息的完整url(绝对URL地址
)。 确认帐户的视图函数如例子8-21所示:
Example 8-21. app/auth/views.py: Confirm a user accountfrom flask.ext.login import current_user@auth.route('/confirm/<token>')@login_requireddef confirm(token): if current_user.confirmed: return redirect(url_for('main.index')) if current_user.confirm(token): 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方法,然后根据结果闪现消息。如果确认成功,User模型的confirmed属性就会改变并被添加到会话中,在请求结束的时候会被提交到数据库更新。 程序可以自行决定未经确认的用户在确认帐户之前可以做什么。一个可能是允许未确认用户登录,但是只显示一个要求进行帐户确认的页面。 这一步可以使用Flask的before_request钩子,在第二章中有过描述。在蓝图中,before_request钩子只会响应本蓝图的请求。要想在蓝图中使用整个程序范围的钩子,就必须使用before_app_request装饰器了。例子8-22说明了如何实现这一处理器:
Example 8-22. app/auth/views.py: Filter unconfirmed accounts in before_app_request handler@auth.before_app_requestdef before_request(): if current_user.is_authenticated() and not current_user.confirmed and request.endpoint[:5] !='auth.': return redirect(url_for('auth.unconfirmed'))@auth.route('/unconfirmed')def unconfirmed(): if current_user.is_anonymous() or current_user.confirmed: return redirect('main.index') return render_template('auth/unconfirmed.html')
如果三个条件为真,before_app_request处理器将中断一个请求:
用户已登录(current_user.is_authenticated()的返回为True)该用户帐户未经确认请求的端点|结束点(可写作request.endpoint,译注:也就是路由名称
)超出了认证蓝图(auth)的范围。要访问的这个认证蓝图的路由需要授权,比如那些允许用户确认帐户或执行其他帐户管理的函数(译注:迷糊了:Access to the authentication routes needs to be granted, as those are the routes that will enable the user to confirm the account or perform other account management functions.
)如果这三个条件都符合,那个就会触发一个重定向,转到/auth/unconfirmed
路由,显示一个关于帐户确认信息的页面。
警告:before_request或before_app_request回调返回一个响应或者一个重定向,Flask将把它发送给客户端,而不会再调用与这一请求相关的视图函数。这样,回调就能在必要的时候中断请求。
显示的确认信息页面(图8-4)只是给未确认用户显示一个模板,介绍如何确认其帐户,并附加一个链接用户重新请求确认邮件——如原始确认邮件丢失。例子8-23路由将再次发送一个确认邮件:
Example 8-23. app/auth/views.py: Resend account confirmation email@auth.route('/confirm')@login_requireddef resend_confirmation(): token=current_user.generate_confirmation_token() send_email('auth/email/confirm','Confirm Your Account', user, token=token) flash('A new confirmation email has been sent to you by email.') return redirect(url_for('main.index'))
这个路由使用current_user(已登录用户)作为目标用户,再次重复了注册路由的工作(给用户发送确认邮件)。这个路由其也是被login_required装饰器保护的,这样确保访问路由的发送这一请求用户已登录。图8-4
Paste_Image.png帐户管理程序的用户可能不时的需要更改自己的帐户信息,我们可以根据本章介绍的技术来给认证蓝图添加一些功能:
密码更改 具有安全意识的用户可能不定期更改自己的密码。这个功能很容易实现,因为一旦用户登录系统,我们可以比较安全的询问其旧密码和输入新密码,通过表单进行新旧替换即可。密码重置 为了防止合法用户遗忘密码后被挡在系统之外,我们需要提供一个密码重置的选项。为了安全实现密码重试,需要像进行帐户确认那样使用一个加密令牌。这样,一旦用户请求密码重设,习题集就给注册的Emailizhi发送一个重设令牌。用户点击邮件中的链接,验证令牌通过后,通过表单输入新的密码即可完成重置。变更Email地址 用户可能也需要更改其注册的email地址,同样需要通过一封确认邮件来验证这一新email地址。用户在表单中输入新地址然后系统就给新地址发送令牌。一旦服务器回收到令牌,就可以验证并更新用户对象了。在服务器等待回收令牌期间,他会把新地址保存到一个数据库新字段中(附加email地址),或者可以把心邮件地址和id一起保存到令牌中。注:以上三个功能还是参照注册,确认帐户两步,利用current_user,分别生成token后进行更新即可
在下一章,我们将扩展用户子系统,加入用户角色。
<<第七章 大型程序架构 第九章 用户资料>>
这是一个快速教程,用来展示如何通过 Flask(目前发展最迅速的 Python 框架之一)来从服务器获取数据。-- Rachel Waston(作者)
Python 是一个以语法简洁著称的高级的、面向对象的程序语言。它一直都是一个用来构建 RESTful API 的顶级编程语言。
Flask 是一个高度可定制化的 Python 框架,可以为开发人员提供用户访问数据方式的完全控制。Flask 是一个基于 Werkzeug 的 WSGI 工具包和 Jinja 2 模板引擎的”微框架“。它是一个被设计来开发 RESTful API 的 web 框架。
Flask 是 Python 发展最迅速的框架之一,很多知名网站如:Netflix、Pinterest 和 LinkedIn 都将 Flask 纳入了它们的开发技术栈。下面是一个简单的示例,展示了 Flask 是如何允许用户通过 HTTP GET 请求来从服务器获取数据的。
初始化一个 Flask 应用
首先,创建一个你的 Flask 项目的目录结构。你可以在你系统的任何地方来做这件事。
$ mkdir tutorial$ cd tutorial$ touch main.py$ python3 -m venv env$ source env/bin/activate(env) $ pip3 install flask-restfulCollecting flask-restfulDownloading https://files.pythonhosted.org/packages/17/44/6e49...8da4/Flask_RESTful-0.3.7-py2.py3-none-any.whlCollecting Flask>=0.8 (from flask-restful)[...]
导入 Flask 模块
然后,在你的 main.py 代码中导入 flask 模块和它的 flask_restful 库:
from flask import Flaskfrom flask_restful import Resource, Apiapp=Flask(__name__)api=Api(app)class Quotes(Resource):????def get(self):????????return {????????????'William Shakespeare': {????????????????'quote': ['Love all,trust a few,do wrong to none',????????????????'Some are born great, some achieve greatness, and some greatness thrust upon them.']????????},????????'Linus': {????????????'quote': ['Talk is cheap. Show me the code.']????????????}????????}api.add_resource(Quotes, '/')if __name__=='__main__':????app.run(debug=True)
运行 app
Flask 包含一个内建的用于测试的 HTTP 服务器。来测试一下这个你创建的简单的 API:
(env) $ python main.py * Serving Flask app "main" (lazy loading) * Environment: production?? WARNING: This is a development server. Do not use it in a production deployment.?? Use a production WSGI server instead. * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
启动开发服务器时将启动 Flask 应用程序,该应用程序包含一个名为 get 的方法来响应简单的 HTTP GET 请求。你可以通过 wget、curl 命令或者任意的 web 浏览器来测试它。
$ curl http://localhost:5000{????"William Shakespeare": {????????"quote": [????????????"Love all,trust a few,do wrong to none",????????????"Some are born great, some achieve greatness, and some greatness thrust upon them."????????]????},????"Linus": {????????"quote": [????????????"Talk is cheap. Show me the code."????????]????}}
要查看使用 Python 和 Flask 的类似 Web API 的更复杂版本,请导航至美国国会图书馆的 Chronicling America 网站,该网站可提供有关这些信息的历史报纸和数字化报纸。
为什么使用 Flask?
Flask 有以下几个主要的优点:
Python 很流行并且广泛被应用,所以任何熟悉 Python 的人都可以使用 Flask 来开发。它轻巧而简约。考虑安全性而构建。出色的文档,其中包含大量清晰,有效的示例代码。还有一些潜在的缺点:
它轻巧而简约。但如果你正在寻找具有大量捆绑库和预制组件的框架,那么这可能不是最佳选择。如果必须围绕 Flask 构建自己的框架,则你可能会发现维护自定义项的成本可能会抵消使用 Flask 的好处。如果你要构建 Web 程序或 API,可以考虑选择 Flask。它功能强大且健壮,并且其优秀的项目文档使入门变得容易。试用一下,评估一下,看看它是否适合你的项目。
在本课中了解更多信息关于 Python 异常处理以及如何以安全的方式进行操作。
via: https://opensource.com/article/19/11/python-web-api-flask
作者: Rachel Waston 选题: lujun9972 译者: hj24 校对: wxy
本文由 LCTT 原创编译, Linux中国 荣誉推出
点击“了解更多”可访问文内链接
发表评论