SpringBoot学习笔记-配置MySQL与实现注册登录模块(下)

笔记内容转载自 AcWing 的 SpringBoot 框架课讲义,课程链接:AcWing SpringBoot 框架课

本节实现登录与注册前端页面,并将 JWT 令牌存储在浏览器的 LocalStorage 中以实现登录状态的持久化。

1. 实现登录页面

打开我们的前端项目代码,在 src/views/user 目录下创建 account 目录,然后创建 UserAccountLoginViewUserAccountRegisterView 组件。

我们需要在全局存一些信息,例如每个页面都需要知道当前登录用户的信息,这就需要用到 Vue 的一个特性叫做 vuex。在 src/store 目录下创建 user.js

import $ from "jquery";

export default {
    
    
  state: {
    
      // 存储的信息
    id: "",
    username: "",
    photo: "",
    jwt_token: "",
    is_login: false,
  },
  getters: {
    
    },
  mutations: {
    
      // 用来修改数据
    updateUser(state, user) {
    
    
      state.id = user.id;
      state.username = user.username;
      state.photo = user.photo;
      state.is_login = user.is_login;
    },
    updateJwtToken(state, jwt_token) {
    
    
      state.jwt_token = jwt_token;
    },
  },
  actions: {
    
    
    login(context, data) {
    
    
      $.ajax({
    
    
        url: "http://localhost:3000/user/account/login/",
        type: "POST",
        data: {
    
    
          username: data.username,
          password: data.password,
        },
        success(resp) {
    
    
          if (resp.result === "success") {
    
    
            context.commit("updateJwtToken", resp.jwt_token);
            data.success(resp);  // 成功后的回调函数
          }
        },
        error(resp) {
    
    
          data.error(resp);  // 失败后的回调函数
        },
      });
    },
    getInfo(context, data) {
    
    
      $.ajax({
    
    
        url: "http://localhost:3000/user/account/info/",
        type: "GET",
        headers: {
    
    
          // 不是固定的,是官方推荐的写法,Authorization是在我们的后端JwtAuthenticationTokenFilter类中设置的
          Authorization: "Bearer " + context.state.jwt_token,
        },
        success(resp) {
    
    
          if (resp.result === "success") {
    
    
            context.commit("updateUser", {
    
    
              ...resp,
              is_login: true,
            });
            data.success(resp);
          }
        },
        error(resp) {
    
    
          data.error(resp);
        },
      });
    },
  },
  modules: {
    
    },
};

然后需要将其引入到 store 目录下的 index.js 中:

import {
    
     createStore } from "vuex";
import ModuleUser from "./user";

export default createStore({
    
    
  state: {
    
    },
  getters: {
    
    },
  mutations: {
    
    },
  actions: {
    
    },
  modules: {
    
    
    user: ModuleUser,
  },
});

现在就可以实现我们的登录前端页面 UserAccountLoginView

<template>
  <div class="container">
    <div class="card" style="margin-top: 20px;">
      <div class="card-header">
        <h3 style="display: inline-block;">Login</h3>
        <div style="float: right; height: 2.5rem; line-height: 2.5rem;">
          <span>还没有账号?</span>
          <router-link :to="{ name: 'user_account_register' }" style="text-decoration: none;">
            去注册 >
          </router-link>
        </div>
        <div style="clear: both;"></div>
      </div>
      <div class="card-body">
        <div class="row justify-content-md-center">
          <div class="col-md-5">
            <div class="card" style="margin: 6rem auto; box-shadow: 5px 5px 20px #aaa;">
              <div class="card-header text-center">
                <h1>用户登录</h1>
              </div>
              <div class="card-body">
                <div class="row justify-content-md-center">
                  <div class="col col-md-8">
                    <!-- @submit后的prevent是阻止掉submit的默认行为,防止组件间的向上或向下传递 -->
                    <form style="margin: 1rem;" @submit.prevent="login">
                      <div class="mb-3">
                        <label for="username" class="form-label">Username</label>
                        <input v-model="username" type="text" class="form-control" id="username" placeholder="请输入用户名" />
                      </div>
                      <div class="mb-3">
                        <label for="password" class="form-label">Password</label>
                        <input v-model="password" type="password" class="form-control" id="password" placeholder="请输入密码" />
                      </div>
                      <div style="font-size: 1rem; color: red;">
                        {
   
   { error_message }}
                      </div>
                      <button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 10px;">
                        登录
                      </button>
                    </form>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import {
      
       useStore } from "vuex";
