前序文章
- Flask 前后端分离实践:Todo App(1) 使用 Vue.js 搭建 Todo App
在上一篇文章里我们已经用 Flask+Vue 搭建了一个可以把数据持久化到服务器的 Todo App。那么,为了让多人一起使用这个 App,我们需要对数据按用户做隔离,这样就自然需要一个注册/登录界面。在前后端分离的架构里,我们是怎么验证用户,保持会话的呢?
用户登录
先复习一下以往用 Flask 是怎么解决这问题的,没错,通过 Flask-Login 模块,从 request 中获取用户名和密码,验证通过后用login_user
记录到会话中,之后的请求就会带有登录信息了。如果要退出登录,只需要调一下logout_user
就可以了。
那么使用前后端分离以后,所有对后端的请求都是以 Ajax 的方式发送,上面的方法依然有效!区别仅仅在于,我们将请求改成 JSON 格式之后,后端是从request.get_json()
中获取的。为此,我们专门建立一个名为auth
的蓝图:
@bp.route('/login', methods=['POST'])
def login():
user_data = request.get_json()
form = LoginForm(data=user_data)
if form.validate():
user = form.get_user()
login_user(user, remember=form.remember.data)
return jsonify({'status': 'success', 'user': user.to_json()})
return jsonify({'status': 'error', 'message': form.errors}), 403
后端只接收 POST 请求,因为 GET 都在前端那边,自然也就没有 login_view 的配置了。前端那边,axios 发请求时自动会带上 cookie,所以后端这边依然可以通过flask_login.current_user
拿到当前用户。
表单与验证
现在我们需要一个包含表单的登录页面,而我们知道,所有的页面都是前端渲染。所以这里 wtform 或 flask-boostrap 就不太能派上用场了。好在表单也比较简单,不是很难写。
<template>
<form action="/auth/login" method="post">
<h2>Login</h2>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" v-model="username" required />
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" v-model="password" required />
</div>
<div class="form-group">
<label for="remember">
<input type="checkbox" id="remember" name="remember" v-model="remember" />
Remember Me
</label>
</div>
<div class="form-footer">
<button type="submit" name="submit" class="btn">Submit</button>
<router-link to="/" class="btn">Return Home</router-link>
</div>
</form>
</template>
有一表单验证的工作,比如必填项,长度限制等,完全不需要后端的,可以在前端完成。我们需要写一个提交的函数,绑定到表单的 submit 动作上:
methods: {
checkForm (e) {
e.preventDefault()
const vm = this
api.login({
username: this.username,
password: this.password,
remember: this.remember
}).then(data => {
vm.$router.push({ path: '/' }, () => {
vm.success('Logged in successfully!')
})
}).catch(e => {
let errors = e.response.data.message
for (let key in errors) {
errors[key].forEach(e => {vm.error(`${key}: ${e}`)})
}
})
}
}
但有些验证工作,比如密码校验,还是要麻烦后端的,所以这里我们获取后端返回的错误(储存在data.message
中),然后依次渲染在页面中(这里我使用了一个 Vue 的插件Vue-flask-message来完成)。
后端验证这一块,由于没有渲染需求了,可以不用 wtform 这一套,改用marshmallow,但为了后面的方便,我还是使用了 Flask-WTF,把验证放到表单类里。
from flask_wtf import FlaskForm
class LoginForm(FlaskForm):
username = StringField('Username', validators=[Length(max=64)])
password = PasswordField('Password', validators=[Length(8, 16)])
remember = BooleanField('Remember Me')
def validate_username(self, field):
if not self.get_user():
raise ValidationError('Invalid username!')
def validate_password(self, field):
if not self.get_user():
return
if not self.get_user().check_password(field.data):
raise ValidationError('Incorrect password!')
def get_user(self):
return User.query.filter_by(username=self.username.data).first()
完成了登录部分,那么注册界面也大同小异,总结起来,大致思想是:
- 对于无需后端的验证,由前端完成。
- 后端的验证,通过响应内容传回错误。
- 验证错误通过 Vue-flash-message 显示到页面上。
- login 和 register 的视图函数仅处理 POST 请求。