Vue Router的导航守卫与后端权限校验:实现客户端/服务端权限验证的同步

Vue Router导航守卫与后端权限校验:客户端/服务端权限验证的同步

大家好,今天我们来探讨 Vue Router 导航守卫与后端权限校验的结合使用,以及如何实现客户端和服务端权限验证的同步。这是一个在实际项目中非常常见且重要的需求,它关系到应用的安全性、用户体验以及整体架构的健壮性。

权限验证的必要性

在任何具有用户身份的应用中,权限验证都是至关重要的。没有权限验证,任何用户都可能访问或修改他们不应该访问或修改的数据,导致安全漏洞和数据泄露。

  • 安全性: 保护敏感数据和功能,防止未授权访问。
  • 数据完整性: 确保只有授权用户才能修改数据,维护数据一致性。
  • 合规性: 满足法律法规对用户数据访问权限的要求。
  • 用户体验: 根据用户角色提供个性化的体验,避免用户看到他们无法使用的功能。

客户端权限验证 vs. 服务端权限验证

权限验证通常需要在客户端和服务端同时进行。

  • 客户端权限验证: 提供快速响应和良好的用户体验。在用户尝试访问某个路由之前,快速判断用户是否具有访问权限,如果权限不足,则阻止路由跳转,并给出友好的提示。
  • 服务端权限验证: 提供最终的保障。即使客户端绕过验证,服务端也必须进行权限验证,确保所有请求都经过授权才能执行。

理想情况下,客户端和服务端的权限验证应该保持同步,避免出现客户端认为有权限而服务端拒绝的情况,或者客户端认为无权限而服务端允许的情况。

Vue Router 导航守卫

Vue Router 提供了一套强大的导航守卫机制,允许我们在路由跳转前后执行一些逻辑,例如权限验证。

常见的导航守卫有:

  • 全局前置守卫:router.beforeEach((to, from, next) => { ... }) 在每次路由跳转之前执行。
  • 全局解析守卫:router.beforeResolve((to, from, next) => { ... }) 在所有组件内守卫和异步路由组件被解析之后执行。
  • 全局后置钩子:router.afterEach((to, from) => { ... }) 在每次路由跳转之后执行。
  • 路由独享的守卫:beforeEnter: (to, from, next) => { ... } 只在进入特定路由时执行。
  • 组件内的守卫:beforeRouteEnter(to, from, next) { ... }beforeRouteUpdate(to, from, next) { ... }beforeRouteLeave(to, from, next) { ... } 在组件内部定义,可以访问组件实例。

我们通常使用 router.beforeEach 全局前置守卫来进行权限验证。

import router from './router'
import store from './store' // Vuex store,用于存储用户信息和权限

router.beforeEach((to, from, next) => {
  // 1. 判断用户是否已登录
  const isLoggedIn = store.getters.isLoggedIn; // 从 Vuex 获取登录状态

  // 2. 如果需要登录才能访问的路由,但用户未登录,则跳转到登录页面
  if (to.meta.requiresAuth && !isLoggedIn) {
    next({
      path: '/login',
      query: { redirect: to.fullPath } // 登录成功后重定向到目标页面
    });
  } else {
    // 3. 如果用户已登录,但访问登录页面,则跳转到首页
    if (to.path === '/login' && isLoggedIn) {
      next('/');
    } else {
      // 4. 权限验证
      if (to.meta.permissions) {
        const userPermissions = store.getters.permissions; // 从 Vuex 获取用户权限
        const hasPermission = to.meta.permissions.every(permission => userPermissions.includes(permission));

        if (hasPermission) {
          next(); // 允许访问
        } else {
          // 权限不足,可以跳转到 403 页面或显示提示信息
          next('/403'); // 或者 next(false);
        }
      } else {
        next(); // 允许访问,无需权限验证
      }
    }
  }
});

代码解释:

  1. 判断用户是否已登录: 从 Vuex 中获取用户登录状态。store.getters.isLoggedIn 是一个 Vuex 的 getter,用于返回当前用户的登录状态。
  2. 需要登录才能访问的路由: 检查 to.meta.requiresAuth 是否为 true。如果为 true,则表示该路由需要用户登录才能访问。如果用户未登录,则跳转到登录页面,并将当前路由的完整路径作为参数传递给登录页面,以便登录成功后重定向到目标页面。
  3. 用户已登录访问登录页面: 如果用户已登录,但尝试访问登录页面,则跳转到首页。这是为了避免用户重复登录。
  4. 权限验证: 检查 to.meta.permissions 是否存在。如果存在,则表示该路由需要特定的权限才能访问。从 Vuex 中获取用户权限,并使用 every 方法判断用户是否拥有所有需要的权限。如果拥有所有需要的权限,则允许访问;否则,跳转到 403 页面或显示提示信息。