import {
      
       ref } from "vue";
import router from "@/router/index";

export default {
      
      
  setup() {
      
      
    const store = useStore();
    let username = ref("");
    let password = ref("");
    let error_message = ref("");

    const login = () => {
      
      
      error_message.value = "";
      store.dispatch("login", {
      
        // 使用dispatch调用store的actions中的函数
        username: username.value,  // ref变量取值用.value
        password: password.value,
        success(resp) {
      
        // actions中的回调函数会返回resp
          console.log(resp);
          store.dispatch("getInfo", {
      
      
            success(resp) {
      
      
              console.log(resp);
              router.push({
      
       name: "home" });  // 跳转至home页面
            },
          });
        },
        error(resp) {
      
      
          console.log(resp);
          error_message.value = "The username or password is wrong!";
        },
      });
    };

    return {
      
      
      username,
      password,
      error_message,
      login,
    };
  },
};
</script>

<style scoped></style>

我们的导航栏也要根据登录状态显示不同的内容,可以用 v-ifv-else 来根据条件决定是否显示内容:

<template>
  <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
    <div class="container">
      <router-link class="navbar-brand" :to="{ name: 'home' }">King of Bots</router-link>
      <div class="collapse navbar-collapse" id="navbarText">
        ...
        <ul class="navbar-nav" v-if="$store.state.user.is_login">
          <li class="nav-item dropdown">
            <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
              {
   
   { $store.state.user.username }}
            </a>
            <ul class="dropdown-menu">
              <li>
                <router-link class="dropdown-item" :to="{ name: 'user_mybots_index' }">My Bots</router-link>
              </li>
              <li><hr class="dropdown-divider" /></li>
              <li><a class="dropdown-item" href="#">退出</a></li>
            </ul>
          </li>
        </ul>
        <ul class="navbar-nav" v-else>
          <li class="nav-item">
            <router-link :class="route_name == 'user_account_login' ? 'nav-link active' : 'nav-link'" :to="{ name: 'user_account_login' }">登录</router-link>
          </li>
          <li class="nav-item">
            <router-link :class="route_name == 'user_account_register' ? 'nav-link active' : 'nav-link'" :to="{ name: 'user_account_register' }">注册</router-link>
          </li>
        </ul>
      </div>
    </div>
  </nav>
</template>

<script>
import {
      
       useRoute } from "vue-router";
import {
      
       computed } from "vue";

export default {
      
      
  setup() {
      
      
    const route = useRoute();
    let route_name = computed(() => route.name);

    return {
      
      
      route_name,
    };
  },
};
</script>

<style scoped></style>

还有别忘了更新路由,即 src/router 目录下的 index.js

import {
    
     createRouter, createWebHistory } from "vue-router";
import PKIndexView from "@/views/pk/PKIndexView";
import RecordIndexView from "@/views/record/RecordIndexView";
import RanklistIndexView from "@/views/ranklist/RanklistIndexView";
import MyBotsIndexView from "@/views/user/mybots/MyBotsIndexView";
import NotFoundView from "@/views/error/NotFoundView";
import UserAccountLoginView from "@/views/user/account/UserAccountLoginView";
import UserAccountRegisterView from "@/views/user/account/UserAccountRegisterView";

const routes = [
  ...
  {
    
    
    path: "/user/account/login/",
    name: "user_account_login",
    component: UserAccountLoginView,
  },
  {
    
    
    path: "/user/account/register/",
    name: "user_account_register",
    component: UserAccountRegisterView,
  },
  ...
];

const router = createRouter({
    
    
  history: createWebHistory(),
  routes,
});

export default router;

2. 实现退出登录功能

在上一节中我们没有实现退出登录的后端 API,我们的 jwt token 完全是存在用户本地的,令牌中会存有过期时间,服务器端能够判断令牌是否过期,因此不用管后端的退出登录。那么如果用户想自己退出登录也很简单,直接将 jwt token 删除即可,无需向后端发送请求,没有令牌后就无法访问后端服务器了。

现在我们是将令牌存在浏览器的内存中,一刷新自动就会重置,之后我们会将其存到 LocalStorage 中,这样即使用户刷新或者关闭浏览器都不会自动退出登录状态。

我们先来实现主动退出登录功能,在 store 目录的 user.js 中添加清空 state 的操作:

import $ from "jquery";

