likes
comments
collection
share

别被用户的恳求打败了,学会使用 RBAC 管理权限吧

作者站长头像
站长
· 阅读数 36

代码是伪代码并不能保证可以正确运行,但是如果你愿意花点时间的话。我认为也没有什么问题

最近这几天在整合系统的过程中发现过去几年的几个系统的权限体系混乱,于是计划把所有的系统权限统一管理起来,就重新设计了一个RBAC系统,当前我们实际开发后端是java,不过我是前端,就用node来替代了

基于角色的访问控制(Role-Based Access Control,RBAC)是一种常用的安全模型,它通过将用户分配到角色,然后再将角色分配到权限来管理访问控制。在这种模型中,用户无需直接分配权限,而是根据其所属的角色进行授权。

在本文中,我们将介绍如何使用 Node.js、Express 和 MySQL 实现 RBAC。我们将从数据库设计开始,逐步实现用户登录、角色管理、权限管理和资源访问控制。示例代码将提供完整的代码实现,以及详细的注释,让您可以更好地理解和应用 RBAC 模型。

数据库设计

在本例中,我们将使用 MySQL 作为数据库,设计以下五个表:

  • User 表:存储用户信息,包括 ID、用户名和密码等字段。
  • Role 表:存储角色信息,包括 ID 和名称等字段。
  • Permission 表:存储权限信息,包括 ID 和名称等字段。
  • User_Role 表:存储用户与角色的关系,包括 UserID 和 RoleID 等字段。
  • Role_Permission 表:存储角色与权限的关系,包括 RoleID 和 PermissionID 等字段。

下面是表的 SQL 定义语句:

CREATE TABLE User (
  ID INT NOT NULL AUTO_INCREMENT,
  Name VARCHAR(255) NOT NULL,
  Password VARCHAR(255) NOT NULL,
  PRIMARY KEY (ID)
);

CREATE TABLE Role (
  ID INT NOT NULL AUTO_INCREMENT,
  Name VARCHAR(255) NOT NULL,
  PRIMARY KEY (ID)
);

CREATE TABLE Permission (
  ID INT NOT NULL AUTO_INCREMENT,
  Name VARCHAR(255) NOT NULL,
  PRIMARY KEY (ID)
);

CREATE TABLE User_Role (
  ID INT NOT NULL AUTO_INCREMENT,
  UserID INT NOT NULL,
  RoleID INT NOT NULL,
  PRIMARY KEY (ID),
  FOREIGN KEY (UserID) REFERENCES User(ID),
  FOREIGN KEY (RoleID) REFERENCES Role(ID)
);

CREATE TABLE Role_Permission (
  ID INT NOT NULL AUTO_INCREMENT,
  RoleID INT NOT NULL,
  PermissionID INT NOT NULL,
  PRIMARY KEY (ID),
  FOREIGN KEY (RoleID) REFERENCES Role(ID),
  FOREIGN KEY (PermissionID) REFERENCES Permission(ID)
);

用户登录

我们首先实现用户登录功能。用户输入用户名和密码,系统验证用户身份,并返回用户信息和授权信息。我们使用 Express 框架来实现服务器端的路由处理和中间件功能。

首先创建一个 Express 应用程序,安装依赖:

$ mkdir rbac-demo && cd rbac-demo
$ npm init -y
$ npm install express mysql

然后在项目根目录下创建一个名为 index.js 的文件,编写如下代码:

// 导入必要的模块
const express = require('express');
const mysql = require('mysql');

// 创建 Express 应用程序
const app = express();

// 创建 MySQL 连接池
const db = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: '123456',
  database: 'rbac_demo'
});

// 添加中间件,解析请求主体
app.use(express.json());

// 用户登录接口
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  // 查询用户信息和授权信息
  const sql = `
    SELECT u.*, r.ID AS role_id, r.Name AS role_name, p.ID AS permission_id, p.Name AS permission_name
    FROM User u
    LEFT JOIN User_Role ur ON u.ID = ur.UserID
    LEFT JOIN Role r ON ur.RoleID = r.ID
    LEFT JOIN Role_Permission rp ON r.ID = rp.RoleID
    LEFT JOIN Permission p ON rp.PermissionID = p.ID
    WHERE u.Name = ? AND u.Password = ?
  `;
  db.query(sql, [username, password], (err, result) => {
    if (err) {
      console.error(err);
      res.status(500).send('服务器内部错误');
    } else if (result.length === 0) {
      res.status(401).send('用户名或密码错误');
    } else {
      // 构造用户信息和授权信息对象
      const user = { id: result[0].ID, name: result[0].Name };
      const roles = [];
      const permissions = [];
      result.forEach(row => {
        const roleId = row.role_id;
        const roleName = row.role_name;
        const permissionId = row.permission_id;
        const permissionName = row.permission_name;
        if (roleId && !roles.some(r => r.id === roleId)) {
          roles.push({ id: roleId, name: roleName });
        }
        if (permissionId && !permissions.some(p => p.id === permissionId)) {
          permissions.push({ id: permissionId, name: permissionName });
        }
      });
      const auth = { roles, permissions };
      // 将用户信息和授权信息发送给客户端
      res.send({ user, auth });
    }
  });
});

