Vue Router与后端权限系统的协调:实现客户端导航守卫与服务端拦截器的同步

Vue Router 与后端权限系统的协调:客户端导航守卫与服务端拦截器的同步

大家好,今天我们来聊聊 Vue Router 与后端权限系统的协调,重点是如何实现客户端导航守卫与服务端拦截器的同步。这是一个在实际项目中非常常见且重要的问题,处理不好会导致用户体验下降、安全漏洞甚至系统崩溃。

权限控制的重要性

在单页应用(SPA)中,权限控制至关重要,它决定了用户可以访问哪些页面和功能。仅仅依靠前端权限控制是不够的,因为前端代码容易被篡改。因此,必须结合后端权限控制,形成双重保障。

为什么需要双重保障?

  • 前端易被绕过: 前端的权限控制逻辑很容易被开发者工具绕过或修改,恶意用户可以直接访问受限的路由或修改页面元素。
  • 数据安全性: 即使前端隐藏了某些功能,用户仍然可能通过直接发送 API 请求来访问敏感数据。后端权限控制可以防止未经授权的数据访问。
  • 代码可维护性: 将所有权限逻辑放在前端会导致代码臃肿且难以维护。

权限控制的策略

常见的权限控制策略有以下几种:

  • 基于角色的访问控制 (RBAC): 为用户分配角色,角色拥有不同的权限。
  • 基于权限的访问控制 (PBAC): 直接为用户分配权限。
  • 基于属性的访问控制 (ABAC): 根据用户的属性、资源属性和环境属性来动态决定是否允许访问。

无论采用哪种策略,都需要在前端和后端都进行相应的实现。

Vue Router 导航守卫

Vue Router 提供了导航守卫,允许我们在路由跳转前后执行一些逻辑,例如权限检查。常用的导航守卫有:

  • beforeEach: 在每次路由跳转前执行。
  • beforeResolve: 在所有组件内守卫 resolve 之后执行。
  • afterEach: 在每次路由跳转后执行。

我们将使用 beforeEach 导航守卫来实现客户端的权限控制。

代码示例:

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

Vue.use(VueRouter)

const routes = [
  {
    path: '/home',
    name: 'Home',
    component: () => import('../views/Home.vue'),
    meta: { requiresAuth: true } // 标记需要登录才能访问的路由
  },
  {
    path: '/profile',
    name: 'Profile',
    component: () => import('../views/Profile.vue'),
    meta: { requiresAuth: true, roles: ['admin'] } // 标记需要登录且角色为 admin 才能访问的路由
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('../views/Login.vue')
  },
  {
    path: '*',
    redirect: '/home'
  }
]

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

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    // 判断该路由是否需要登录权限
    if (!store.getters.isLoggedIn) {
      // 没有登录,跳转到登录页面
      next({
        path: '/login',
        query: { redirect: to.fullPath } // 将要跳转的路由路径作为参数传递给登录页面,方便登录后重定向
      })
    } else {
      // 已经登录,判断是否有权限访问
      if (to.meta.roles) {
        // 需要特定的角色才能访问
        if (store.getters.userRoles.some(role => to.meta.roles.includes(role))) {
          // 用户拥有所需的角色,允许访问
          next()
        } else {
          // 用户没有所需的角色,跳转到 403 页面或者其他处理方式
          next('/403') // 假设有一个 403 页面
        }
      } else {
        // 不需要特定的角色,允许访问
        next()
      }
    }
  } else {
    // 不需要登录权限,直接访问
    next()
  }
})

export default router

代码解释:

  • routes 数组定义了应用的路由,每个路由对象都有 pathnamecomponentmeta 属性。
  • meta 属性用于存储路由的元数据,例如 requiresAuth 标记该路由是否需要登录才能访问,roles 标记需要哪些角色才能访问。
  • router.beforeEach 是一个全局前置守卫,它会在每次路由跳转前执行。
  • beforeEach 守卫中,我们首先判断目标路由是否需要登录权限。
  • 如果需要登录权限,我们再判断用户是否已经登录。
  • 如果用户没有登录,我们跳转到登录页面,并将要跳转的路由路径作为参数传递给登录页面,方便登录后重定向。
  • 如果用户已经登录,我们再判断用户是否有权限访问。
  • 如果需要特定的角色才能访问,我们判断用户是否拥有所需的角色。
  • 如果用户拥有所需的角色,我们允许访问。
  • 如果用户没有所需的角色,我们跳转到 403 页面或者其他处理方式。
  • 如果不需要登录权限,我们直接访问。

Vuex 的配合:

在上面的代码中,我们使用了 Vuex store 来存储用户信息和权限。Vuex 可以帮助我们在不同的组件之间共享状态。

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    isLoggedIn: false,
    user: null,
    userRoles: []
  },
  getters: {
    isLoggedIn: state => state.isLoggedIn,
    user: state => state.user,
    userRoles: state => state.userRoles
  },
  mutations: {
    setLoggedIn (state, isLoggedIn) {
      state.isLoggedIn = isLoggedIn
    },
    setUser (state, user) {
      state.user = user
    },
    setUserRoles (state, roles) {
      state.userRoles = roles
    }
  },
  actions: {
    login ({ commit }, userInfo) {
      // 模拟登录,实际项目中需要调用后端 API
      return new Promise(resolve => {
        setTimeout(() => {
          commit('setLoggedIn', true)
          commit('setUser', { username: userInfo.username })
          // 模拟获取用户角色,实际项目中需要从后端获取
          const roles = userInfo.username === 'admin' ? ['admin', 'editor'] : ['editor']
          commit('setUserRoles', roles)
          resolve()
        }, 500)
      })
    },
    logout ({ commit }) {
      // 模拟登出,实际项目中需要调用后端 API
      return new Promise(resolve => {
        setTimeout(() => {
          commit('setLoggedIn', false)
          commit('setUser', null)
          commit('setUserRoles', [])
          resolve()
        }, 500)
      })
    }
  },
  modules: {}
})

登录组件:

<template>
  <div>
    <input type="text" v-model="username" placeholder="Username">
    <input type="password" v-model="password" placeholder="Password">
    <button @click="login">Login</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      username: '',
      password: ''
    }
  },
  methods: {
    async login() {
      try {
        await this.$store.dispatch('login', { username: this.username, password: this.password });
        // 登录成功后重定向到之前的页面
        this.$router.push(this.$route.query.redirect || '/home');
      } catch (error) {
        console.error('Login failed:', error);
      }
    }
  }
}
</script>

后端权限拦截器

光有前端权限控制是不够的,我们还需要在后端进行权限拦截。后端的权限拦截器会在接收到请求后,检查用户是否拥有访问该资源的权限。

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

const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const port = 3000;

// 密钥,用于签名和验证 JWT
const secretKey = 'your-secret-key';

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

// 模拟数据库中的权限数据
const permissions = {
  '/admin': ['admin'],
  '/editor': ['editor', 'admin'],
  '/public': []
};

app.use(express.json());

// 登录接口,生成 JWT
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const user = users[username];

  if (user) {
    // 实际项目中需要验证密码
    const token = jwt.sign({ userId: user.id, username: user.username, roles: user.roles }, secretKey, { expiresIn: '1h' });
    res.json({ token });
  } else {
    res.status(401).json({ message: 'Invalid credentials' });
  }
});

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

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

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

    req.user = user;
    next();
  });
}

// 角色验证中间件
function authorize(requiredRoles) {
  return (req, res, next) => {
    const userRoles = req.user.roles;

    if (requiredRoles && !requiredRoles.some(role => userRoles.includes(role))) {
      return res.status(403).json({ message: 'Unauthorized' });
    }

    next();
  };
}

// 路由示例
app.get('/admin', authenticate, authorize(['admin']), (req, res) => {
  res.json({ message: 'Welcome to the admin area!' });
});

app.get('/editor', authenticate, authorize(['editor', 'admin']), (req, res) => {
  res.json({ message: 'Welcome to the editor area!' });
});

app.get('/public', (req, res) => {
  res.json({ message: 'This is a public area.' });
});

app.listen(port, () => {
  console.log(`Server listening at http://localhost:${port}`);
});

代码解释:

  • authenticate 中间件用于验证 JWT,如果 JWT 无效或过期,则返回 401 或 403 错误。
  • authorize 中间件用于检查用户是否拥有访问该资源的权限,如果用户没有权限,则返回 403 错误。
  • 在路由处理函数中,我们使用 authenticateauthorize 中间件来保护路由。
  • /login 接口用于生成 JWT,实际项目中需要验证用户名和密码。

前后端权限同步的方案

实现前后端权限同步的关键在于:

  1. 统一的权限数据源: 前后端都需要使用同一个权限数据源,例如数据库或配置文件。
  2. 一致的权限验证逻辑: 前后端都需要使用一致的权限验证逻辑,例如 RBAC 或 PBAC。
  3. JWT (JSON Web Token): 使用 JWT 作为用户身份的载体,JWT 中包含用户的角色和权限信息。

流程:

  1. 用户登录后,后端生成 JWT,并将 JWT 返回给前端。
  2. 前端将 JWT 存储在 Cookie 或 LocalStorage 中。
  3. 前端在每次发送 API 请求时,将 JWT 放在 Authorization Header 中。
  4. 后端接收到请求后,从 Authorization Header 中提取 JWT,并验证 JWT 的有效性。
  5. 后端根据 JWT 中的用户信息和权限信息,判断用户是否拥有访问该资源的权限。
  6. 前端在路由跳转前,从 JWT 中提取用户的角色和权限信息,并判断用户是否拥有访问该路由的权限。