export default {
    
    
  state: {
    
      // 存储的信息
    id: "",
    username: "",
    photo: "",
    jwt_token: "",
    is_login: false,
  },
  getters: {
    
    },
  mutations: {
    
      // 用来修改数据
    ...
    clearState(state) {
    
    
      state.id = "";
      state.username = "";
      state.photo = "";
      state.jwt_token = "";
      state.is_login = false;
    },
  },
  actions: {
    
    
    ...
    logout(context) {
    
    
      context.commit("clearState");
    },
  },
  modules: {
    
    },
};

然后在 NavBar 中调用函数:

<template>
  <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
    <div class="container">
      <router-link class="navbar-brand" :to="{ name: 'home' }">King of Bots</router-link>
      <div class="collapse navbar-collapse" id="navbarText">
        ...
        <ul class="navbar-nav" v-if="$store.state.user.is_login">
          <li class="nav-item dropdown">
            <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
              {
   
   { $store.state.user.username }}
            </a>
            <ul class="dropdown-menu">
              <li>
                <router-link class="dropdown-item" :to="{ name: 'user_mybots_index' }">My Bots</router-link>
              </li>
              <li><hr class="dropdown-divider" /></li>
              <li><a class="dropdown-item" href="#" @click="logout">退出</a></li>
            </ul>
          </li>
        </ul>
        ...
      </div>
    </div>
  </nav>
</template>

<script>
import {
      
       useRoute } from "vue-router";
import {
      
       computed } from "vue";
import {
      
       useStore } from "vuex";
import router from "@/router/index";

export default {
      
      
  setup() {
      
      
    const route = useRoute();
    let route_name = computed(() => route.name);

    const store = useStore();
    const logout = () => {
      
      
      store.dispatch("logout");
      router.push({
      
       name: "home" });  // 跳转至home页面
    };

    return {
      
      
      route_name,
      logout,
    };
  },
};
</script>

<style scoped></style>

3. 设置前端页面授权机制

现在我们的前端页面还没有任何的访问限制,例如在未登录状态下也可以访问任意的页面。当未登录时访问任何页面都应该重定向到登录页面。

页面的授权控制可以在 router 中通过 beforeEach 函数实现,当我们每次在通过 router 进入某个页面之前都会先调用该函数,函数有三个参数:to 表示要跳转到哪个页面,from 表示从哪个页面跳转过去,next 表示页面执行的下一步跳转操作。

我们每次在跳转到某个页面之前需要先判断一下该页面是否需要登录,如果需要登录且当前处于未登录状态则跳转至登录页面。因此我们就需要在每个页面中存储是否需要授权的信息,可以定义在任意名字的变量中,一般可以把额外信息放在 meta 域中。

修改后的 router/index.js 如下:

import {
    
     createRouter, createWebHistory } from "vue-router";
import PKIndexView from "@/views/pk/PKIndexView";
import RecordIndexView from "@/views/record/RecordIndexView";
import RanklistIndexView from "@/views/ranklist/RanklistIndexView";
import MyBotsIndexView from "@/views/user/mybots/MyBotsIndexView";
import NotFoundView from "@/views/error/NotFoundView";
import UserAccountLoginView from "@/views/user/account/UserAccountLoginView";
import UserAccountRegisterView from "@/views/user/account/UserAccountRegisterView";
import store from "@/store/index";

const routes = [
  {
    
    
    path: "/",
    name: "home",
    redirect: "/pk/",  // 如果是根路径则重定向到对战页面
    meta: {
    
    
      requestAuth: true,
    },
  },
  {
    
    
    path: "/pk/",
    name: "pk_index",
    component: PKIndexView,
    meta: {
    
    
      requestAuth: true,
    },
  },
  {
    
    
    path: "/record/",
    name: "record_index",
    component: RecordIndexView,
    meta: {
    
    
      requestAuth: true,
    },
  },
  {
    
    
    path: "/ranklist/",
    name: "ranklist_index",
    component: RanklistIndexView,
    meta: {
    
    
      requestAuth: true,
    },
  },
  {
    
    
    path: "/user/mybots/",
    name: "user_mybots_index",
    component: MyBotsIndexView,
    meta: {
    
    
      requestAuth: true,
    },
  },
  {
    
    
    path: "/user/account/login/",
    name: "user_account_login",
    component: UserAccountLoginView,
    meta: {
    
    
      requestAuth: false,
    },
  },
  {
    
    
    path: "/user/account/register/",
    name: "user_account_register",
    component: UserAccountRegisterView,
    meta: {
    
    
      requestAuth: false,
    },
  },
  {
    
    
    path: "/404/",
    name: "404",
    component: NotFoundView,
    meta: {
    
    
      requestAuth: false,
    },
  },
  {
    
    
    path: "/:catchAll(.*)",
    name: "others",
    redirect: "/404/",  // 如果不是以上路径之一说明不合法,重定向到404页面
  },
];