路由配置:

// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Login from '../views/Login.vue'
import Admin from '../views/Admin.vue'
import NotFound from '../views/NotFound.vue'
import Forbidden from '../views/Forbidden.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/login',
    name: 'Login',
    component: Login
  },
  {
    path: '/admin',
    name: 'Admin',
    component: Admin,
    meta: { requiresAuth: true, permissions: ['admin'] } // 需要登录且具有 'admin' 权限
  },
  {
    path: '/403',
    name: 'Forbidden',
    component: Forbidden
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: NotFound
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

Vuex Store (简化示例):

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    isLoggedIn: false,
    user: null,
    permissions: []
  },
  mutations: {
    SET_USER(state, user) {
      state.user = user;
      state.isLoggedIn = !!user;
    },
    SET_PERMISSIONS(state, permissions) {
      state.permissions = permissions;
    }
  },
  actions: {
    login({ commit }, credentials) {
      // 模拟登录,实际应该调用 API
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          if (credentials.username === 'admin' && credentials.password === 'password') {
            const user = { id: 1, username: 'admin', role: 'admin' };
            const permissions = ['admin', 'edit', 'view']; // 模拟权限
            commit('SET_USER', user);
            commit('SET_PERMISSIONS', permissions);
            resolve();
          } else {
            reject(new Error('Invalid credentials'));
          }
        }, 500);
      });
    },
    logout({ commit }) {
      commit('SET_USER', null);
      commit('SET_PERMISSIONS', []);
    }
  },
  getters: {
    isLoggedIn: state => state.isLoggedIn,
    user: state => state.user,
    permissions: state => state.permissions
  }
})

后端权限校验

客户端的权限验证只是第一道防线。为了确保安全性,服务端也必须进行权限验证。

服务端权限验证通常包含以下步骤:

  1. 身份验证 (Authentication): 验证用户的身份,例如通过用户名和密码、Token 等。
  2. 授权 (Authorization): 确定用户是否有权访问特定的资源或执行特定的操作。

服务端可以使用各种技术来实现权限验证,例如:

  • 基于角色的访问控制 (RBAC): 将权限分配给角色,然后将角色分配给用户。
  • 基于属性的访问控制 (ABAC): 根据用户的属性、资源的属性和环境的属性来决定是否允许访问。
  • 访问控制列表 (ACL): 为每个资源维护一个列表,记录哪些用户或角色可以访问该资源。

示例 (Node.js + Express + JWT):

// 引入必要的模块
const express = require('express');
const jwt = require('jsonwebtoken');

const app = express();
app.use(express.json()); // 解析 JSON 格式的请求体

const secretKey = 'your-secret-key'; // 替换成你的密钥 (生产环境必须使用安全的密钥)

// 模拟用户数据
const users = [
  { id: 1, username: 'admin', password: 'password', role: 'admin' },
  { id: 2, username: 'user', password: 'password', role: 'user' }
];

// 模拟需要权限验证的资源
const articles = [
  { id: 1, title: 'Article 1', content: 'This is article 1.' },
  { id: 2, title: 'Article 2', content: 'This is article 2.' }
];

// 登录接口
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const user = users.find(u => u.username === username && u.password === password);

  if (!user) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  // 生成 JWT Token
  const token = jwt.sign({ id: user.id, username: user.username, role: user.role }, secretKey, { expiresIn: '1h' });
  res.json({ token });
});

// 权限验证中间件
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer <token>

  if (!token) {
    return res.status(401).json({ message: 'No token provided' });
  }

  jwt.verify(token, secretKey, (err, user) => {
    if (err) {
      return res.status(403).json({ message: 'Invalid token' });
    }

    req.user = user; // 将用户信息添加到请求对象中
    next();
  });
}

// 权限验证中间件
function authorize(roles) {
  return (req, res, next) => {
    const user = req.user;

    if (!roles.includes(user.role)) {
      return res.status(403).json({ message: 'Unauthorized' });
    }

    next();
  };
}

// 获取所有文章 (需要登录)
app.get('/articles', authenticateToken, (req, res) => {
  res.json(articles);
});

