Go语言企业级权限管理系统设计与实现

发布于:2025-08-16 ⋅ 阅读:(15) ⋅ 点赞:(0)

最近跟着学长再写河南师范大学附属中学图书馆的项目,学长交给了我一个任务,把本项目的权限管理给吃透,然后应用到下一个项目上。

我当然是偷着乐呐,因为读代码的时候,总是莫名给我一种公费旅游的感觉。
本来就想去了解图书管理这个项目的全貌。但一直腾不出时间。
现在正巧,我要写一个权限管理,正好可以拐回来细细品读图书管理系统的代码( ̄﹃ ̄)。

熟悉的配方,本项目使用的是RBAC模型来管理权限。

目录

一、RBAC

1、传统方案

2、如何通过RBAC改进?

3、如何设计代码?

二、中间件设计

1、理论设计方式

2、图书馆项目的设计

a.跳过重复路径

b.获取JWT凭证

可以拓展一下(AI):

三、基于图书馆的权限树设计

1、权限树设计

2、权限层级映射:

四、图书馆项目

1、整体架构:

2、核心组件解析

a、 用户信息结构 (UserInfo)

b、角色模型 (Role)

c、权限树结构 (Permission)

3、权限设计层级

a、三级权限结构:

五、前端如何进行权限控制

现实场景:

完整权限控制

首先从后端获取权限数据

第一层防护:菜单不显示

第二层防护:系统管理子菜单过滤

第三层防护:权限指令控制

第四层防护:组合式函数权限检查

第五层防护:直接URL访问

收获:


一、RBAC

为什么需要RBAC来管理项目的呢

大家可以想象这样一个场景:

想象你是一所大学图书馆的IT负责人。新学期开始了,
图书馆迎来了以下用户:

学生小王:只想借书还书,查看自己的借阅记录
老师张三:除了借书,还需要帮学生查询图书,管理班级借阅情况
管理员李四:需要添加新书、管理用户账号、查看所有借阅统计
系统管理员王五:拥有系统的完全控制权,包括备份数据、修改系统配置

假设没有权限管理,可能会发生什么事情呢?

学生小王误点了"删除所有图书"按钮
老师张三想查看其他班级的借阅情况被拒绝了
管理员李四无法访问系统设置,找你求助
......

所以权限管理,是非常必要的!!

现在问题来了:在咱们项目中,如何为他们添加权限?

1、传统方案

传统的解决方式是什么?在代码里写死:

// 传统方式:硬编码权限检查
func DeleteBook(userType string) {
    if userType == "student" {
        return // 学生不能删除
    }
    if userType == "teacher" {
        return // 老师也不能删除
    }
    if userType == "admin" {
        // 只有管理员能删除
        deleteBook()
    }
}

每次有新角色加入,你都要修改代码...
这样写有什么问题?
1. 新增一个"图书管理员"角色,要改遍所有函数
2. 权限规则散落在各处,难以维护
3. 想临时给某个老师管理员权限?改代码重新部署!

我在面向对象的七大设计原则一文中提到,接口的设计中的开闭原则中的,闭原则,就是为了解决解决每次有新改动,就要修改原有的代码。

所以直接把代码写死,极其不合理,那该如何解决?

2、如何通过RBAC改进?

什么是RBAC呢?

大家可以想象到这样一种场景:

公司的门禁卡系统
   - 员工卡:只能进办公区
   - 管理卡:能进办公区+会议室
   - 主管卡:能进所有区域

这就是灵感:能不能给用户分配"权限卡"?

咱们可以这样设计系统:

用户(User) ←→ 角色(Role) ←→ 权限(Permission)

这里的角色相当于上方公司的门禁卡

具体来说:
- 小王 → 学生角色 → [借书, 还书, 查看个人记录]
- 张三 → 教师角色 → [借书, 还书, 查看班级记录, 推荐图书]
- 李四 → 管理员角色 → [所有学生权限 + 添加图书 + 用户管理]

3、如何设计代码?

第一步:定义角色权限

// 不再硬编码,而是用数据库存储
1、定义角色
type Role struct {
    ID          uint   `json:"id"`
    Name        string `json:"name"`        // "学生", "教师", "管理员"
    Description string `json:"description"` // "普通学生用户"
}

2、定义权限
type Permission struct {
    ID     uint   `json:"id"`
    Name   string `json:"name"`   // "book:borrow", "user:create"
    Action string `json:"action"` // "借阅图书", "创建用户"
}