具体步骤:

  1. 后端生成 JWT: 用户登录后,后端根据用户的角色和权限信息,生成 JWT,并将 JWT 返回给前端。
  2. 前端存储 JWT: 前端将 JWT 存储在 Cookie 或 LocalStorage 中,方便后续使用。
  3. 前端发送 API 请求: 前端在每次发送 API 请求时,将 JWT 放在 Authorization Header 中,例如:

    axios.defaults.headers.common['Authorization'] = `Bearer ${localStorage.getItem('token')}`;
  4. 后端验证 JWT: 后端接收到请求后,从 Authorization Header 中提取 JWT,并验证 JWT 的有效性。
  5. 后端进行权限验证: 后端根据 JWT 中的用户信息和权限信息,判断用户是否拥有访问该资源的权限。
  6. 前端进行路由守卫: 前端在路由跳转前,从 JWT 中提取用户的角色和权限信息,并判断用户是否拥有访问该路由的权限。

    router.beforeEach((to, from, next) => {
      const token = localStorage.getItem('token');
      if (to.matched.some(record => record.meta.requiresAuth)) {
        if (!token) {
          next({
            path: '/login',
            query: { redirect: to.fullPath }
          });
        } else {
          try {
            const decoded = jwt_decode(token); // 使用 jwt-decode 库解码 JWT
            const userRoles = decoded.roles; // 假设 JWT 中包含 roles 字段
            if (to.meta.roles && !to.meta.roles.every(role => userRoles.includes(role))) {
              next('/403');
            } else {
              next();
            }
          } catch (error) {
            // JWT 解析失败,可能是无效的 token
            console.error('JWT 解析失败:', error);
            next('/login'); // 重新登录
          }
        }
      } else {
        next();
      }
    });

    注意: jwt_decode 是一个前端库,用于解码 JWT。 它验证 JWT 的签名,因此不能用于后端的身份验证。 在后端,你应该使用 jsonwebtoken 库的 verify 方法来验证 JWT 的签名。 roles 字段是假设 JWT 中包含角色信息,实际项目中需要根据 JWT 的结构进行调整。

权限变更的同步

当用户的权限发生变更时,我们需要及时同步前端的权限信息。常见的做法有:

  • 重新登录: 当用户的权限发生变更时,强制用户重新登录,重新获取 JWT。
  • 刷新 JWT: 在 JWT 过期前,自动刷新 JWT,获取最新的权限信息。 这需要在后端提供一个刷新 JWT 的接口。
  • WebSocket: 使用 WebSocket 技术,当用户的权限发生变更时,后端主动推送消息给前端,前端更新权限信息。

代码示例 (刷新 JWT):

  • 前端:

    // 定时刷新 JWT
    setInterval(async () => {
      try {
        const response = await axios.post('/refresh-token'); // 调用后端刷新 JWT 的接口
        const newToken = response.data.token;
        localStorage.setItem('token', newToken);
        axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
      } catch (error) {
        console.error('Failed to refresh token:', error);
        // 刷新失败,可能需要重新登录
        // ...
      }
    }, 30 * 60 * 1000); // 每 30 分钟刷新一次
  • 后端:

    // 刷新 JWT 的接口
    app.post('/refresh-token', authenticate, (req, res) => {
      const user = req.user;
      const newToken = jwt.sign({ userId: user.userId, username: user.username, roles: user.roles }, secretKey, { expiresIn: '1h' });
      res.json({ token: newToken });
    });

常见问题与解决方案

  • CSRF 攻击: 使用 Cookie 存储 JWT 时,需要注意 CSRF 攻击。 可以使用 SameSite 属性和 CSRF Token 来防止 CSRF 攻击。
  • XSS 攻击: 使用 LocalStorage 存储 JWT 时,需要注意 XSS 攻击。 对用户输入进行严格的过滤和转义,避免 XSS 攻击。
  • JWT 过期: JWT 过期后,需要重新登录或刷新 JWT。 合理设置 JWT 的过期时间,避免频繁的重新登录或刷新 JWT。
  • 权限粒度: 权限粒度需要根据实际需求进行设计。 权限粒度过粗会导致权限控制不够精细,权限粒度过细会导致权限管理过于复杂。

总结一些建议

本次讲座我们学习了 Vue Router 与后端权限系统的协调,以及如何实现客户端导航守卫与服务端拦截器的同步。实现前后端权限同步的关键在于统一的权限数据源、一致的权限验证逻辑和 JWT。我们需要根据实际需求选择合适的权限控制策略和同步方案,并注意安全问题。

希望今天的分享对大家有所帮助!

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

发表回复

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