const router = createRouter({
    
    
  history: createWebHistory(),
  routes,
});

router.beforeEach((to, from, next) => {
    
    
  if (to.meta.requestAuth && !store.state.user.is_login) {
    
    
    alert("Please login!");
    next({
    
     name: "user_account_login" });
  } else {
    
    
    next();  // 如果不需要授权就直接跳转即可
  }
});

export default router;

4. 实现注册页面

注册页面 UserAccountRegisterView 的实现其实就和登录页面基本一致,多加一个确认密码输入框即可。注册时不会修改前端的 state 值,因此也无需将 register 函数实现在 store/user.js 中:

<template>
  <div class="container">
    <div class="card" style="margin-top: 20px;">
      <div class="card-header">
        <h3 style="display: inline-block;">Login</h3>
        <div style="float: right; height: 2.5rem; line-height: 2.5rem;">
          <span>还没有账号?</span>
          <router-link :to="{ name: 'user_account_login' }" style="text-decoration: none;">
            去登录 >
          </router-link>
        </div>
        <div style="clear: both;"></div>
      </div>
      <div class="card-body">
        <div class="row justify-content-md-center">
          <div class="col-md-5">
            <div class="card" style="margin: 6rem auto; box-shadow: 5px 5px 20px #aaa;">
              <div class="card-header text-center">
                <h1>用户注册</h1>
              </div>
              <div class="card-body">
                <div class="row justify-content-md-center">
                  <div class="col col-md-8">
                    <!-- @submit后的prevent是阻止掉submit的默认行为,防止组件间的向上或向下传递 -->
                    <form style="margin: 1rem;" @submit.prevent="register">
                      <div class="mb-3">
                        <label for="username" class="form-label">Username</label>
                        <input v-model="username" type="text" class="form-control" id="username" placeholder="请输入用户名" />
                      </div>
                      <div class="mb-3">
                        <label for="password" class="form-label">Password</label>
                        <input v-model="password" type="password" class="form-control" id="password" placeholder="请输入密码" />
                      </div>
                      <div class="mb-3">
                        <label for="confirmedPassword" class="form-label">Confirmed Password</label>
                        <input v-model="confirmedPassword" type="password" class="form-control" id="confirmedPassword" placeholder="请再次输入密码" />
                      </div>
                      <div style="font-size: 1rem; color: red;">
                        {
   
   { error_message }}
                      </div>
                      <div class="success_message" style="font-size: 1rem; color: green;">
                        {
   
   { success_message }}
                      </div>
                      <button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 10px;">
                        注册
                      </button>
                    </form>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import $ from "jquery";
import {
      
       ref } from "vue";
import {
      
       useStore } from "vuex";
import router from "@/router/index";

export default {
      
      
  setup() {
      
      
    const store = useStore();
    let username = ref("");
    let password = ref("");
    let confirmedPassword = ref("");
    let error_message = ref("");
    let success_message = ref("");

    const register = () => {
      
      
      error_message.value = "";
      $.ajax({
      
      
        url: "http://localhost:3000/user/account/register/",
        type: "POST",
        data: {
      
      
          username: username.value,
          password: password.value,
          confirmedPassword: confirmedPassword.value,
        },
        success(resp) {
      
      
          console.log(resp);
          if (resp.result === "success") {
      
      
            success_message.value = "Success! Go to home page after 3 seconds...";
            $(".success_message").fadeIn();  // 渐变出现注册成功的提示
            setTimeout(() => {
      
        // 2秒后将注册成功的提示渐变消去
              $(".success_message").fadeOut();
            }, 2000);
            setTimeout(() => {
      
        // 3秒后自动登录并跳转至首页,此处计时与上面同时进行
              success_message.value = "";
              store.dispatch("login", {
      
      
                username: username.value,
                password: password.value,
                success() {
      
      
                  store.dispatch("getInfo", {
      
      
                    success() {
      
      
                      router.push({
      
       name: "home" });  // 跳转至home页面
                    },
                  });
                },
              });
            }, 3000);
          } else {
      
      
            error_message.value = resp.result;
          }
        },
        error(resp) {
      
      
          console.log(resp);
        },
      });
    };

    return {
      
      
      username,
      password,
      confirmedPassword,
      error_message,
      success_message,
      register,
    };
  },
};
</script>

