Vue 3 中如何JWT、Vuex、Axios和Vue Router进行身份验证实战

2024年 3月 8日 140.2k 0

在本教程中,我们将在Vue 3中使用JWT、Vuex、Axios、Vue Router和VeeValidate构建一个身份验证和授权的示例。

内容包括:

  • 用户注册和用户登录的JWT身份验证流程
  • 使用Vuex 4和Vue Router 4进行Vue 3身份验证的项目结构
  • 定义Vuex认证模块
  • 使用Vuex Store创建Vue 3身份验证组件
  • 使用VeeValidate 4实现响应式表单验证
  • 访问受保护资源的Vue 3组件
  • 向Vue 3 App添加动态导航栏

出发!

使用JWT的Vue 3身份验证实战

我们将构建一个Vue 3应用程序,其中包含:

  • 登录/注销、注册页面。
  • 表单数据在发送到后端之前由前端进行验证。
  • 根据用户的角色(管理员、版主、用户)自动更改导航栏项目。

截图

– 注册页面:

图片图片

– 表单验证如下所示:

图片图片

– 登录页面和个人资料页面:

图片图片

– 管理员帐户的导航栏:

图片图片

演示

下面是完整的Vue JWT身份验证App演示(有表单验证、检查注册用户名/电子邮件重复项,并使用管理员、版主、用户3个角色测试授权)。后端REST API使用Spring Boot。

https://youtube.com/watch?v=pPSRVu-Ysjw%3Frel%3D0

上面地视频使用的是Vue 2和VeeValidate 2,逻辑和UI与本教程相同。

用户注册和用户登录流程

JWT身份验证将调用2个接口服务:

  • 用于用户注册的POST api/auth/signup
  • 用于用户登录的POST api/auth/signin

你可以看看下面的流程,对Vue客户端如何发出或接收请求和响应有一个大致的了解。

图片图片

Vue客户端在向受保护的资源发送请求之前,必须将JWT添加到HTTP授权标头中。

Vue App组件图

现在请看下图:

图片图片

我们知道:

– App组件是一个具有Router的容器。它从Vuex store/auth获取应用状态。然后导航栏可以根据状态来显示。App组件还会将状态传递给子组件。

– Login和Register组件具有用于提交数据的表单(支持vee-validate)。我们调用Vuex store dispatch()函数来执行登录/注册操作。

– Vuex操作调用auth.service方法,auth.service方法将使用axios发出HTTP请求。这些方法还可以存储或从浏览器本地存储中获取JWT。

– home组件对所有访客都是公开的。

– Profile组件从父组件获取user数据并显示用户信息。

– BoardUser、BoardModerator、BoardAdmin组件将由Vuex状态user.roles显示。这些组件使用user.service获取来自API的受保护的资源。

– user.service使用auth-header()辅助函数将JWT添加到HTTP授权标头。auth-header()从本地存储返回一个对象,这个对象包含当前登录用户的JWT。

技术

我们将用到以下模块:

  • vue 3
  • vue-router 4
  • Vuex 4
  • axios:0.21.1
  • VEE-validate 4
  • bootstrap 4
  • vue-fontawesome 3

项目结构

Vue 3身份验证和授权项目的文件夹和文件结构如下:

图片图片

设置Vue 3项目

在Project文件夹中打开cmd,运行命令:

vue create vue-3-authentication-jwt

你会看到一些选项,选择Default ([Vue 3] babel, eslint)。

项目准备就绪后,运行以下命令安装必要的模块:

npm install vue-router@4
npm install vuex@4
npm install vee-validate@4 yup
npm install axios
npm install bootstrap@4 jquery popper.js
npm install @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/vue-fontawesome@prerelease

安装完成后,可以检查package.json文件中的依赖项。

"dependencies": {
  "@fortawesome/fontawesome-svg-core": "^1.2.35",
  "@fortawesome/free-solid-svg-icons": "^5.15.3",
  "@fortawesome/vue-fontawesome": "^3.0.0-3",
  "axios": "^0.21.1",
  "bootstrap": "^4.6.0",
  "core-js": "^3.6.5",
  "jquery": "^3.6.0",
  "popper.js": "^1.16.1",
  "vee-validate": "^4.3.5",
  "vue": "^3.0.0",
  "vue-router": "^4.0.6",
  "vuex": "^4.0.0",
  "yup": "^0.32.9"
},

在src文件夹中使用以下代码创建plugins/font-awesome.js文件:

import { library } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import {
  faHome,
  faUser,
  faUserPlus,
  faSignInAlt,
  faSignOutAlt,
} from "@fortawesome/free-solid-svg-icons";
 
library.add(faHome, faUser, faUserPlus, faSignInAlt, faSignOutAlt);
 
export { FontAwesomeIcon };