第二步:新方式如何检查权限

// 现在的权限检查
func DeleteBook(userID uint) error {
    if !permission.HasPermission(userID, "book:delete") {
        return errors.New("权限不足:您无法删除图书")
    }
    return deleteBook()
}
 新增角色?只需要配置数据,无需改代码!
// 2. 权限检查 - 如何工作的?
func (r *RoleService) HasPermission(roleID uint, permission string) bool {
    // 具体的权限验证逻辑
    // 为什么这样设计?
}

第三步:前后对比

// 传统方式:硬编码权限
if userType == "teacher" {
    // 教师相关操作
} else if userType == "student" {
    // 学生相关操作
}
// 问题:新增角色需要修改代码

// 你的方案:动态权限
if permission.HasRole(user.RoleID, "teacher") {
    // 教师相关操作
}
// 优势:新增角色只需要配置数据

哈哈,这就像及了接口(interface)的设计方式。
让人感觉赏心悦目。

二、中间件设计

虽然在引入RBAC后,确实能优化代码,但是又遇到了新问题。

每个API都要手动检查权限,代码重复。

如下,这里是调用DeleteBook,需要permission认证

// 现在的权限检查
func DeleteBook(userID uint) error {
    if !permission.HasPermission(userID, "book:delete") {
        return errors.New("权限不足:您无法删除图书")
    }
    return deleteBook()
}

如果咱们调用其他不同的函数,你都需要在每个函数上添加如下这段代码:

 if !permission.HasPermission(userID, "book:delete") {
        return errors.New("权限不足:您无法删除图书")
    }

是不是特别麻烦(~ ̄▽ ̄)~

1、理论设计方式

咱们可以设计成如下,通过middleware中间件:

// 展示如何在路由中应用权限中间件
router.POST("/role", middleware.RequirePermission("role:create"), roleHandler.CreateRole)
router.GET("/role", middleware.RequirePermission("role:read"), roleHandler.GetRoles)

2、图书馆项目的设计


// - 1.  路径跳过检查 - 检查当前请求路径是否在跳过列表中,如果是则直接放行
// - 2. 获取用户信息 - 从请求头 X-Userinfo 中获取 Base64 编码的用户信息
// - 3.  解码和反序列化 - 将 Base64 字符串解码后,反序列化为 UserInfo 结构体
// - 4. 租户ID处理 - 清理租户ID格式(移除前导斜杠),从请求头获取目标租户ID
// - 5. 权限验证 - 检查用户是否有权限访问请求的租户(用户的租户列表中是否包含目标租户)
// - 6. 设置上下文 - 验证通过后,将用户信息和租户ID设置到 Gin 上下文中供后续使用
func Auth() gin.HandlerFunc {
	return AuthWithConfig(AuthConfig{})
}

func AuthWithConfig(config AuthConfig) gin.HandlerFunc {
	notAuth := config.SkipPaths
	var skip map[string]struct{}
	if len(notAuth) > 0 {
		skip = make(map[string]struct{})
		for _, path := range notAuth {
			skip[path] = struct{}{}
		}
	}

	return func(c *gin.Context) {
		if _, ok := skip[c.FullPath()]; ok {
			c.Next()
			return
		}

		userInfos := c.Request.Header.Get("X-Userinfo")
		if userInfos == "" {
			err := errs.NewUnauthorizedError("missing user information")
			response.BuildErrorResponse(err, c)
			c.Abort()
			return
		}
		userProfile := &userModel.UserInfo{}
		user, err := base64.StdEncoding.DecodeString(userInfos)
		if err != nil {
			logrus.Error("x-userinfo base64 decoding failed", err)
			err = errs.NewUnauthorizedError("invalid user info encoding")
			response.BuildErrorResponse(err, c)
			c.Abort()
			return
		}

		err = json.Unmarshal(user, &userProfile)
		if err != nil {
			logrus.Error("x-userinfo json unmarshal failed", err)
			err = errs.NewUnauthorizedError("invalid user info format")
			response.BuildErrorResponse(err, c)
			c.Abort()
			return
		}

		// Remove the leading slash from each tenant ID
		for i, tenantId := range userProfile.TenantIds {
			if len(tenantId) > 0 && tenantId[0] == '/' {
				userProfile.TenantIds[i] = tenantId[1:]
			}
		}

		// Get tenantId from request header
		requestTenantId := c.GetHeader("tenantId")
		c.Set("tenantId", requestTenantId)
		if requestTenantId == "" && len(userProfile.TenantIds) > 0 {
			requestTenantId = userProfile.TenantIds[0]
			c.Set("tenantId", requestTenantId)
		}
		if requestTenantId == "" {
			logrus.Error("Missing tenantId in request header")
			err = errs.NewUnauthorizedError("missing tenantId in request header")
			response.BuildErrorResponse(err, c)
			c.Abort()
			return
		}

		// Check if the requested tenantId is in user's tenant list
		authorized := false
		for _, tenantId := range userProfile.TenantIds {
			if tenantId == requestTenantId {
				authorized = true
				break
			}
		}

		if !authorized {
			logrus.Warnf("User attempted to access unauthorized tenant: %s", requestTenantId)
			err = errs.NewUnauthorizedError("unauthorized tenant access")
			response.BuildErrorResponse(err, c)
			c.Abort()
			return
		}
		// Authorized, continue
		c.Set("user", userProfile)
		c.Next()
	}
}

