3 个回答

思路

1.数据库设计:

  • 设计用户、角色和权限相关的表结构。

2.后端实现:

  • 使用 Node.js 和 Express 实现用户认证和权限管理。

3.前端实现:

  • 使用 Vue3 和 Pinia 实现状态管理和路由权限控制。

4.组件权限控制:

  • 使用自定义指令或方法在组件层面控制权限。

目录结构

project-root/
├── backend/
│   ├── controllers/
│   │   └── authController.js
│   ├── models/
│   │   ├── user.js
│   │   ├── role.js
│   │   └── permission.js
│   ├── routes/
│   │   └── authRoutes.js
│   ├── config/
│   │   └── db.js
│   ├── middleware/
│   │   └── authMiddleware.js
│   └── server.js
├── frontend/
│   ├── src/
│   │   ├── components/
│   │   │   └── UserButton.vue
│   │   ├── views/
│   │   │   ├── Home.vue
│   │   │   ├── Login.vue
│   │   │   └── Denied.vue
│   │   ├── store/
│   │   │   └── userPermissions.js
│   │   ├── router/
│   │   │   └── index.js
│   │   ├── App.vue
│   │   └── main.js
└── package.json

1. 数据库设计
创建用户、角色和权限相关的表:

-- 用户表,存储用户的基本信息
CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,  -- 用户ID,主键,自增
  username VARCHAR(255) NOT NULL,     -- 用户名,非空
  password VARCHAR(255) NOT NULL,     -- 密码,非空
  role_id INT,                        -- 角色ID,外键
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,  -- 创建时间,默认当前时间
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP  -- 更新时间,默认当前时间,更新时自动更新
);

-- 角色表,存储不同的角色信息
CREATE TABLE roles (
  id INT AUTO_INCREMENT PRIMARY KEY,  -- 角色ID,主键,自增
  name VARCHAR(255) NOT NULL,         -- 角色名称,非空
  description VARCHAR(255),           -- 角色描述
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,  -- 创建时间,默认当前时间
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP  -- 更新时间,默认当前时间,更新时自动更新
);

-- 权限表,存储具体的权限信息
CREATE TABLE permissions (
  id INT AUTO_INCREMENT PRIMARY KEY,  -- 权限ID,主键,自增
  name VARCHAR(255) NOT NULL,         -- 权限名称,非空
  description VARCHAR(255),           -- 权限描述
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,  -- 创建时间,默认当前时间
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP  -- 更新时间,默认当前时间,更新时自动更新
);

-- 角色权限关联表,存储角色与权限的对应关系
CREATE TABLE role_permissions (
  role_id INT,                        -- 角色ID,外键
  permission_id INT,                  -- 权限ID,外键
  PRIMARY KEY (role_id, permission_id),  -- 联合主键
  FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,  -- 角色ID外键,级联删除
  FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE  -- 权限ID外键,级联删除
);

-- 用户权限关联表,存储用户与权限的直接对应关系(可选)
CREATE TABLE user_permissions (
  user_id INT,                        -- 用户ID,外键
  permission_id INT,                  -- 权限ID,外键
  PRIMARY KEY (user_id, permission_id),  -- 联合主键
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,  -- 用户ID外键,级联删除
  FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE  -- 权限ID外键,级联删除
);

2. 后端实现
用户认证和权限管理:

  • 在 controllers/authController.js 中实现登录和权限检查逻辑。
  • 在 routes/authRoutes.js 中定义相关路由。
  • 在 middleware/authMiddleware.js 中实现权限中间件。

authController.js

const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { getUserByUsername, createUser } = require('../models/user');
const { getRoleById } = require('../models/role');
const { getPermissionsByRoleId } = require('../models/permission');