// 创建文章 (需要 admin 权限)
app.post('/articles', authenticateToken, authorize(['admin']), (req, res) => {
  // 只有 admin 角色才能创建文章
  const newArticle = { id: articles.length + 1, ...req.body };
  articles.push(newArticle);
  res.status(201).json(newArticle);
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server listening on port ${port}`);
});

代码解释:

  1. 登录接口: /login 接口接收用户名和密码,验证用户信息,如果验证成功,则生成 JWT Token 并返回给客户端。
  2. JWT Token: JWT (JSON Web Token) 是一种常用的身份验证方式。Token 包含了用户的身份信息和权限信息,客户端在后续的请求中携带 Token,服务端通过验证 Token 来确认用户的身份和权限。
  3. authenticateToken 中间件: 用于验证 JWT Token。该中间件从请求头中获取 Token,验证 Token 的有效性,如果 Token 有效,则将用户信息添加到请求对象中。
  4. authorize 中间件: 用于权限验证。该中间件接收一个角色列表,判断当前用户是否拥有这些角色中的任何一个。如果用户没有权限,则返回 403 状态码。
  5. /articles 接口: 需要登录才能访问。
  6. /articles POST 接口: 需要 admin 权限才能访问。

客户端/服务端权限验证的同步

为了实现客户端和服务端权限验证的同步,我们可以采取以下策略:

  1. 在登录时获取用户权限: 当用户登录成功后,从服务端获取用户的权限信息,并将权限信息存储到客户端 (例如 Vuex)。
  2. 在客户端进行权限验证: 使用 Vue Router 导航守卫,根据客户端存储的权限信息进行权限验证。
  3. 在服务端进行权限验证: 在每个需要权限验证的 API 接口中,验证用户的权限。
  4. 保持权限信息同步: 如果用户的权限发生变化,需要及时更新客户端存储的权限信息。可以通过以下方式实现:
    • 手动刷新: 当用户手动刷新页面时,重新从服务端获取权限信息。
    • WebSocket: 使用 WebSocket 技术,当服务端的权限信息发生变化时,主动推送给客户端。
    • 轮询: 客户端定期向服务端发送请求,检查权限信息是否发生变化。

以下是如何在前端获取权限并在路由守卫中使用的示例:

// store/index.js (修改)
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios' // 引入 axios

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    isLoggedIn: false,
    user: null,
    permissions: []
  },
  mutations: {
    SET_USER(state, user) {
      state.user = user;
      state.isLoggedIn = !!user;
    },
    SET_PERMISSIONS(state, permissions) {
      state.permissions = permissions;
    }
  },
  actions: {
    async login({ commit }, credentials) {
      try {
        const response = await axios.post('/api/login', credentials); // 替换成你的登录 API
        const { token } = response.data;

        // 将 token 存储到 localStorage 或 cookie 中
        localStorage.setItem('token', token);

        // 获取用户信息和权限
        const userResponse = await axios.get('/api/user', { // 替换成你的获取用户信息 API
          headers: { Authorization: `Bearer ${token}` }
        });
        const { user, permissions } = userResponse.data;

        commit('SET_USER', user);
        commit('SET_PERMISSIONS', permissions);
      } catch (error) {
        throw error;
      }
    },
    logout({ commit }) {
      localStorage.removeItem('token');
      commit('SET_USER', null);
      commit('SET_PERMISSIONS', []);
    },
    async fetchPermissions({ commit }) {
      const token = localStorage.getItem('token');
      if (token) {
        try {
          const userResponse = await axios.get('/api/user', { // 替换成你的获取用户信息 API
            headers: { Authorization: `Bearer ${token}` }
          });
          const { permissions } = userResponse.data;
          commit('SET_PERMISSIONS', permissions);
        } catch (error) {
          // 处理错误,例如 token 过期或无效
          console.error('Failed to fetch permissions:', error);
          commit('SET_USER', null);
          commit('SET_PERMISSIONS', []);
          localStorage.removeItem('token');
          router.push('/login'); // 重定向到登录页面
        }
      }
    }
  },
  getters: {
    isLoggedIn: state => state.isLoggedIn,
    user: state => state.user,
    permissions: state => state.permissions
  }
})

// router/index.js (修改)
import router from './router'
import store from './store'

router.beforeEach(async (to, from, next) => {
  // 1. 首次加载或刷新页面时,从服务端获取权限
  if (store.getters.isLoggedIn && store.getters.permissions.length === 0) {
    await store.dispatch('fetchPermissions');
  }

  // 2. 判断用户是否已登录
  const isLoggedIn = store.getters.isLoggedIn;

  // 3. 如果需要登录才能访问的路由,但用户未登录,则跳转到登录页面
  if (to.meta.requiresAuth && !isLoggedIn) {
    next({
      path: '/login',
      query: { redirect: to.fullPath }
    });
  } else {
    // 4. 如果用户已登录,但访问登录页面,则跳转到首页
    if (to.path === '/login' && isLoggedIn) {
      next('/');
    } else {
      // 5. 权限验证
      if (to.meta.permissions) {
        const userPermissions = store.getters.permissions;
        const hasPermission = to.meta.permissions.every(permission => userPermissions.includes(permission));

        if (hasPermission) {
          next();
        } else {
          next('/403');
        }
      } else {
        next();
      }
    }
  }
});

关键点:

  • fetchPermissions action: 在 Vuex 中添加一个 fetchPermissions action,用于从服务端获取用户权限。
  • beforeEach 中调用 fetchPermissionsrouter.beforeEach 中,判断用户是否已登录且权限列表为空,如果是,则调用 fetchPermissions action 从服务端获取权限。这样确保了在首次加载或刷新页面时,能够从服务端获取最新的权限信息。
  • 存储 Token: 在登录成功后,将 Token 存储到 localStorage 或 cookie 中,以便在后续的请求中携带 Token。
  • 携带 Token: 在发送请求时,将 Token 添加到请求头中,以便服务端进行身份验证。

权限验证流程表格

步骤 客户端 服务端 说明
1 用户尝试访问需要权限的路由 用户点击链接或在地址栏输入 URL。
2 Vue Router 导航守卫 (beforeEach) 拦截路由 检查用户是否已登录,以及是否具有访问该路由所需的权限。
3 如果未登录,跳转到登录页面 提示用户登录。
4 用户登录,提交用户名和密码
5 验证用户名和密码,生成 JWT Token 验证用户身份,如果验证成功,则生成 JWT Token,包含用户 ID、用户名和角色等信息。
6 存储 JWT Token 到客户端 (例如 localStorage) 方便后续请求携带身份信息。
7 从服务端获取用户权限信息 通常在登录成功后调用一个 API,返回用户的角色和权限列表。
8 将权限信息存储到 Vuex 中 方便在客户端进行权限验证。
9 继续路由跳转 如果客户端权限验证通过,则允许路由跳转。
10 发送 API 请求 验证 JWT Token,检查用户权限 在服务端,每个需要权限验证的 API 都会验证 JWT Token,并检查用户是否具有访问该 API 所需的权限。
11 如果权限不足,返回 403 状态码 拒绝访问,并返回错误信息。
12 显示 403 页面或提示信息 如果客户端收到 403 状态码,则显示 403 页面或提示信息,告知用户没有权限访问该资源。
13 定期刷新权限信息 (可选) 可以使用 WebSocket、轮询等方式,定期从服务端获取最新的权限信息,确保客户端权限信息与服务端同步。

常见问题及解决方案

  • 权限信息不一致: 客户端和服务端的权限信息不一致可能导致用户体验问题。解决方案是保持权限信息同步,可以使用 WebSocket、轮询等方式。
  • Token 过期: JWT Token 有过期时间,过期后需要重新登录。可以在客户端添加 Token 刷新机制,在 Token 即将过期时,自动向服务端请求新的 Token。
  • 跨域问题: 如果客户端和服务端不在同一个域名下,可能会遇到跨域问题。可以使用 CORS (Cross-Origin Resource Sharing) 来解决跨域问题。
  • 安全性问题: JWT Token 的安全性非常重要,必须使用安全的密钥,并防止 Token 泄露。

最佳实践

  • 使用 JWT Token 进行身份验证。
  • 在客户端和服务端都进行权限验证。
  • 保持客户端和服务端的权限信息同步。
  • 使用 RBAC 或 ABAC 进行权限管理。
  • 对敏感数据进行加密。
  • 定期审计权限配置。
  • 使用 HTTPS 协议。

总结

通过结合 Vue Router 导航守卫和后端权限校验,我们可以实现客户端和服务端权限验证的同步,从而提高应用的安全性、用户体验和整体架构的健壮性。关键在于保持客户端和服务端权限信息的一致性,并采取有效的安全措施。希望今天的分享对大家有所帮助,谢谢!

记住,权限控制是双重保障

客户端和服务端都需要权限验证,客户端提供快速反馈,服务端提供最终保障。

谨记,权限同步是关键

客户端和服务端权限信息需要保持一致,可以通过多种方式实现,例如WebSocket、轮询等。

安全,始终是第一要务

采取有效的安全措施,例如使用 JWT Token、加密敏感数据、定期审计权限配置等。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注