在本教程中,我们将在Vue 3中使用JWT、Vuex、Axios、Vue Router和VeeValidate构建一个身份验证和授权的示例。
内容包括:
出发!
我们将构建一个Vue 3应用程序,其中包含:
– 注册页面:
图片
– 表单验证如下所示:
图片
– 登录页面和个人资料页面:
图片
– 管理员帐户的导航栏:
图片
下面是完整的Vue JWT身份验证App演示(有表单验证、检查注册用户名/电子邮件重复项,并使用管理员、版主、用户3个角色测试授权)。后端REST API使用Spring Boot。
https://www.youtube.com/embed/pPSRVu-Ysjw?rel=0
上面地视频使用的是Vue 2和VeeValidate 2,逻辑和UI与本教程相同。
JWT身份验证将调用2个接口服务:
你可以看看下面的流程,对Vue客户端如何发出或接收请求和响应有一个大致的了解。
图片
Vue客户端在向受保护的资源发送请求之前,必须将JWT添加到HTTP授权标头中。
现在请看下图:
图片
我们知道:
– 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身份验证和授权项目的文件夹和文件结构如下:
图片
在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请求和响应提供了三种重要方法:
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模块放在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身份验证模块,其中包含:
我们使用上面定义的AuthService来发出身份验证请求。
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;
}
}
};
继续身份验证组件,这些组件应该与Vuex Store一起使用,而不是直接使用axios或AuthService:
– 使用this.$store.state.auth获取status
– 通过调度操作this.$store.dispatch()发出请求
图片
在src/components文件夹使用以下代码创建Login.vue文件:
此页面有一个包含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页面,否则显示错误消息。
注册页面类似于登录页面。
不一样的是,表单验证需要提供更多详细信息:
而表单提交,则调度'auth/register' Vuex Action。
{{ message }}
此页面从Vuex Store获取当前用户并显示信息。如果用户未登录,则定向到登录页面。
{{currentUser.username}} Profile
Token:
{{currentUser.accessToken.substring(0, 20)}} ... {{currentUser.accessToken.substr(currentUser.accessToken.length - 20)}}
Id:
{{currentUser.id}}
Email:
{{currentUser.email}}
Authorities:
- {{role}}
这些组件将使用UserService来请求数据。
图片
这是一个公共页面。
{{ content }}
我们有3个页面用于访问受保护的数据:
请看下面的示例。
{{ content }}
现在我们为Vue 3应用程序定义所有路由。
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;
这是应用程序中包含导航栏的根容器。我们要添加router-view。
使用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();
}
});
由于大多数HTTP Server使用CORS配置,接受仅限于某些站点或端口的资源共享,因此我们还需要为App配置端口。
在项目根文件夹中,创建包含以下内容的vue.config.js文件:
module.exports = {
devServer: {
port: 8081
}
}
我们将app设置为运行在端口8081上。
今天,我们学习了很多有趣的内容,学习了如何使用Axios、Vuex和Vue Router构建支持JWT身份验证和授权的Vue应用程序。