Frost MingFrost's Blog

Flask前后端分离实践:Todo App(2)

Flask前后端分离实践:Todo App(2)

前序文章

本文项目地址: https://github.com/frostming/flask-vue-todo

在上一篇文章里我们已经用 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 动作上:

export default {
  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) => {
          const errors = e.response.data.message;
          for (const 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 请求。

评论