// 启动应用程序,监听端口 3000
app.listen(3000, () => {
  console.log('应用程序已启动,监听端口 3000。');
});

原生客户端代码

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>RBAC Demo</title>
  </head>
  <body>
    <h1>RBAC Demo</h1>
    <form>
      <label>用户名:<input type="text" name="username"></label><br>
      <label>密码:<input type="password" name="password"></label><br>
      <button type="submit">登录</button>
    </form>
    <script>
      const form = document.querySelector('form');
      form.addEventListener('submit', async (event) => {
        event.preventDefault();
        const username = form.elements.username.value;
        const password = form.elements.password.value;
        try {
          // 发送登录请求
          const response = await fetch('/login', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({ username, password })
          });
          if (!response.ok) {
            throw new Error('登录失败');
          }
          const data = await response.json();
          // 获取用户信息和授权信息
          const user = data.user;
          const auth = data.auth;
          // 显示用户信息
          const userInfo = document.createElement('p');
          userInfo.textContent = `当前用户:${user.username},角色:${user.role}`;
          document.body.appendChild(userInfo);
          // 根据授权信息显示可用的操作
          const actions = ['read', 'write', 'delete'];
          const availableActions = actions.filter(action => auth.permissions.includes(action));
          const actionsList = document.createElement('ul');
          availableActions.forEach(action => {
            const actionItem = document.createElement('li');
            actionItem.textContent = `可以进行${action}操作`;
            actionsList.appendChild(actionItem);
          });
          document.body.appendChild(actionsList);
        } catch (error) {
          console.error(error);
        }
      });
    </script>
  </body>
</html>

这段代码包括了一个简单的表单,用户可以输入用户名和密码进行登录。客户端会向服务器发送一个 POST 请求,在服务器端会验证用户信息,如果验证通过则会返回用户信息和授权信息。客户端会根据用户的角色和授权信息,显示当前用户信息和可用的操作。在这个示例中,可用的操作包括读、写和删除。

如果用户尝试进行未授权的操作,客户端可以根据需要显示相应的错误信息。

在实际应用中,通常会使用更高级的 UI 框架和库来构建客户端应用。这些框架和库通常包括更多的功能和组件,处理更复杂的场景。

例如,在 React 中,可以使用组件来构建客户端应用,并使用状态来管理应用的行为和数据

React 组件示例

import React, { useState } from 'react';