咱们在这里详细解释一下代码:

a.跳过重复路径
// 从配置中获取,需要跳过的路径	
// 通过map存储实现O(1)查询
notAuth := config.SkipPaths
	var skip map[string]struct{}
	if len(notAuth) > 0 {
		skip = make(map[string]struct{})
		for _, path := range notAuth {
			skip[path] = struct{}{}
		}
	}
// 跳过
if _, ok := skip[c.FullPath()]; ok {
	c.Next()
	return
}
b.获取JWT凭证
// 1. 获取网关传递的用户信息       
 userInfos := c.Request.Header.Get("X-Userinfo")
		....

// 2. Base64解码
userProfile := &userModel.UserInfo{}
user, err := base64.StdEncoding.DecodeString(userInfos)
		....
    
// 3. JSON反序列化为用户对象
err = json.Unmarshal(user, &userProfile)
		....

// 4. 租户权限验证
        ....

认证的思路如下:

客户端 → 网关/认证服务 → 业务服务
       ↓
   JWT验证/登录
       ↓
   生成用户信息
       ↓
   Base64编码后放入Header
       ↓
   转发到后端服务

这里的采用的是第三方验证身份,并且采用Keycloak解决问题

Keycloak 是一个开源的身份和访问管理(IAM)解决方案

可以拓展一下(AI):

1.单点登录(SSO)

- 用户只需登录一次,即可访问多个应用系统
- 支持SAML 2.0、OpenID Connect、OAuth 2.0等标准协议
2.身份认证

- 用户名密码认证
- 多因素认证(MFA)
- 社交登录(Google、Facebook、GitHub等)
- LDAP/Active Directory集成
3.授权管理

- 基于角色的访问控制(RBAC)
- 细粒度权限控制
- 资源和策略管理
4.用户管理

- 用户注册、密码重置
- 用户组织和角色分配
- 用户会话管理

图书馆项目生成用于验证的JWT的方式

本项目JWT令牌的生成方式
通过对项目代码的深入分析,我发现本项目的JWT令牌生成采用了以下架构:

JWT令牌生成流程
1. Keycloak作为JWT令牌签发中心

- 项目使用 `keycloak.go` 中的 `GetAdminToken` 方法
- 通过调用 k.client.LoginAdmin() 向Keycloak服务器请求JWT令牌
- 使用配置文件中的管理员账户(AdminUser/AdminPass)进行认证
2. JWT令牌的具体生成过程

```
token, err := k.client.LoginAdmin(k.ctx, global.Config.Keycloak.
AdminUser, global.Config.Keycloak.AdminPass, "master")
```
3. 令牌使用场景

- 管理操作 :在用户创建、更新、删除等管理操作中使用
- 权限验证 :通过 `auth.go` 中间件验证用户身份
- API调用 :所有需要认证的API都通过JWT令牌进行权限控制

JWT是在创建角色的时候生成的,有兴趣的可以了解一下:

// CreateUser 在 Keycloak 中创建新用户
// 实现了完整的用户创建流程,包括权限分配和事务回滚
func (k *KeycloakService) CreateUser(req *UserCreateRequest) (string, error) {
    // 步骤1: 获取 Keycloak 管理员访问令牌
    token, err := k.GetAdminToken()
    if err != nil {
        logrus.Error(err)
        return "", err
    }
    
    // 步骤2: 检查用户名(身份证号)是否已存在
    exists, err := k.CheckUsernameExists(req.IdNumber)
    if err != nil {
        logrus.Error(err)
        return "", err
    }
    if exists {
        logrus.Errorf("User with idNumber %s already exists", req.IdNumber)
        return "", errors.NewResourceAlreadyExistError("身份证重复!")
    }
    
    // 步骤3: 设置默认密码(如果未提供)
    if len(req.Password) == 0 {
        req.Password = "Aa123456" // 建议:提取为配置项
    }
    
    // 步骤4: 构建 Keycloak 用户对象
    keycloakUser := gocloak.User{
        Username: gocloak.StringP(req.IdNumber),    // 使用身份证作为用户名
        Enabled:  gocloak.BoolP(true),              // 启用用户
        LastName: gocloak.StringP(req.Name),        // 设置姓名
        Credentials: &[]gocloak.CredentialRepresentation{
            {
                Type:      gocloak.StringP("password"),
                Value:     gocloak.StringP(req.Password),
                Temporary: gocloak.BoolP(false),        // 非临时密码
            },
        },
    }
    
    // 步骤5: 在 Keycloak 中创建用户
    userID, err := k.client.CreateUser(k.ctx, token.AccessToken, k.realm, keycloakUser)
    if err != nil {
        logrus.Errorf("Failed to create user %s in realm %s: %v", req.Name, k.realm, err)
        return "", err
    }
    
    // 步骤6: 添加用户到指定组(带事务回滚)
    err = k.AddUserToGroup(userID, req.GroupName)
    if err != nil {
        logrus.Errorf("Failed to add user %s to group %s: %v", userID, req.GroupName, err)
        // 回滚:删除已创建的用户
        if rollbackErr := k.DeleteUser(userID); rollbackErr != nil {
            logrus.Errorf("Rollback failed: %v", rollbackErr)
        }
        return "", err
    }
    
    // 步骤7: 为用户分配角色(带事务回滚)
    err = k.AddRoleToUser(userID, req.Role)
    if err != nil {
        logrus.Errorf("Failed to add role %s to user %s: %v", req.Role, userID, err)
        // 回滚:删除已创建的用户
        if rollbackErr := k.DeleteUser(userID); rollbackErr != nil {
            logrus.Errorf("Rollback failed: %v", rollbackErr)
        }
        return "", err
    }
    
    return userID, nil
}

三、基于图书馆的权限树设计

1、权限树设计


// Permission 权限结构体
type Permission struct {
	Key      string       `json:"key"`
	Title    string       `json:"title"`
	Children []Permission `json:"children,omitempty"`
}

// DefaultPermissions 默认权限树结构
var DefaultPermissions = []Permission{
	{
		Key:   "home",
		Title: "首页",
	},
	{
		Key:   "bookshelf",
		Title: "个人书架",
	},
	{
		Key:   "borrow-history",
		Title: "借阅记录",
	},
	{
		Key:   "activity-center",
		Title: "活动中心",
	},
	{
		Key:   "message-center",
		Title: "消息中心",
	},
	{
		Key:   "system-manage",
		Title: "系统管理",
		Children: []Permission{
			{
				Key:   "book-manage",
				Title: "图书管理",
				Children: []Permission{
					{Key: "book-entry", Title: "图书录入"},
					{Key: "book-list", Title: "图书列表"},
					{Key: "book-recommend", Title: "图书推荐"},
					{Key: "book-check", Title: "图书清查"},
				},
			},
			{
				Key:   "borrow-manage",
				Title: "借阅管理",
				Children: []Permission{
					{Key: "book-borrow", Title: "图书借阅"},
					{Key: "book-return", Title: "图书归还"},
					{Key: "flow-approve", Title: "漂流审批"},
					{Key: "reserve-list", Title: "候补列表"},
					{Key: "borrow-record", Title: "借阅记录"},
				},
			},
			{
				Key:   "activity-manage",
				Title: "活动管理",
				Children: []Permission{
					{Key: "activity-create", Title: "活动创建"},
					{Key: "activity-approve", Title: "活动审批"},
					{Key: "activity-list", Title: "活动列表"},
				},
			},
			{
				Key:   "notice-manage",
				Title: "通知管理",
				Children: []Permission{
					{Key: "notice-create", Title: "通知创建"},
					{Key: "notice-list", Title: "通知列表"},
				},
			},
			{
				Key:   "system-setting",
				Title: "系统设置",
				Children: []Permission{
					{Key: "user-manage", Title: "读者管理"},
					{Key: "role-manage", Title: "角色配置"},
					{Key: "system-configure", Title: "系统配置"},
					{Key: "grade-configure", Title: "年级配置"},
					{Key: "venue-configure", Title: "馆场地配置"},
					{Key: "activity-configure", Title: "活动配置"},
				},
			},
		},
	},
}