<style scoped></style>

5. 登陆状态的持久化

我们可以将登录后获得的 jwt token 存放在浏览器的一小块硬盘空间 LocalStorage 中,首先在 store/user.js 中修改:

import $ from "jquery";

export default {
    
    
  ...
  actions: {
    
    
    login(context, data) {
    
    
      $.ajax({
    
    
        url: "http://localhost:3000/user/account/login/",
        type: "POST",
        data: {
    
    
          username: data.username,
          password: data.password,
        },
        success(resp) {
    
    
          if (resp.result === "success") {
    
    
            localStorage.setItem("jwt_token", resp.jwt_token);  // 将令牌存到LocalStorage中实现登录状态持久化
            context.commit("updateJwtToken", resp.jwt_token);
            data.success(resp);  // 成功后的回调函数
          }
        },
        error(resp) {
    
    
          data.error(resp);  // 失败后的回调函数
        },
      });
    },
    getInfo(context, data) {
    
    
      $.ajax({
    
    
        url: "http://localhost:3000/user/account/info/",
        type: "GET",
        async: false,
        ...
      });
    },
    logout(context) {
    
    
      localStorage.removeItem("jwt_token");
      context.commit("clearState");
    },
  },
  modules: {
    
    },
};

我们在 login 函数中将登录成功后收到的 jwt token 存在 LocalStorage 中,在 logout 函数中清除 LocalStorage 中的 jwt token。需要特别注意的是 getInfo 函数中添加了 async: false,这是表示将该 Ajax 请求变为同步的,具体作用在之后讲解。

现在当我们要跳转到某个链接前可以先取出 LocalStorage 中的 jwt token,判断是否存在并且未过期,如果有效则在跳转之前直接调用 store/user.js 中的 updateJwtToken 更新浏览器内存中的 jwt token,并通过 getInfo 函数更新用户信息。还是在 router/index.js 中的 router.beforeEach 函数中实现:

import {
    
     createRouter, createWebHistory } from "vue-router";
import PKIndexView from "@/views/pk/PKIndexView";
import RecordIndexView from "@/views/record/RecordIndexView";
import RanklistIndexView from "@/views/ranklist/RanklistIndexView";
import MyBotsIndexView from "@/views/user/mybots/MyBotsIndexView";
import NotFoundView from "@/views/error/NotFoundView";
import UserAccountLoginView from "@/views/user/account/UserAccountLoginView";
import UserAccountRegisterView from "@/views/user/account/UserAccountRegisterView";
import store from "@/store/index";

const routes = [
  ...
];

const router = createRouter({
    
    
  history: createWebHistory(),
  routes,
});

router.beforeEach((to, from, next) => {
    
    
  const jwt_token = localStorage.getItem("jwt_token");
  let jwt_token_valid = false;  // jwt_token是否存在且有效

  if (jwt_token) {
    
      // jwt_token存在
    store.commit("updateJwtToken", jwt_token);
    store.dispatch("getInfo", {
    
    
      success() {
    
      // jwt_token有效
        jwt_token_valid = true;
      },
      error() {
    
    
        alert("Invalid token! Please login!");
        store.dispatch("logout");  // 清除浏览器内存和LocalStorage中的jwt_token
        next({
    
     name: "user_account_login" });
      },
    });
  }

  if (to.meta.requestAuth && !store.state.user.is_login && !jwt_token_valid) {
    
    
    alert("Please login!");
    next({
    
     name: "user_account_login" });
  } else {
    
    
    next();  // 如果不需要授权就直接跳转即可
  }
});

export default router;

注意,在第一个 if 语句中调用了 storegetInfo 函数,由于 Ajax 的回调函数默认是异步的,因此第二个 if 语句会在 success 回调函数执行前就被执行了,这会导致 jwt_token_valid 还没被更新,从而被判断成未登录状态,直接跳转至登录页面,所以我们在前面将 getInfo 函数中的 Ajax 设置为同步,保证了以上代码的正确执行逻辑。

猜你喜欢

转载自blog.csdn.net/m0_51755720/article/details/134456655