const login = async (req, res) => {
  const { username, password } = req.body;
  const user = await getUserByUsername(username);

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

  const isMatch = await bcrypt.compare(password, user.password);
  if (!isMatch) {
    return res.status(401).json({ message: 'Invalid username or password' });
  }

  const role = await getRoleById(user.role_id);
  const permissions = await getPermissionsByRoleId(user.role_id);

  const token = jwt.sign({ id: user.id, role: role.name, permissions: permissions.map(p => p.name) }, 'your_jwt_secret', { expiresIn: '1h' });

  res.json({ token });
};

const register = async (req, res) => {
  const { username, password, role_id } = req.body;
  const hashedPassword = await bcrypt.hash(password, 10);
  const userId = await createUser(username, hashedPassword, role_id);

  res.status(201).json({ id: userId });
};

module.exports = { login, register };

user.js

const pool = require('../config/db');

const getUserByUsername = async (username) => {
  const [rows] = await pool.query('SELECT * FROM users WHERE username = ?', [username]);
  return rows[0];
};

const createUser = async (username, password, role_id) => {
  const [result] = await pool.query('INSERT INTO users (username, password, role_id) VALUES (?, ?, ?)', [username, password, role_id]);
  return result.insertId;
};

module.exports = { getUserByUsername, createUser };

role.js

const pool = require('../config/db');

const getRoleById = async (id) => {
  const [rows] = await pool.query('SELECT * FROM roles WHERE id = ?', [id]);
  return rows[0];
};

module.exports = { getRoleById };

permission.js

const pool = require('../config/db');

const getPermissionsByRoleId = async (role_id) => {
  const [rows] = await pool.query('SELECT p.* FROM permissions p JOIN role_permissions rp ON p.id = rp.permission_id WHERE rp.role_id = ?', [role_id]);
  return rows;
};

module.exports = { getPermissionsByRoleId };

authRoutes.js

const express = require('express');
const { login, register } = require('../controllers/authController');

const router = express.Router();

router.post('/login', login);
router.post('/register', register);

module.exports = router;

db.js

const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'your_database_name',
});

module.exports = pool;

authMiddleware.js

const jwt = require('jsonwebtoken');

const authenticate = (req, res, next) => {
  const token = req.header('Authorization').replace('Bearer ', '');
  if (!token) {
    return res.status(401).json({ message: 'Access denied' });
  }

  try {
    const decoded = jwt.verify(token, 'your_jwt_secret');
    req.user = decoded;
    next();
  } catch (err) {
    res.status(401).json({ message: 'Invalid token' });
  }
};

const authorize = (requiredPermissions) => {
  return (req, res, next) => {
    const userPermissions = req.user.permissions;
    const hasPermission = requiredPermissions.every(permission => userPermissions.includes(permission));

    if (!hasPermission) {
      return res.status(403).json({ message: 'Forbidden' });
    }

    next();
  };
};

module.exports = { authenticate, authorize };

server.js:

const express = require('express');
const bodyParser = require('body-parser');
const authRoutes = require('./routes/authRoutes');

const app = express();
app.use(bodyParser.json());

app.use('/api/auth', authRoutes);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

3.前端实现
状态管理和路由权限控制:

  • 在 store/userPermissions.js 中存储用户角色和权限信息。
  • 在 router/index.js 中使用路由守卫检查权限。

UserButton.vue

<template>
  <div>
    <el-button v-if="hasPermission('sys:user:add')" type="success" plain>添加用户</el-button>
  </div>
</template>

<script>
import { userPermissionsStore } from '@/store/userPermissions';
import { storeToRefs } from 'pinia';