2、权限层级映射:

大白话来说就是能快速找到子节点父节点之间的关系


// BuildPermissionParentMap 从DefaultPermissions构建权限层级关系映射
func BuildPermissionParentMap() map[string]string {
	parentMap := make(map[string]string)
	buildParentMapRecursive(DefaultPermissions, "", parentMap)
	return parentMap
}

// buildParentMapRecursive 递归构建权限父子关系映射
// 能够快速找到子权限的父权限
func buildParentMapRecursive(permissions []Permission, parentKey string, parentMap map[string]string) {
	for _, perm := range permissions {
		if parentKey != "" {
			parentMap[perm.Key] = parentKey
		}
		if len(perm.Children) > 0 {
			buildParentMapRecursive(perm.Children, perm.Key, parentMap)
		}
	}
}

四、图书馆项目

1、整体架构:

用户(User) → 角色(Role) → 权限(Permission) → 资源(Resource)
     ↓           ↓           ↓              ↓
  身份认证    角色分配    权限控制      资源访问

2、核心组件解析

a、 用户信息结构 (UserInfo)
type UserInfo struct {
    Name        string   // 用户姓名
    Username    string   // 用户名
    AccountId   string   // 账户ID
    Roles       []string // 用户角色列表
    TenantIds   []string // 租户ID列表(多租户支持)
    // ... 其他字段
}
b、角色模型 (Role)
type Role struct {
    Name        string      // 角色名称
    Description string      // 角色描述
    BorrowLimit int         // 借阅数量限制
    BorrowDays  int         // 借阅天数限制
    TenantId    string      // 租户ID
    Permissions string      // 权限配置JSON
    Status      enum.Status // 状态
}
c、权限树结构 (Permission)
type Permission struct {
    Key      string       // 权限标识
    Title    string       // 权限名称
    Children []Permission // 子权限
}

3、权限设计层级

a、三级权限结构:

1. 一级权限 :模块级别(如:系统管理)
2.二级权限 :功能级别(如:图书管理)
3.三级权限 :操作级别(如:图书录入、图书列表)

系统管理 (system-manage)
├── 图书管理 (book-manage)
│   ├── 图书录入 (book-entry)
│   ├── 图书列表 (book-list)
│   └── 图书推荐 (book-recommend)
├── 借阅管理 (borrow-manage)
│   ├── 图书借阅 (book-borrow)
│   └── 图书归还 (book-return)
└── 系统设置 (system-setting)
    ├── 读者管理 (user-manage)
    └── 角色配置 (role-manage)

五、前端如何进行权限控制

现实场景:

假设:
一个普通读者(角色:student)
他的权限只有 ["home", "personal-bookshelf", "borrow-record", "activity-center", "message-center"] ,想要访问"读者管理"页面。

完整权限控制

用户登录
    ↓
后端返回用户权限列表: ["home", "personal-bookshelf", "borrow-record", "activity-center", "message-center"]
    ↓
前端存储权限到 Pinia Store
    ↓
菜单渲染时过滤权限
    ↓
系统管理菜单不显示(因为没有任何系统管理权限)
    ↓
用户无法通过正常途径访问读者管理页面
    ↓
即使通过直接URL访问,组件内部也会进行权限检查
    ↓
最终被拒绝访问或跳转到403页面
首先从后端获取权限数据

当用户登录后,前端会调用 `user.ts` 中的 fetchAndSetStaffInfo() 方法:

async fetchAndSetStaffInfo() {
  try {
    this.isLoading = true;
    const response = await getCurrentStaff(); // 调用后端API获取用户信息
    if (response && (response as any).data && (response as any).code === 0) {
      const staffData = (response as any).data;
      // 设置用户权限
      this.permissions = staffData.permissions || []; // 普通读者只有基础权限
    }
  } catch (error) {
    console.error('获取用户信息失败:', error);
  }
}