function Login() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [user, setUser] = useState(null);
  const [auth, setAuth] = useState(null);
  const [error, setError] = useState(null);

  const handleLogin = async (event) => {
    event.preventDefault();
    try {
      // 发送登录请求
      const response = await fetch('/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ username, password })
      });
      if (!response.ok) {
        throw new Error('登录失败');
      }
      const data = await response.json();
      // 获取用户信息和授权信息
      setUser(data.user);
      setAuth(data.auth);
    } catch (error) {
      setError(error.message);
    }
  };

  const actions = ['read', 'write', 'delete'];
  const availableActions = auth ? actions.filter(action => auth.permissions.includes(action)) : [];

  return (
    <div>
      <h1>RBAC Demo</h1>
      {user ? (
        <p>当前用户:{user.username},角色:{user.role}</p>
      ) : (
        <form onSubmit={handleLogin}>
          <label>
            用户名:
            <input type="text" value={username} onChange={event => setUsername(event.target.value)} />
          </label>
          <br />
          <label>
            密码:
            <input type="password" value={password} onChange={event => setPassword(event.target.value)} />
          </label>
          <br />
          <button type="submit">登录</button>
        </form>
      )}
      {error && <p>{error}</p>}
      {availableActions.length > 0 && (
        <ul>
          {availableActions.map(action => (
            <li key={action}>可以进行{action}操作</li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default Login;

在这个示例中,使用了 React 的 hooks 和组件来构建登录表单和显示用户信息和可用操作的部分。使用状态来管理应用的行为和数据。根据当前的用户和授权信息,显示用户信息和可用的操作。

在Vue-router中的实现

我们可以通过路由守卫来实现。路由守卫可以在路由变化时对用户进行身份验证和权限控制,并根据用户的权限来决定是否允许访问受保护的路由。

在 Vue-router 中,我们可以使用 beforeEach 钩子函数来实现路由守卫。beforeEach 函数会在每次路由变化前被调用,并传入三个参数:to、from 和 next。to 表示将要跳转到的路由,from 表示当前的路由,next 是一个函数,可以用来控制路由的跳转。

我们可以在 beforeEach 函数中获取当前用户的角色和将要访问的路由的权限,然后根据这些信息来决定是否允许访问受保护的路由。例如,我们可以定义一个路由守卫函数如下:

const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
  const userRole = getUserRole() // 获取当前用户的角色
  const requiredPermission = to.meta.permission // 获取将要访问的路由的权限要求

  if (requiredPermission && !hasPermission(userRole, requiredPermission)) {
    // 如果用户没有权限访问该路由,则跳转到 403 页面
    next({ name: 'Forbidden' })
  } else {
    // 否则,允许访问该路由
    next()
  }
})

在这个例子中,我们首先获取当前用户的角色和将要访问的路由的权限要求。然后,我们使用 hasPermission 函数来判断当前用户是否有权限访问该路由。如果用户没有权限访问该路由,则跳转到 403 页面;否则,允许访问该路由。

在使用 Vue-router 实现 RBAC 时,还需要注意以下几点:

  • 将角色和权限信息存储在 Vuex 中,以方便在路由守卫函数中进行访问。
  • 在定义路由时,可以使用 meta 字段来存储路由的权限要求信息。
  • 在 Vue 组件中,可以通过 router和router 和 routerroute 对象来访问当前路由信息和路由跳转方法。

React 路由中的实现

我们可以通过路由守卫(route guard)来实现。路由守卫可以在路由变化时对用户进行身份验证和权限控制,并根据用户的权限来决定是否允许访问受保护的路由。

在 react-router 中,我们可以使用 Route 组件和 useHistory 钩子函数来实现路由守卫。Route 组件用来定义路由,useHistory 钩子函数用来获取路由信息和控制路由跳转。

我们可以在 Route 组件中使用 render 函数来定义路由守卫。例如,我们可以定义一个路由守卫函数如下:

javascriptCopy code
import { Route, useHistory } from 'react-router-dom'

function ProtectedRoute({ path, component: Component, permission }) {
  const history = useHistory()
  const userRole = getUserRole() // 获取当前用户的角色

  return (
    <Route
      path={path}
      render={() => {
        if (hasPermission(userRole, permission)) {
          return <Component />
        } else {
          history.push('/forbidden')
        }
      }}
    />
  )
}

在这个例子中,我们首先使用 useHistory 钩子函数获取 history 对象,然后获取当前用户的角色。然后,在 Route 组件的 render 函数中,我们使用 hasPermission 函数来判断当前用户是否有权限访问该路由。如果用户没有权限访问该路由,则使用 history.push 方法跳转到 403 页面。

在使用 react-router 实现 RBAC 时,还需要注意以下几点:

  • 将角色和权限信息存储在 Redux 中,以方便在路由守卫函数中进行访问。
  • 在定义路由时,可以使用 component 或 render 属性来定义路由组件和路由守卫函数。
  • 在 React 组件中,可以使用 useHistory 和 useLocation 钩子函数来访问当前路由信息和控制路由跳转。

总结

RBAC(Role-Based Access Control)是一种常见的权限管理模型,它通过对用户、角色和权限之间的关系进行定义和管理,来实现对系统资源的访问控制。

在实际开发中,我们通常需要在后端、前端和客户端中实现 RBAC。在后端,我们可以通过数据库设计和权限验证中间件来实现 RBAC;在前端和客户端,我们可以通过路由守卫和组件级权限控制来实现 RBAC。

无论是在哪个环节,RBAC 都需要定义和管理用户、角色和权限之间的关系,并对用户的访问进行授权。RBAC 模型的实现可以帮助我们保护系统的安全,保证用户只能访问他们被授权的资源,从而提高系统的可靠性和安全性。

转载自:https://juejin.cn/post/7231165433637650492
评论
请登录