export default {
  setup() {
    const store = userPermissionsStore();
    const { userPermissions } = storeToRefs(store);

    const hasPermission = (permission) => {
      return userPermissions.value.includes(permission);
    };

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

Home.vue

<template>
  <div>
    <h1>Home Page</h1>
    <UserButton />
  </div>
</template>

<script>
import UserButton from '@/components/UserButton.vue';

export default {
  components: {
    UserButton,
  },
};
</script>

Login.vue

<template>
  <div>
    <h1>Login Page</h1>
    <form @submit.prevent="login">
      <input v-model="username" placeholder="Username" />
      <input v-model="password" type="password" placeholder="Password" />
      <button type="submit">Login</button>
    </form>
  </div>
</template>

<script>
import { userPermissionsStore } from '@/store/userPermissions';
import { useRouter } from 'vue-router';

export default {
  data() {
    return {
      username: '',
      password: '',
    };
  },
  setup() {
    const store = userPermissionsStore();
    const router = useRouter();

    const login = async () => {
      // 模拟登录请求
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username: this.username, password: this.password }),
      });
      const data = await response.json();
      if (response.ok) {
        store.setUserPermissions(data);
        router.push('/home');
      } else {
        alert(data.message);
      }
    };

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

Denied.vue

<template>
  <div>
    <h1>Access Denied</h1>
    <p>You do not have permission to view this page.</p>
  </div>
</template>

userPermissions.js

import { defineStore } from 'pinia';
import { ref } from 'vue';

export const userPermissionsStore = defineStore('userPermissions', () => {
  const roles = ref('');
  const userPermissions = ref([]);
  const isLogin = ref(false);

  const setUserPermissions = (params) => {
    userPermissions.value = params.permissions;
    roles.value = params.role;
    isLogin.value = true;
  };

  const logout = () => {
    userPermissions.value = [];
    roles.value = '';
    isLogin.value = false;
  };

  return { isLogin, userPermissions, roles, setUserPermissions, logout };
});

index.js

import { createRouter, createWebHashHistory } from 'vue-router';
import { userPermissionsStore } from '@/store/userPermissions';
import { storeToRefs } from 'pinia';

const routes = [
  {
    path: '/home',
    name: 'home',
    component: () => import('../views/Home.vue'),
    meta: { requireAuth: true, roles: ['admin', 'guest'] },
  },
  { path: '/login', name: 'login', component: () => import('../views/Login.vue') },
  { path: '/denied', name: 'denied', component: () => import('../views/Denied.vue') },
];

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

router.beforeEach((to, from, next) => {
  const store = userPermissionsStore();
  const { isLogin, roles } = storeToRefs(store);

  if (to.meta.requireAuth) {
    if (isLogin.value) {
      if (to.meta.roles.includes(roles.value)) {
        next();
      } else {
        next('/denied');
      }
    } else {
      next('/login');
    }
  } else {
    next();
  }
});

export default router;

App.vue

<template>
  <router-view />
</template>

main.js:

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';

const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount('#app');

package.json

{
  "name": "vue3-nodejs-auth",
  "version": "1.0.0",
  "description": "A project with Vue3 and Node.js for user authentication and authorization",
  "main": "backend/server.js",
  "scripts": {
    "start": "node backend/server.js",
    "dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"",
    "dev:backend": "nodemon backend/server.js",
    "dev:frontend": "cd frontend && npm run serve"
  },
  "dependencies": {
    "bcrypt": "^5.0.1",
    "body-parser": "^1.19.0",
    "concurrently": "^6.2.1",
    "express": "^4.17.1",
    "jsonwebtoken": "^8.5.1",
    "mysql2": "^2.2.5",
    "pinia": "^2.0.11",
    "vue": "^3.2.31",
    "vue-router": "^4.0.12"
  },
  "devDependencies": {
    "nodemon": "^2.0.7"
  }
}

vue项目中,页面的显示跟路由有关,你可以给指定的账号配置不同的页面权限,然后在用户登录的时候拿到用户的角色信息,通过角色获取到对应的页面权限,然后用页面权限匹配用户路由信息,最后返还给前端,前端拿到的就是符合账号权限的路由信息,最后在动态添加路由渲染成页面。

用户登录->获取token->获取用户信息及配置的路由数据->根据数据添加页面路由、生成按钮权限数据->进入路由中的第一个页面

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