打开src/main.js,如下修改里面的代码:

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import "bootstrap";
import "bootstrap/dist/css/bootstrap.min.css";
import { FontAwesomeIcon } from './plugins/font-awesome'
 
createApp(App)
  .use(router)
  .use(store)
  .component("font-awesome-icon", FontAwesomeIcon)
  .mount("#app");

可以看到我们导入并应用了:

– Vuex的store(稍后在src/store实现)

– Vue Router的router(稍后在src/router.js实现)

– CSS的bootstrap

– 用于图标的vue-fontawesome(稍后在nav中使用)

创建服务

在src/services文件夹中创建两个服务:

图片图片

身份验证服务

该服务在axios的帮助下为HTTP请求和响应提供了三种重要方法:

  • login(): POST {username, password}并将JWT保存到Local Storage
  • logout():删除来自Local Storage中的JWT
  • register():POST { username, email, password}
import axios from 'axios';
 
const API_URL = 'http://localhost:8080/api/auth/';
 
class AuthService {
  login(user) {
    return axios
      .post(API_URL + 'signin', {
        username: user.username,
        password: user.password
      })
      .then(response => {
        if (response.data.accessToken) {
          localStorage.setItem('user', JSON.stringify(response.data));
        }
 
        return response.data;
      });
  }
 
  logout() {
    localStorage.removeItem('user');
  }
 
  register(user) {
    return axios.post(API_URL + 'signup', {
      username: user.username,
      email: user.email,
      password: user.password
    });
  }
}
 
export default new AuthService();

数据服务

还有从服务器检索数据的方法。如果要访问受保护的资源,那么HTTP请求需要Authorization标头。

在auth-header.js中创建辅助函数authHeader():

export default function authHeader() {
  let user = JSON.parse(localStorage.getItem('user'));
 
  if (user && user.accessToken) {
    return { Authorization: 'Bearer ' + user.accessToken };
  } else {
    return {};
  }
}

它检查user项的Local Storage。

如果存在使用accessToken(JWT)登录的user,则返回HTTP Authorization标头。否则返回空对象。

注意:对于Node.js Express后端,请使用x-access-token标头,如下所示:

export default function authHeader() {
  let user = JSON.parse(localStorage.getItem('user'));
 
  if (user && user.accessToken) {
    // for Node.js Express back-end
    return { 'x-access-token': user.accessToken };
  } else {
    return {};
  }
}

接着在user.service.js中定义用于访问数据的服务:

import axios from 'axios';
import authHeader from './auth-header';
 
const API_URL = 'http://localhost:8080/api/test/';
 
class UserService {
  getPublicContent() {
    return axios.get(API_URL + 'all');
  }
 
  getUserBoard() {
    return axios.get(API_URL + 'user', { headers: authHeader() });
  }
 
  getModeratorBoard() {
    return axios.get(API_URL + 'mod', { headers: authHeader() });
  }
 
  getAdminBoard() {
    return axios.get(API_URL + 'admin', { headers: authHeader() });
  }
}
 
export default new UserService();

可以看到,在请求授权的资源时,我们在authHeader()函数的帮助下添加了HTTP标头。

定义Vuex认证模块

我们将用于身份验证的Vuex模块放在src/store文件夹。

图片图片

现在打开index.js文件,将auth.module导入到主Vuex Store。

import { createStore } from "vuex";
import { auth } from "./auth.module";
 
const store = createStore({
  modules: {
    auth,
  },
});
 
export default store;

然后开始定义Vuex身份验证模块,其中包含:

  • state: { status, user }
  • actions: { login, logout, register }
  • mutations: { loginSuccess, loginFailure, logout, registerSuccess, registerFailure }

我们使用上面定义的AuthService来发出身份验证请求。

auth.module.js

import AuthService from '../services/auth.service';
 
const user = JSON.parse(localStorage.getItem('user'));
const initialState = user
  ? { status: { loggedIn: true }, user }
  : { status: { loggedIn: false }, user: null };
 
export const auth = {
  namespaced: true,
  state: initialState,
  actions: {
    login({ commit }, user) {
      return AuthService.login(user).then(
        user => {
          commit('loginSuccess', user);
          return Promise.resolve(user);
        },
        error => {
          commit('loginFailure');
          return Promise.reject(error);
        }
      );
    },
    logout({ commit }) {
      AuthService.logout();
      commit('logout');
    },
    register({ commit }, user) {
      return AuthService.register(user).then(
        response => {
          commit('registerSuccess');
          return Promise.resolve(response.data);
        },
        error => {
          commit('registerFailure');
          return Promise.reject(error);
        }
      );
    }
  },
  mutations: {
    loginSuccess(state, user) {
      state.status.loggedIn = true;
      state.user = user;
    },
    loginFailure(state) {
      state.status.loggedIn = false;
      state.user = null;
    },
    logout(state) {
      state.status.loggedIn = false;
      state.user = null;
    },
    registerSuccess(state) {
      state.status.loggedIn = false;
    },
    registerFailure(state) {
      state.status.loggedIn = false;
    }
  }
};