结果:
普通读者的 permissions 数组为: ["home", "personal-bookshelf", "borrow-record", "activity-center", "message-center"] , 不包含 "user-manage" 权限。

第一层防护:菜单不显示

在 `index.vue` 中,菜单会根据权限进行过滤:

const filterRoute = (routeList: TRouter[], currentPermissions: string[]) => {
  // 检查用户是否有系统管理权限
  const hasSystemManagePermission = systemManagePermissions.some((permission) =>
    currentPermissions.includes(permission),
  );

  for (let i = routeList.length - 1; i >= 0; i--) {
    const route = routeList[i];
    const routeName = route.name as string;
    
    // 特殊处理系统管理菜单
    if (routeName === 'SystemManage') {
      if (!hasSystemManagePermission) {
        routeList.splice(i, 1); // 移除系统管理菜单
      }
    }
  }
};

结果:
由于普通读者没有任何系统管理相关权限(如 user-manage 、 role-manage 等),整个"系统管理"菜单都不会显示在导航栏中。

第二层防护:系统管理子菜单过滤

即使用户通过某种方式进入了系统管理页面,在 `layout.vue` 中还有二级权限过滤:

// 菜单权限映射
const menuPermissionMap = {
  'user-manage': 'user-manage',
  'role-manage': 'role-manage',
  // ... 其他权限映射
};

// 根据权限过滤菜单组
const filteredMenuGroups = computed(() => {
  return menuGroups
    .map((group) => ({
      ...group,
      items: group.items.filter((item) => {
        const requiredPermission = menuPermissionMap[item.key];
        return !requiredPermission || permissions.value.includes(requiredPermission);
      }),
    }))
    .filter((group) => group.items.length > 0); // 过滤掉没有可用菜单项的组
});

结果:
"读者管理" 菜单项不会出现在系统管理的侧边栏中。

第三层防护:权限指令控制

在具体的页面组件中,还可以使用权限指令 `permission.ts` 来控制元素显示:

<!-- 在任何组件中使用权限指令 -->
<a-button v-permission="'user-manage'" type="primary">
  读者管理
</a-button>

权限指令的实现:

const permission: Directive = {
  mounted(el: HTMLElement, binding) {
    const { value } = binding;
    const user = useUserStore();
    const { permissions } = user;

    if (value) {
      let hasPermission = false;
      if (typeof value === 'string') {
        hasPermission = permissions.includes(value); // 检查是否有该权限
      }

      if (!hasPermission) {
        el.style.display = 'none'; // 没有权限则隐藏元素
      }
    }
  },
};

结果: 任何带有 v-permission="'user-manage'" 指令的元素都会被隐藏。

第四层防护:组合式函数权限检查

在组件逻辑中,可以使用 `usePermission.ts` 进行权限检查:

export function usePermission() {
  const user = useUserStore();

  // 检查是否有指定权限
  const hasPermission = (permission: string): boolean => {
    return user.hasPermission(permission);
  };

  return {
    hasPermission,
    // ... 其他权限检查方法
  };
}

在组件中使用:

<script setup>
import { usePermission } from '@/hooks/usePermission';

const { hasPermission } = usePermission();

// 检查权限
if (!hasPermission('user-manage')) {
  // 没有权限,执行相应逻辑
  router.push('/403'); // 跳转到无权限页面
}
</script>
第五层防护:直接URL访问

如果用户直接在浏览器地址栏输入 /systemManage/user-manage :

1、路由存在 :路由配置中确实有这个路径
2、组件加载 :UserManage 组件会被加载
3、权限检查 :组件内部会进行权限检查
4、访问被拒绝 :如果没有权限,会显示无权限提示或跳转到403页面

收获:

在学习权限控制的时候,由于我需要专门设计一套简单的权限控制,我专门找来我们的前端。
想要深入了解一下,我后端传递数据到前端后,前端进行的权限控制流程

浏览器上的页面是静态页面,当点击发送url时,会被前端拦截(Vue Router)的工作原理;
然后经过代码书写的一系列操作之后,在传递到后端,
后端返回的具体数据,是先返回到前端,
经前端处理,才最终到显示的页面。


网站:

1、活动广场 - 河南师范大学附属中学图书馆



网站公告

今日签到

点亮在社区的每一天
去签到