创建Vue 3认证组件

继续身份验证组件,这些组件应该与Vuex Store一起使用,而不是直接使用axios或AuthService:

– 使用this.$store.state.auth获取status

– 通过调度操作this.$store.dispatch()发出请求

图片图片

Vue 3登录页面

在src/components文件夹使用以下代码创建Login.vue文件:


  
    
      
      
        
          Username
          
          
        
        
          Password
          
          
        
 
        
          
            
            Login
          
        
 
        
          
            {{ message }}
          
        
      
    
  

 

import { Form, Field, ErrorMessage } from "vee-validate";
import * as yup from "yup";
 
export default {
  name: "Login",
  components: {
    Form,
    Field,
    ErrorMessage,
  },
  data() {
    const schema = yup.object().shape({
      username: yup.string().required("Username is required!"),
      password: yup.string().required("Password is required!"),
    });
 
    return {
      loading: false,
      message: "",
      schema,
    };
  },
  computed: {
    loggedIn() {
      return this.$store.state.auth.status.loggedIn;
    },
  },
  created() {
    if (this.loggedIn) {
      this.$router.push("/profile");
    }
  },
  methods: {
    handleLogin(user) {
      this.loading = true;
 
      this.$store.dispatch("auth/login", user).then(
        () => {
          this.$router.push("/profile");
        },
        (error) => {
          this.loading = false;
          this.message =
            (error.response &&
              error.response.data &&
              error.response.data.message) ||
            error.message ||
            error.toString();
        }
      );
    },
  },
};

 

...

此页面有一个包含2个Field,即username和password的Form。使用VeeValidate 4.x来验证输入,如果存在无效字段,则显示错误消息。

我们使用Vuex Store—this.$store.state.auth.status.loggedIn检查用户登录状态。如果状态为true,则使用Vue Router将用户定向到Profile页面:

created() {
  if (this.loggedIn) {
    this.$router.push('/profile');
  }
},

在handleLogin()函数中,我们将'auth/login' Action调度到Vuex Store。如果登录成功,则转到Profile页面,否则显示错误消息。

Vue 3注册页面

注册页面类似于登录页面。

不一样的是,表单验证需要提供更多详细信息:

  • username:必填,最小长度:3,最大长度:20
  • email: 必填, email, 最大长度:50
  • password:必填,最小长度:6,最大长度:40

而表单提交,则调度'auth/register' Vuex Action。

components/Register.vue


  
    
      
      
        
          
            Username
            
            
          
          
            Email
            
            
          
          
            Password
            
            
          
 
          
            
              
              Sign Up
            
          
        
      
 
      
        {{ message }}
      
    
  

 

import { Form, Field, ErrorMessage } from "vee-validate";
import * as yup from "yup";
 
export default {
  name: "Register",
  components: {
    Form,
    Field,
    ErrorMessage,
  },
  data() {
    const schema = yup.object().shape({
      username: yup
        .string()
        .required("Username is required!")
        .min(3, "Must be at least 3 characters!")
        .max(20, "Must be maximum 20 characters!"),
      email: yup
        .string()
        .required("Email is required!")
        .email("Email is invalid!")
        .max(50, "Must be maximum 50 characters!"),
      password: yup
        .string()
        .required("Password is required!")
        .min(6, "Must be at least 6 characters!")
        .max(40, "Must be maximum 40 characters!"),
    });
 
    return {
      successful: false,
      loading: false,
      message: "",
      schema,
    };
  },
  computed: {
    loggedIn() {
      return this.$store.state.auth.status.loggedIn;
    },
  },
  mounted() {
    if (this.loggedIn) {
      this.$router.push("/profile");
    }
  },
  methods: {
    handleRegister(user) {
      this.message = "";
      this.successful = false;
      this.loading = true;
 
      this.$store.dispatch("auth/register", user).then(
        (data) => {
          this.message = data.message;
          this.successful = true;
          this.loading = false;
        },
        (error) => {
          this.message =
            (error.response &&
              error.response.data &&
              error.response.data.message) ||
            error.message ||
            error.toString();
          this.successful = false;
          this.loading = false;
        }
      );
    },
  },
};

 

...

Profile页面

此页面从Vuex Store获取当前用户并显示信息。如果用户未登录,则定向到登录页面。

components/Profile.vue


  
    
      

{{currentUser.username}} Profile

Token: {{currentUser.accessToken.substring(0, 20)}} ... {{currentUser.accessToken.substr(currentUser.accessToken.length - 20)}}

Id: {{currentUser.id}}

Email: {{currentUser.email}}

Authorities:
  • {{role}}
export default { name: 'Profile', computed: { currentUser() { return this.$store.state.auth.user; } }, mounted() { if (!this.currentUser) { this.$router.push('/login'); } } };

创建用于访问资源的Vue组件

这些组件将使用UserService来请求数据。

图片图片

主页

这是一个公共页面。

components/Home.vue


  
    
      

{{ content }}

import UserService from "../services/user.service"; export default { name: "Home", data() { return { content: "", }; }, mounted() { UserService.getPublicContent().then( (response) => { this.content = response.data; }, (error) => { this.content = (error.response && error.response.data && error.response.data.message) || error.message || error.toString(); } ); }, };

基于角色的页面

我们有3个页面用于访问受保护的数据:

  • BoardUser页面调用UserService.getUserBoard()
  • BoardModerator页面调用UserService.getModeratorBoard()
  • BoardAdmin页面调用UserService.getAdminBoard()

请看下面的示例。

components/BoardUser.vue


  
    
      

{{ content }}

import UserService from "../services/user.service"; export default { name: "User", data() { return { content: "", }; }, mounted() { UserService.getUserBoard().then( (response) => { this.content = response.data; }, (error) => { this.content = (error.response && error.response.data && error.response.data.message) || error.message || error.toString(); } ); }, };

定义Vue Router的路由

现在我们为Vue 3应用程序定义所有路由。

src/router.js

import { createWebHistory, createRouter } from "vue-router";
import Home from "./components/Home.vue";
import Login from "./components/Login.vue";
import Register from "./components/Register.vue";
// lazy-loaded
const Profile = () => import("./components/Profile.vue")
const BoardAdmin = () => import("./components/BoardAdmin.vue")
const BoardModerator = () => import("./components/BoardModerator.vue")
const BoardUser = () => import("./components/BoardUser.vue")
 
const routes = [
  {
    path: "/",
    name: "home",
    component: Home,
  },
  {
    path: "/home",
    component: Home,
  },
  {
    path: "/login",
    component: Login,
  },
  {
    path: "/register",
    component: Register,
  },
  {
    path: "/profile",
    name: "profile",
    // lazy-loaded
    component: Profile,
  },
  {
    path: "/admin",
    name: "admin",
    // lazy-loaded
    component: BoardAdmin,
  },
  {
    path: "/mod",
    name: "moderator",
    // lazy-loaded
    component: BoardModerator,
  },
  {
    path: "/user",
    name: "user",
    // lazy-loaded
    component: BoardUser,
  },
];
 
const router = createRouter({
  history: createWebHistory(),
  routes,
});
 
export default router;

向Vue app添加导航栏

这是应用程序中包含导航栏的根容器。我们要添加router-view。

src/App.vue


  
    
      bezKoder
      
        
  • Home
  • Admin Board
  • Moderator Board
  • User
  • Sign Up
  • Login
  • {{ currentUser.username }}
  • LogOut
  • export default { computed: { currentUser() { return this.$store.state.auth.user; }, showAdminBoard() { if (this.currentUser && this.currentUser['roles']) { return this.currentUser['roles'].includes('ROLE_ADMIN'); } return false; }, showModeratorBoard() { if (this.currentUser && this.currentUser['roles']) { return this.currentUser['roles'].includes('ROLE_MODERATOR'); } return false; } }, methods: { logOut() { this.$store.dispatch('auth/logout'); this.$router.push('/login'); } } };

    使用font-awesome-icon可以使得导航栏看起来更专业。

    而且导航栏还可以根据从Vuex Store state检索到的当前用户的roles而动态变化。

    处理未经授权的访问

    如果你想在每次触发导航操作时检查授权状态,只需在src/router.js中添加router.beforeEach(),如下所示:

    router.beforeEach((to, from, next) => {
      const publicPages = ['/login', '/register', '/home'];
      const authRequired = !publicPages.includes(to.path);
      const loggedIn = localStorage.getItem('user');
     
      // trying to access a restricted page + not logged in
      // redirect to login page
      if (authRequired && !loggedIn) {
        next('/login');
      } else {
        next();
      }
    });

    为Vue App配置端口

    由于大多数HTTP Server使用CORS配置,接受仅限于某些站点或端口的资源共享,因此我们还需要为App配置端口。

    在项目根文件夹中,创建包含以下内容的vue.config.js文件:

    module.exports = {
      devServer: {
        port: 8081
      }
    }

    我们将app设置为运行在端口8081上。

    结论

    今天,我们学习了很多有趣的内容,学习了如何使用Axios、Vuex和Vue Router构建支持JWT身份验证和授权的Vue应用程序。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论