整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          《優(yōu)化接口設(shè)計(jì)的思路》系列:接口的權(quán)限控制

          前言
          一、權(quán)限底層表結(jié)構(gòu)設(shè)計(jì)
          ??1. RBAC模型簡(jiǎn)介
          ??2. 建表語句
          二、用戶身份認(rèn)證和授權(quán)
          ??1. 初始化數(shù)據(jù)
          ??2、新增/user/login接口模擬登錄
          ??3. 調(diào)用登錄接口
          三、用戶權(quán)限驗(yàn)證邏輯
          ??1. 定義接口權(quán)限注解
          ??2. 注解使用方式
          ??3. 接口驗(yàn)權(quán)的流程
          四、用戶權(quán)限變動(dòng)后的狀態(tài)刷新
          五、認(rèn)證失敗或無權(quán)限等異常情況處理
          寫在最后

          前言

          大家好!我是sum墨,一個(gè)一線的底層碼農(nóng),平時(shí)喜歡研究和思考一些技術(shù)相關(guān)的問題并整理成文,限于本人水平,如果文章和代碼有表述不當(dāng)之處,還請(qǐng)不吝賜教。

          作為一名從業(yè)已達(dá)六年的老碼農(nóng),我的工作主要是開發(fā)后端Java業(yè)務(wù)系統(tǒng),包括各種管理后臺(tái)和小程序等。在這些項(xiàng)目中,我設(shè)計(jì)過單/多租戶體系系統(tǒng),對(duì)接過許多開放平臺(tái),也搞過消息中心這類較為復(fù)雜的應(yīng)用,但幸運(yùn)的是,我至今還沒有遇到過線上系統(tǒng)由于代碼崩潰導(dǎo)致資損的情況。這其中的原因有三點(diǎn):一是業(yè)務(wù)系統(tǒng)本身并不復(fù)雜;二是我一直遵循某大廠代碼規(guī)約,在開發(fā)過程中盡可能按規(guī)約編寫代碼;三是經(jīng)過多年的開發(fā)經(jīng)驗(yàn)積累,我成為了一名熟練工,掌握了一些實(shí)用的技巧。

          我們?cè)谧鱿到y(tǒng)的時(shí)候,只要這個(gè)系統(tǒng)里面存在角色和權(quán)限相關(guān)的業(yè)務(wù)需求,那么接口的權(quán)限控制肯定必不可少。但是大家一搜接口權(quán)限相關(guān)的資料,出來的就是整合Shrio、Spring Security等各種框架,然后下面一頓貼配置和代碼,看得人云里霧里。實(shí)際上接口的權(quán)限控制是整個(gè)系統(tǒng)權(quán)限控制里面很小的一環(huán),沒有設(shè)計(jì)好底層數(shù)據(jù)結(jié)構(gòu),是無法做好接口的權(quán)限控制的。那么怎么做一個(gè)系統(tǒng)的權(quán)限控制呢?我認(rèn)為有以下幾步:

          那么接下來我就按這個(gè)流程一一給大家說明權(quán)限是怎么做出來的。(注:只需要SpringBoot和Redis,不需要額外權(quán)限框架。)

          本文參考項(xiàng)目源碼地址:summo-springboot-interface-demo

          一、權(quán)限底層表結(jié)構(gòu)設(shè)計(jì)

          第一,只要一個(gè)系統(tǒng)是給人用的,那么這個(gè)系統(tǒng)就一定會(huì)有一張用戶表;第二,只要有人的地方,就一定會(huì)有角色權(quán)限的劃分,最簡(jiǎn)單的就是超級(jí)管理員、普通用戶;第三,如此常見的設(shè)計(jì),會(huì)有一套相對(duì)規(guī)范的設(shè)計(jì)標(biāo)準(zhǔn)。
          權(quán)限底層表結(jié)構(gòu)設(shè)計(jì)的標(biāo)準(zhǔn)就是:RBAC模型

          1. RBAC模型簡(jiǎn)介

          RBAC(Role-Based Access Control)權(quán)限模型的概念,即:基于角色的權(quán)限控制。通過角色關(guān)聯(lián)用戶,角色關(guān)聯(lián)權(quán)限的方式間接賦予用戶權(quán)限。

          回到業(yè)務(wù)需求上來,應(yīng)該是下面這樣的要求:

          上圖可以看出,用戶 多對(duì)多 角色 多對(duì)多 權(quán)限

          用表結(jié)構(gòu)展示的話就是這樣,一共5張表,3張實(shí)體表,2張關(guān)聯(lián)表

          2. 建表語句

          (1) t_user

          DROP TABLE IF EXISTS `t_user`;
          CREATE TABLE `t_user` (
            `user_id` bigint(20) unsigned zerofill NOT NULL AUTO_INCREMENT COMMENT '用戶ID',
            `user_name` varchar(32) DEFAULT NULL COMMENT '用戶名稱',
            `gmt_create` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
            `gmt_modified` datetime DEFAULT NULL COMMENT '更新時(shí)間',
            `creator_id` bigint DEFAULT NULL COMMENT '創(chuàng)建人ID',
            `modifier_id` bigint DEFAULT NULL COMMENT '更新人ID',
            PRIMARY KEY (`user_id`)
          ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ;
          

          (2) t_role

          DROP TABLE IF EXISTS `t_role`;
          CREATE TABLE `t_role` (
            `role_id` bigint(20) unsigned zerofill NOT NULL AUTO_INCREMENT COMMENT '角色I(xiàn)D',
            `role_name` varchar(32) CHARACTER SET utf8mb4   DEFAULT NULL COMMENT '角色名稱',
            `role_code` varchar(32) CHARACTER SET utf8mb4   DEFAULT NULL COMMENT '角色code',
            `gmt_create` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
            `gmt_modified` datetime DEFAULT NULL COMMENT '更新時(shí)間',
            `creator_id` bigint DEFAULT NULL COMMENT '創(chuàng)建人ID',
            `modifier_id` bigint DEFAULT NULL COMMENT '更新人ID',
            PRIMARY KEY (`role_id`) USING BTREE
          ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
          

          (3) t_auth

          DROP TABLE IF EXISTS `t_auth`;
          CREATE TABLE `t_auth` (
            `auth_id` bigint(20) unsigned zerofill NOT NULL AUTO_INCREMENT COMMENT '權(quán)限ID',
            `auth_code` varchar(32) DEFAULT NULL COMMENT '權(quán)限code',
            `auth_name` varchar(32) CHARACTER SET utf8mb4  DEFAULT NULL COMMENT '權(quán)限名稱',
            `gmt_create` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
            `gmt_modified` datetime DEFAULT NULL COMMENT '更新時(shí)間',
            `creator_id` bigint DEFAULT NULL COMMENT '創(chuàng)建人ID',
            `modifier_id` bigint DEFAULT NULL COMMENT '更新人ID',
            PRIMARY KEY (`auth_id`) USING BTREE
          ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
          

          (4) t_user_role

          DROP TABLE IF EXISTS `t_user_role`;
          CREATE TABLE `t_user_role` (
            `id` bigint(20) unsigned zerofill NOT NULL AUTO_INCREMENT COMMENT '物理ID',
            `user_id` bigint NOT NULL COMMENT '用戶ID',
            `role_id` bigint NOT NULL COMMENT '角色I(xiàn)D',
            `gmt_create` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
            `gmt_modified` datetime DEFAULT NULL COMMENT '更新時(shí)間',
            `creator_id` bigint DEFAULT NULL COMMENT '創(chuàng)建人ID',
            `modifier_id` bigint DEFAULT NULL COMMENT '更新人ID',
             PRIMARY KEY (`id`) USING BTREE
          ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
          

          (5) t_role_auth

          DROP TABLE IF EXISTS `t_role_auth`;
          CREATE TABLE `t_role_auth` (
           `id` bigint(20) unsigned zerofill NOT NULL AUTO_INCREMENT COMMENT '物理ID',
            `role_id` bigint DEFAULT NULL COMMENT '角色I(xiàn)D',
            `auth_id` bigint DEFAULT NULL COMMENT '權(quán)限ID',
            `gmt_create` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
            `gmt_modified` datetime DEFAULT NULL COMMENT '更新時(shí)間',
            `creator_id` bigint DEFAULT NULL COMMENT '創(chuàng)建人ID',
            `modifier_id` bigint DEFAULT NULL COMMENT '更新人ID',
             PRIMARY KEY (`id`) USING BTREE
          ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
          

          二、用戶身份認(rèn)證和授權(quán)

          上面已經(jīng)把表設(shè)計(jì)好了,接下來就是代碼開發(fā)了。不過,在開發(fā)之前我們要搞清楚認(rèn)證授權(quán)這兩個(gè)詞是啥意思。

          • 什么是認(rèn)證?
            認(rèn)證是確認(rèn)一個(gè)用戶的身份,確保用戶是其所聲稱的人。它通過驗(yàn)證用戶的身份信息,例如用戶名和密碼,來確認(rèn)用戶的身份。
          • 什么是授權(quán)?
            授權(quán)是根據(jù)用戶的身份和權(quán)限,給予用戶特定的訪問權(quán)限或使用某些資源的權(quán)力。它確定用戶可以執(zhí)行的操作,并限制他們不能執(zhí)行的操作。授權(quán)確保用戶只能訪問他們被允許的內(nèi)容和功能。

          光看定義也很難懂,這里我舉個(gè)例子配合說明。

          現(xiàn)有兩個(gè)用戶:小A和小B;兩個(gè)角色:管理員和普通用戶;4個(gè)操作:新增/刪除/修改/查詢。圖例如下:


          那么,對(duì)于小A來說,
          認(rèn)證就是小A登錄系統(tǒng)后,會(huì)授予管理員的角色,授權(quán)就是授予小A新增/刪除/修改/查詢的權(quán)限;
          同理,對(duì)于小B來說,
          認(rèn)證就是小B登錄系統(tǒng)后,會(huì)授予普通用戶的角色,授權(quán)就是授予小B查詢的權(quán)限。

          接下來且看如何實(shí)現(xiàn)

          1. 初始化數(shù)據(jù)

          t_user表數(shù)據(jù)

          INSERT INTO `t_user` (`user_id`, `user_name`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (1, '小A', '2023-09-21 09:48:14', '2023-09-21 09:48:19', -1, -1);
          INSERT INTO `t_user` (`user_id`, `user_name`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (2, '小B', '2023-09-21 09:48:14', '2023-09-21 09:48:19', -1, -1);
          

          t_role表數(shù)據(jù)

          INSERT INTO `t_role` (`role_id`, `role_name`, `role_code`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (1, '管理員', 'admin', '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
          INSERT INTO `t_role` (`role_id`, `role_name`, `role_code`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (2, '普通用戶', 'normal', '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
          

          t_auth表數(shù)據(jù)

          INSERT INTO `t_auth` (`auth_id`, `auth_code`, `auth_name`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (1, 'add', '新增', '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
          INSERT INTO `t_auth` (`auth_id`, `auth_code`, `auth_name`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (2, 'delete', '刪除', '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
          INSERT INTO `t_auth` (`auth_id`, `auth_code`, `auth_name`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (3, 'query', '查詢', '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
          INSERT INTO `t_auth` (`auth_id`, `auth_code`, `auth_name`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (4, 'update', '更新', '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
          

          t_user_role表數(shù)據(jù)

          INSERT INTO `t_user_role` (`user_id`, `role_id`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (1, 1, '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
          INSERT INTO `t_user_role` (`user_id`, `role_id`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (2, 2, '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
          

          t_role_auth表數(shù)據(jù)

          INSERT INTO `t_role_auth` (`role_id`, `auth_id`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (1, 2, '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
          INSERT INTO `t_role_auth` (`role_id`, `auth_id`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (1, 1, '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
          INSERT INTO `t_role_auth` (`role_id`, `auth_id`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (1, 3, '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
          INSERT INTO `t_role_auth` (`role_id`, `auth_id`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (1, 4, '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
          INSERT INTO `t_role_auth` (`role_id`, `auth_id`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (2, 3, '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
          

          2、新增/user/login接口模擬登錄

          接口代碼如下

          @GetMapping("/login")
          public ResponseEntity<String> userLogin(@RequestParam(required = true) String userName,
                  HttpServletRequest httpServletRequest,
                  HttpServletResponse httpServletResponse) {
            return userService.login(userName, httpServletRequest, httpServletResponse);
          }
          

          業(yè)務(wù)代碼如下

          @Override
          public ResponseEntity<String> login(String userName, HttpServletRequest httpServletRequest,
                  HttpServletResponse httpServletResponse) {
            //根據(jù)名稱查詢用戶信息
            UserDO userDO = userMapper.selectOne(new QueryWrapper<UserDO>().lambda().eq(UserDO::getUserName, userName));
            if (Objects.isNull(userDO)) {
              return ResponseEntity.ok("未查詢到用戶");
            }
            //查詢當(dāng)前用戶的角色信息
            List<UserRoleDO> userRoleDOList = userRoleMapper.selectList(
                      new QueryWrapper<UserRoleDO>().lambda().eq(UserRoleDO::getUserId, userDO.getUserId()));
            if (CollectionUtils.isEmpty(userRoleDOList)) {
              return ResponseEntity.ok("當(dāng)前用戶沒有角色");
            }
            //查詢當(dāng)前用戶的權(quán)限
            List<RoleAuthDO> roleAuthDOS = roleAuthMapper.selectList(new QueryWrapper<RoleAuthDO>().lambda()
                      .in(RoleAuthDO::getRoleId, userRoleDOList.stream().map(UserRoleDO::getRoleId).collect(
                          Collectors.toList())));
            if (CollectionUtils.isEmpty(roleAuthDOS)) {
              return ResponseEntity.ok("當(dāng)前角色沒有對(duì)應(yīng)權(quán)限");
            }
            //查詢權(quán)限code
            List<AuthDO> authDOS = authMapper.selectList(new QueryWrapper<AuthDO>().lambda()
                      .in(AuthDO::getAuthId, roleAuthDOS.stream().map(RoleAuthDO::getAuthId).collect(
                          Collectors.toList())));
          
            //生成唯一token
            String token = UUID.randomUUID().toString();
            //緩存用戶信息
            redisUtil.set(token, JSONObject.toJSONString(userDO), tokenTimeout);
            //緩存用戶權(quán)限信息
            redisUtil.set("auth_" + userDO.getUserId(),
                      JSONObject.toJSONString(authDOS.stream().map(AuthDO::getAuthCode).collect(Collectors.toList())),
                      tokenTimeout);
            //向localhost中添加Cookie
            Cookie cookie = new Cookie("token", token);
            cookie.setDomain("localhost");
            cookie.setPath("/");
            cookie.setMaxAge(tokenTimeout.intValue());
            httpServletResponse.addCookie(cookie);
            //返回登錄成功
            return ResponseEntity.ok(JSONObject.toJSONString(userDO));
          }
          

          上面代碼用流程圖表示如下

          3. 調(diào)用登錄接口

          小A登錄:http://localhost:8080/user/login?userName=小A
          小B登錄:http://localhost:8080/user/login?userName=小B

          (沒畫前端界面,大家將就看下哈)

          小A登錄調(diào)用返回如下

          小B登錄調(diào)用返回如下

          三、用戶權(quán)限驗(yàn)證邏輯

          通過第二步,用戶已經(jīng)進(jìn)行了認(rèn)證、授權(quán)的操作,那么接下來就是用戶驗(yàn)權(quán):即驗(yàn)證用戶是否有調(diào)用接口的權(quán)限。

          1. 定義接口權(quán)限注解

          前面定義了4個(gè)權(quán)限:新增/刪除/修改/查詢,分別對(duì)應(yīng)著4個(gè)接口。這里我們使用注解進(jìn)行一一對(duì)應(yīng)。
          注解定義如下:
          RequiresPermissions.java

          package com.summo.demo.config.permissions;
          
          import java.lang.annotation.ElementType;
          import java.lang.annotation.Retention;
          import java.lang.annotation.RetentionPolicy;
          import java.lang.annotation.Target;
          
          @Target({ElementType.TYPE, ElementType.METHOD})
          @Retention(RetentionPolicy.RUNTIME)
          public @interface RequiresPermissions {
              /**
               * 權(quán)限列表
               * @return
               */
              String[] value();
          
              /**
               * 權(quán)限控制方式,且或者和
               * @return
               */
              Logical logical() default Logical.AND;
          
          }
          

          該注解有兩個(gè)屬性,value和logical。value是一個(gè)數(shù)組,代表當(dāng)前接口擁有哪些權(quán)限;logical有兩個(gè)值A(chǔ)ND和OR,AND的意思是當(dāng)前用戶必須要有value中所有的權(quán)限才可以調(diào)用該接口,OR的意思是當(dāng)前用戶只需要有value中任意一個(gè)權(quán)限就可以調(diào)用該接口。

          注解處理代碼邏輯如下:
          RequiresPermissionsHandler.java

          package com.summo.demo.config.permissions;
          
          import java.lang.reflect.Method;
          import java.text.MessageFormat;
          import java.util.ArrayList;
          import java.util.Arrays;
          import java.util.List;
          import java.util.Objects;
          import java.util.Set;
          import java.util.stream.Collectors;
          
          import com.alibaba.fastjson.JSONObject;
          
          import com.summo.demo.config.context.GlobalUserContext;
          import com.summo.demo.config.context.UserContext;
          import com.summo.demo.config.manager.UserManager;
          import org.aspectj.lang.ProceedingJoinPoint;
          import org.aspectj.lang.annotation.Around;
          import org.aspectj.lang.annotation.Aspect;
          import org.aspectj.lang.annotation.Pointcut;
          import org.aspectj.lang.reflect.MethodSignature;
          import org.springframework.beans.factory.annotation.Autowired;
          import org.springframework.stereotype.Component;
          
          @Aspect
          @Component
          public class RequiresPermissionsHandler {
          
              @Autowired
              private UserManager userManager;
          
              @Pointcut("@annotation(com.summo.demo.config.permissions.RequiresPermissions)")
              public void pointcut() {
                  // do nothing
              }
          
              @Around("pointcut()")
              public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
                  //獲取用戶上下文
                  UserContext userContext = GlobalUserContext.getUserContext();
                  if (Objects.isNull(userContext)) {
                      throw new RuntimeException("用戶認(rèn)證失敗,請(qǐng)檢查是否登錄");
                  }
                  //獲取注解
                  MethodSignature signature = (MethodSignature)joinPoint.getSignature();
                  Method method = signature.getMethod();
                  RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);
                  //獲取當(dāng)前接口上數(shù)據(jù)權(quán)限
                  String[] permissions = requiresPermissions.value();
                  if (Objects.isNull(permissions) && permissions.length == 0) {
                      throw new RuntimeException("用戶認(rèn)證失敗,請(qǐng)檢查該接口是否添加了數(shù)據(jù)權(quán)限");
                  }
                  //判斷當(dāng)前是and還是or
                  String[] notHasPermissions;
                  switch (requiresPermissions.logical()) {
                      case AND:
                          //當(dāng)邏輯為and時(shí),所有的數(shù)據(jù)權(quán)限必須存在
                          notHasPermissions = checkPermissionsByAnd(userContext.getUserId(), permissions);
                          if (Objects.nonNull(notHasPermissions) && notHasPermissions.length > 0) {
                              throw new RuntimeException(
                                  MessageFormat.format("用戶權(quán)限不足,缺失以下權(quán)限:[{0}]", JSONObject.toJSONString(notHasPermissions)));
                          }
                          break;
                      case OR:
                          //當(dāng)邏輯為and時(shí),所有的數(shù)據(jù)權(quán)限必須存在
                          notHasPermissions = checkPermissionsByOr(userContext.getUserId(), permissions);
                          if (Objects.nonNull(notHasPermissions) && notHasPermissions.length > 0) {
                              throw new RuntimeException(
                                  MessageFormat.format("用戶權(quán)限不足,缺失以下權(quán)限:[{0}]", JSONObject.toJSONString(notHasPermissions)));
                          }
                          break;
                      default:
                          //默認(rèn)為and
                  }
                  return joinPoint.proceed();
              }
          
              /**
               * 當(dāng)數(shù)據(jù)權(quán)限為or時(shí),進(jìn)行判斷
               *
               * @param userId      用戶ID
               * @param permissions 權(quán)限組
               * @return 沒有授予的權(quán)限
               */
              private String[] checkPermissionsByOr(Long userId, String[] permissions) {
                  // 獲取用戶權(quán)限集
                  Set<String> permissionSet = userManager.queryAuthByUserId(userId);
                  if (permissionSet.isEmpty()) {
                      return permissions;
                  }
                  //一一比對(duì)
                  List<String> tempPermissions = new ArrayList<>();
                  for (String permission1 : permissions) {
                      permissionSet.forEach(permission -> {
                          if (permission1.equals(permission)) {
                              tempPermissions.add(permission);
                          }
                      });
                  }
                  if (Objects.nonNull(tempPermissions) && tempPermissions.size() > 0) {
                      return null;
                  }
                  return permissions;
              }
          
              /**
               * 當(dāng)數(shù)據(jù)權(quán)限為and時(shí),進(jìn)行判斷
               *
               * @param userId      用戶ID
               * @param permissions 權(quán)限組
               * @return 沒有授予的權(quán)限
               */
              private String[] checkPermissionsByAnd(Long userId, String[] permissions) {
                  // 獲取用戶權(quán)限集
                  Set<String> permissionSet = userManager.queryAuthByUserId(userId);
                  if (permissionSet.isEmpty()) {
                      return permissions;
                  }
                  //如果permissions大小為1,可以單獨(dú)處理一下
                  if (permissionSet.size() == 1 && permissionSet.contains(permissions[0])) {
                      return null;
                  }
                  if (permissionSet.size() == 1 && !permissionSet.contains(permissions[0])) {
                      return permissions;
                  }
                  //一一比對(duì)
                  List<String> tempPermissions = new ArrayList<>();
                  for (String permission1 : permissions) {
                      permissionSet.forEach(permission -> {
                          if (permission1.equals(permission)) {
                              tempPermissions.add(permission);
                          }
                      });
                  }
                  //如果tempPermissions的長(zhǎng)度與permissions相同,那么說明權(quán)限吻合
                  if (permissions.length == tempPermissions.size()) {
                      return null;
                  }
                  //否則取出當(dāng)前用戶沒有的權(quán)限,并返回用作提示
                  List<String> notHasPermissions = Arrays.stream(permissions).filter(
                      permission -> !tempPermissions.contains(permission)).collect(Collectors.toList());
                  return notHasPermissions.toArray(new String[notHasPermissions.size()]);
              }
          
          }
          

          2. 注解使用方式

          使用比較簡(jiǎn)單,直接放到接口的方法上

          @GetMapping("/add")
          @RequiresPermissions(value = "add", logical = Logical.OR)
          public ResponseEntity<String> add(@RequestBody AddReq addReq) {
            return userService.add(addReq);
          }
          
          @GetMapping("/delete")
          @RequiresPermissions(value = "delete", logical = Logical.OR)
          public ResponseEntity<String> delete(@RequestParam Long userId) {
            return userService.delete(userId);
          }
          
          @GetMapping("/query")
          @RequiresPermissions(value = "query", logical = Logical.OR)
          public ResponseEntity<String> query(@RequestParam String userName) {
            return userService.query(userName);
          }
          
          @GetMapping("/update")
          @RequiresPermissions(value = "update", logical = Logical.OR)
          public ResponseEntity<String> update(@RequestBody UpdateReq updateReq) {
            return userService.update(updateReq);
          }
          
          

          3. 接口驗(yàn)權(quán)的流程

          四、用戶權(quán)限變動(dòng)后的狀態(tài)刷新

          其實(shí)前面三步完成后,正向流已經(jīng)完成了,但用戶的權(quán)限是變化的,比如:

          小B的權(quán)限從查詢變?yōu)榱?/span>查詢更新


          但小B的token還未過期,這時(shí)應(yīng)該怎么辦呢?

          還記得登錄的時(shí)候,我有緩存兩個(gè)信息嗎

          對(duì)應(yīng)代碼中的

          //緩存用戶信息
          redisUtil.set(token, JSONObject.toJSONString(userDO), tokenTimeout);
          //緩存用戶權(quán)限信息
          redisUtil.set("auth_" + userDO.getUserId(),JSONObject.toJSONString(authDOS.stream().map(AuthDO::getAuthCode).collect(Collectors.toList())),tokenTimeout);
          

          在這里我其實(shí)將token和權(quán)限是分開存儲(chǔ)的,token只存用戶信息,而權(quán)限信息用auth_userId為key進(jìn)行存儲(chǔ)的,這樣就可以做到即使token還在,我也能動(dòng)態(tài)修改當(dāng)前用戶的權(quán)限信息了,且權(quán)限實(shí)時(shí)變更不會(huì)影響用戶體驗(yàn)。

          不過,這個(gè)地方有一個(gè)爭(zhēng)議的點(diǎn)
          用戶權(quán)限發(fā)生變更的時(shí)候,是更新權(quán)限緩存呢?還是直接刪除用戶的權(quán)限緩存呢?

          我的建議是:刪除權(quán)限緩存。原因有三

          • 用戶權(quán)限緩存并不是一直存在,存在連緩存都沒有的情況。
          • 緩存更新只適用于單個(gè)用戶權(quán)限的更新,但是我要把角色和權(quán)限的關(guān)聯(lián)變動(dòng)了呢?
          • 直接把權(quán)限緩存刪除,用戶會(huì)不會(huì)報(bào)錯(cuò)?我查詢權(quán)限緩存的方式是:先查詢緩存,緩存沒有在查詢數(shù)據(jù)庫,所以并不會(huì)出現(xiàn)緩存被刪除就報(bào)錯(cuò)的情況。

          tips:如何優(yōu)雅的實(shí)現(xiàn)“先查詢緩存再查詢數(shù)據(jù)庫?”請(qǐng)看我這篇文章:https://juejin.cn/post/7124885941117779998

          五、認(rèn)證失敗或無權(quán)限等異常情況處理

          出現(xiàn)由于權(quán)限不足或認(rèn)證失敗的問題,常見的做法有重定向到登錄頁、通知用戶刷新界面等,具體怎么處理還要看產(chǎn)品是怎么要求的。
          關(guān)于網(wǎng)站的異常有很多,權(quán)限相關(guān)的狀態(tài)碼是401、服務(wù)器錯(cuò)誤的狀態(tài)碼是500,除此之外還會(huì)有自定義的錯(cuò)誤碼,我打算放在接口優(yōu)化系列的后面用專篇說明,敬請(qǐng)期待哦~

          寫在最后

          《優(yōu)化接口設(shè)計(jì)的思路》系列已結(jié)寫到第四篇了,前面幾篇都沒有總結(jié),在這篇總結(jié)一下吧。

          從我開始寫博客到現(xiàn)在已經(jīng)6年了,差不多也寫了將近60篇左右的文章。剛開始的時(shí)候就是寫SpringBoot,寫SpringBoot如何整合Vue,那是2017年。

          得益于老大的要求(或者是公司想省錢),剛工作的時(shí)候就是前后端代碼都寫,但是寫的一塌糊涂,甚至連最基礎(chǔ)的項(xiàng)目環(huán)境都搭不好。那時(shí)候在網(wǎng)上找個(gè)pom.xml配置,依賴死活下載不下來,后來才知道m(xù)aven倉庫默認(rèn)國外的源,要把它換成國內(nèi)的才能提高下載速度。那時(shí)候上班就是下午把項(xiàng)目跑起來了,第二天上午項(xiàng)目又啟動(dòng)不了了,如此循環(huán)往復(fù),我的筆記里面存了非常多的配置文件。再后來技術(shù)水平提高了點(diǎn),單項(xiàng)目終于會(huì)玩了,微服務(wù)又火起來了,了解過SpringCloud的小伙伴應(yīng)該知道SpringCloud的版本更復(fù)雜,搭建環(huán)境更難。在這可能有人會(huì)疑惑,你不會(huì)不能去問人嗎?我也很無奈,一則是社恐不敢問,二則是我們部門全是菜鳥,都等著我學(xué)會(huì)教他們呢...

          后來我老大說,既然用不來人家的,那就自己寫一套,想起來那時(shí)真單純,我就真的自己開始寫微服務(wù)架構(gòu)。最開始我對(duì)微服務(wù)的唯一印象就是一個(gè)服務(wù)提供者、一個(gè)服務(wù)消費(fèi)者,肯定是兩個(gè)應(yīng)用,至于為啥是這樣,查的百度都是這樣寫的。然后我就建了兩個(gè)應(yīng)用,一個(gè)網(wǎng)關(guān)應(yīng)用、一個(gè)業(yè)務(wù)應(yīng)用,自己寫HttpUtil進(jìn)行服務(wù)間調(diào)用,也不知道啥是注冊(cè)中心,我只知道網(wǎng)關(guān)應(yīng)用那里要有業(yè)務(wù)應(yīng)用的IP地址,否則網(wǎng)關(guān)調(diào)不了業(yè)務(wù)代碼。當(dāng)時(shí)的調(diào)用代碼我已經(jīng)找不了,只記得當(dāng)時(shí)代碼的形狀很像一個(gè)“>”,用了太多的if...else...了!!!

          那時(shí)候雖然代碼寫的很爛、bug一堆,但我們老大也沒罵我們,每周四還會(huì)給我們上夜校,跟我們講一些大廠的框架和技術(shù)棧。他跟我們說,現(xiàn)在多用用人家的技術(shù),到時(shí)候出去面試大廠也容易一些。寫博文也是老大讓我們做的,他說現(xiàn)在一點(diǎn)點(diǎn)的積累,等到過幾年就會(huì)變成文庫了。現(xiàn)在想來,真是一個(gè)不錯(cuò)的老大!

          現(xiàn)在2023年了,我還在寫代碼,但也不僅僅只是寫代碼,還帶一些項(xiàng)目,獨(dú)立負(fù)責(zé)的也有。要說我現(xiàn)在的代碼水平嘛,屬于那種工廠熟練工水平,八股里面的什么JVM調(diào)優(yōu)啊、高并發(fā)系統(tǒng)架構(gòu)設(shè)計(jì)啊我一次都沒有接觸到過,遠(yuǎn)遠(yuǎn)稱不上大神。不過我還是想寫一些文章,不是為了炫技,只是想把我工作中遇到的問題變成后續(xù)解決問題的經(jīng)驗(yàn),說真的這些文章已經(jīng)開始幫到我了,如果它們也能幫助到你,榮幸之至!

          原文鏈接:https://www.cnblogs.com/wlovet/p/17717905.html



          ntro#

          由于項(xiàng)目需要,需要在基于 asp.net mvc 的 Web 項(xiàng)目框架中做權(quán)限的控制,于是才有了這個(gè)權(quán)限控制組件,最初只是支持 netframework,后來 dotnetcore 2.0 發(fā)布了之后添加了對(duì) asp.net core 的支持,在 dotnetcore 3.0 發(fā)布之后也增加了對(duì) asp.net core 3.0 的支持(1.9.0及之后版本),目前對(duì)于 asp.net core 支持的更多一些,asp.net core 可以使用 TagHelper 來控制頁面上元素的權(quán)限訪問,也可以通過 Policy 來控制權(quán)限訪問,同時(shí)支持通過中間件也可以實(shí)現(xiàn)對(duì)靜態(tài)資源的訪問。

          安裝 AccessControlHelper nuget 包#

          安裝 nuget 包 WeihanLi.AspNetMvc.AccessControlHelper

          Copydotnet add package WeihanLi.AspNetMvc.AccessControlHelper
          

          實(shí)現(xiàn)自己的訪問策略#

          資源訪問策略/API訪問策略#

          以下代碼定義了一個(gè)簡(jiǎn)單的訪問策略,需要登錄且擁有 Admin 角色,可以根據(jù)自己需要調(diào)整優(yōu)化

          Copypublic class AdminPermissionRequireStrategy : IResourceAccessStrategy
          {
              private readonly IHttpContextAccessor _accessor;
          
              public AdminPermissionRequireStrategy(IHttpContextAccessor accessor)
              {
                  _accessor = accessor;
              }
          
              public bool IsCanAccess(string accessKey)
              {
                  var user = _accessor.HttpContext.User;
                  return user.Identity.IsAuthenticated && user.IsInRole("Admin");
              }
          
              public IActionResult DisallowedCommonResult => new ContentResult
              {
                  Content = "No Permission",
                  ContentType = "text/plain",
                  StatusCode = 403
              };
          
              public IActionResult DisallowedAjaxResult => new JsonResult(new JsonResultModel
              {
                  ErrorMsg = "No Permission",
                  Status = JsonResultStatus.NoPermission
              });
          }
          

          頁面元素訪問策略#

          定義頁面元素/控件訪問策略:

          Copypublic class AdminOnlyControlAccessStrategy : IControlAccessStrategy
          {
              private readonly IHttpContextAccessor _accessor;
          
              public AdminOnlyControlAccessStrategy(IHttpContextAccessor httpContextAccessor) => _accessor = httpContextAccessor;
          
              public bool IsControlCanAccess(string accessKey)
              {
                  if ("Never".Equals(accessKey, System.StringComparison.OrdinalIgnoreCase))
                  {
                      return false;
                  }
                  var user = _accessor.HttpContext.User;
                  return user.Identity.IsAuthenticated && user.IsInRole("Admin");
              }
          }
          

          服務(wù)注冊(cè)配置#

          在 Startup 里注冊(cè)服務(wù):

          Copyservices.AddAccessControlHelper()
              .AddResourceAccessStrategy<AdminPermissionRequireStrategy>()
              .AddControlAccessStrategy<AdminOnlyControlAccessStrategy>()
              ;
          

          如果你只是 web api ,不涉及到頁面元素的權(quán)限控制可以只注冊(cè) ResourceAccessStrategy

          Copyservices.AddAccessControlHelper()
          .AddResourceAccessStrategy<AdminPermissionRequireStrategy>();
          

          默認(rèn)訪問策略的生命周期是單例的,如果需要注冊(cè)為Scoped,可以指定默認(rèn)的生命周期

          Copyservices.AddAccessControlHelper()
          .AddResourceAccessStrategy<AdminPermissionRequireStrategy>(ServiceLifetime.Scoped);
          

          API/資源的權(quán)限控制#

          對(duì)于 asp.net core 應(yīng)用推薦使用 Policy 來控制權(quán)限的訪問,可以在需要權(quán)限控制的 Action 或者 Controller 上設(shè)置 [Authorize("AccessControl")] 或者 [Authorize(AccessControlHelperConstants.PolicyName)]

          Copy[Authorize(AccessControlHelperConstants.PolicyName)]
          public class SystemSettingsController : AdminBaseController
          {
              // ...
          }
          
          Copy[Authorize(AccessControlHelperConstants.PolicyName)]
          public ActionResult UserList()
          {
              return View();
          }
          

          頁面元素的權(quán)限控制#

          引用 TagHelper#

          在 Views 目錄下的 _ViewImports.cshtml 文件中導(dǎo)入 AccessControlHelper 的 TagHelper

          Copy@using ActivityReservation
          @using WeihanLi.AspNetMvc.AccessControlHelper
          @using WeihanLi.AspNetMvc.MvcSimplePager
          
          @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
          @addTagHelper *, WeihanLi.AspNetMvc.AccessControlHelper
          

          頁面元素配置#

          在需要權(quán)限控制的元素上增加 asp-access 的 attribute 就可以了,如果需要 access-key 通過 asp-access-key 來配置

          Copy<ul class="list-group" asp-access asp-access-key="AdminOnly">
              <li role="separator" class="list-unstyled">
                  <br />
              </li>
              <li class="list-group-item">@Html.ActionLink("用戶管理", "UserList", "Account")</li>
          
              <li class="list-group-item">@Html.ActionLink("操作日志查看", "Index", "OperationLog")</li>
              <li class="list-group-item">@Html.ActionLink("系統(tǒng)設(shè)置管理", "Index", "SystemSettings")</li>
              <li class="list-group-item">
                  @Html.ActionLink("微信設(shè)置管理", "Index", new {
                  controller = "Config",
                  area = "Wechat"
              })
              </li>
          </ul>
          

          這樣就可以了,有權(quán)限訪問的時(shí)候才會(huì)正常渲染,沒有權(quán)限訪問的時(shí)候,這一段 ul 并不會(huì)渲染輸出,在客戶端瀏覽器查看源代碼也不會(huì)看到對(duì)應(yīng)的代碼



          整項(xiàng)目地址:vue-element-admin

          https://github.com/PanJiaChen/vue-element-admin

          前言

          拖更有點(diǎn)嚴(yán)重,過了半個(gè)月才寫了第二篇教程。無奈自己是一個(gè)業(yè)務(wù)猿,每天被我司的產(chǎn)品虐的死去活來,之前又病了一下休息了幾天,大家見諒。

          進(jìn)入正題,做后臺(tái)項(xiàng)目區(qū)別于做其它的項(xiàng)目,權(quán)限驗(yàn)證與安全性是非常重要的,可以說是一個(gè)后臺(tái)項(xiàng)目一開始就必須考慮和搭建的基礎(chǔ)核心功能。我們所要做到的是:不同的權(quán)限對(duì)應(yīng)著不同的路由,同時(shí)側(cè)邊欄也需根據(jù)不同的權(quán)限,異步生成。這里先簡(jiǎn)單說一下,我實(shí)現(xiàn)登錄和權(quán)限驗(yàn)證的思路。

          • 登錄:當(dāng)用戶填寫完賬號(hào)和密碼后向服務(wù)端驗(yàn)證是否正確,驗(yàn)證通過之后,服務(wù)端會(huì)返回一個(gè)token,拿到token之后(我會(huì)將這個(gè)token存貯到cookie中,保證刷新頁面后能記住用戶登錄狀態(tài)),前端會(huì)根據(jù)token再去拉取一個(gè) user_info 的接口來獲取用戶的詳細(xì)信息(如用戶權(quán)限,用戶名等等信息)。
          • 權(quán)限驗(yàn)證:通過token獲取用戶對(duì)應(yīng)的 role,動(dòng)態(tài)根據(jù)用戶的 role 算出其對(duì)應(yīng)有權(quán)限的路由,通過 router.addRoutes 動(dòng)態(tài)掛載這些路由。

          上述所有的數(shù)據(jù)和操作都是通過vuex全局管理控制的。(補(bǔ)充說明:刷新頁面后 vuex的內(nèi)容也會(huì)丟失,所以需要重復(fù)上述的那些操作)接下來,我們一起手摸手一步一步實(shí)現(xiàn)這個(gè)系統(tǒng)。

          登錄篇

          首先我們不管什么權(quán)限,來實(shí)現(xiàn)最基礎(chǔ)的登錄功能。

          隨便找一個(gè)空白頁面擼上兩個(gè)input的框,一個(gè)是登錄賬號(hào),一個(gè)是登錄密碼。再放置一個(gè)登錄按鈕。我們將登錄按鈕上綁上click事件,點(diǎn)擊登錄之后向服務(wù)端提交賬號(hào)和密碼進(jìn)行驗(yàn)證。 這就是一個(gè)最簡(jiǎn)單的登錄頁面。如果你覺得還要寫的更加完美點(diǎn),你可以在向服務(wù)端提交之前對(duì)賬號(hào)和密碼做一次簡(jiǎn)單的校驗(yàn)。詳細(xì)代碼



          click事件觸發(fā)登錄操作:

          this.$store.dispatch('LoginByUsername', this.loginForm).then(() => {
            this.$router.push({ path: '/' }); //登錄成功之后重定向到首頁
          }).catch(err => {
            this.$message.error(err); //登錄失敗提示錯(cuò)誤
          });
          
          復(fù)制代碼

          action:

          LoginByUsername({ commit }, userInfo) {
            const username = userInfo.username.trim()
            return new Promise((resolve, reject) => {
              loginByUsername(username, userInfo.password).then(response => {
                const data = response.data
                Cookies.set('Token', response.data.token) //登錄成功后將token存儲(chǔ)在cookie之中
                commit('SET_TOKEN', data.token)
                resolve()
              }).catch(error => {
                reject(error)
              });
            });
          }
          復(fù)制代碼

          登錄成功后,服務(wù)端會(huì)返回一個(gè) token(該token的是一個(gè)能唯一標(biāo)示用戶身份的一個(gè)key),之后我們將token存儲(chǔ)在本地cookie之中,這樣下次打開頁面或者刷新頁面的時(shí)候能記住用戶的登錄狀態(tài),不用再去登錄頁面重新登錄了。

          ps:為了保證安全性,我司現(xiàn)在后臺(tái)所有token有效期(Expires/Max-Age)都是Session,就是當(dāng)瀏覽器關(guān)閉了就丟失了。重新打開瀏覽器都需要重新登錄驗(yàn)證,后端也會(huì)在每周固定一個(gè)時(shí)間點(diǎn)重新刷新token,讓后臺(tái)用戶全部重新登錄一次,確保后臺(tái)用戶不會(huì)因?yàn)殡娔X遺失或者其它原因被人隨意使用賬號(hào)。

          獲取用戶信息

          用戶登錄成功之后,我們會(huì)在全局鉤子router.beforeEach中攔截路由,判斷是否已獲得token,在獲得token之后我們就要去獲取用戶的基本信息了

          //router.beforeEach
          if (store.getters.roles.length === 0) { // 判斷當(dāng)前用戶是否已拉取完user_info信息
            store.dispatch('GetInfo').then(res => { // 拉取user_info
              const roles = res.data.role;
              next();//resolve 鉤子
            })
          
          復(fù)制代碼

          就如前面所說的,我只在本地存儲(chǔ)了一個(gè)用戶的token,并沒有存儲(chǔ)別的用戶信息(如用戶權(quán)限,用戶名,用戶頭像等)。有些人會(huì)問為什么不把一些其它的用戶信息也存一下?主要出于如下的考慮:

          假設(shè)我把用戶權(quán)限和用戶名也存在了本地,但我這時(shí)候用另一臺(tái)電腦登錄修改了自己的用戶名,之后再用這臺(tái)存有之前用戶信息的電腦登錄,它默認(rèn)會(huì)去讀取本地 cookie 中的名字,并不會(huì)去拉去新的用戶信息。

          所以現(xiàn)在的策略是:頁面會(huì)先從 cookie 中查看是否存有 token,沒有,就走一遍上一部分的流程重新登錄,如果有token,就會(huì)把這個(gè) token 返給后端去拉取user_info,保證用戶信息是最新的。 當(dāng)然如果是做了單點(diǎn)登錄得功能的話,用戶信息存儲(chǔ)在本地也是可以的。當(dāng)你一臺(tái)電腦登錄時(shí),另一臺(tái)會(huì)被提下線,所以總會(huì)重新登錄獲取最新的內(nèi)容。

          而且從代碼層面我建議還是把 login和get_user_info兩件事分開比較好,在這個(gè)后端全面微服務(wù)的年代,后端同學(xué)也想寫優(yōu)雅的代碼~


          權(quán)限篇

          先說一說我權(quán)限控制的主體思路,前端會(huì)有一份路由表,它表示了每一個(gè)路由可訪問的權(quán)限。當(dāng)用戶登錄之后,通過 token 獲取用戶的 role ,動(dòng)態(tài)根據(jù)用戶的 role 算出其對(duì)應(yīng)有權(quán)限的路由,再通過router.addRoutes動(dòng)態(tài)掛載路由。但這些控制都只是頁面級(jí)的,說白了前端再怎么做權(quán)限控制都不是絕對(duì)安全的,后端的權(quán)限驗(yàn)證是逃不掉的。

          我司現(xiàn)在就是前端來控制頁面級(jí)的權(quán)限,不同權(quán)限的用戶顯示不同的側(cè)邊欄和限制其所能進(jìn)入的頁面(也做了少許按鈕級(jí)別的權(quán)限控制),后端則會(huì)驗(yàn)證每一個(gè)涉及請(qǐng)求的操作,驗(yàn)證其是否有該操作的權(quán)限,每一個(gè)后臺(tái)的請(qǐng)求不管是 get 還是 post 都會(huì)讓前端在請(qǐng)求 header里面攜帶用戶的 token,后端會(huì)根據(jù)該 token 來驗(yàn)證用戶是否有權(quán)限執(zhí)行該操作。若沒有權(quán)限則拋出一個(gè)對(duì)應(yīng)的狀態(tài)碼,前端檢測(cè)到該狀態(tài)碼,做出相對(duì)應(yīng)的操作。

          權(quán)限 前端or后端 來控制?

          有很多人表示他們公司的路由表是于后端根據(jù)用戶的權(quán)限動(dòng)態(tài)生成的,我司不采取這種方式的原因如下:

          • 項(xiàng)目不斷的迭代你會(huì)異常痛苦,前端新開發(fā)一個(gè)頁面還要讓后端配一下路由和權(quán)限,讓我們想了曾經(jīng)前后端不分離,被后端支配的那段恐怖時(shí)間了。
          • 其次,就拿我司的業(yè)務(wù)來說,雖然后端的確也是有權(quán)限驗(yàn)證的,但它的驗(yàn)證其實(shí)是針對(duì)業(yè)務(wù)來劃分的,比如超級(jí)編輯可以發(fā)布文章,而實(shí)習(xí)編輯只能編輯文章不能發(fā)布,但對(duì)于前端來說不管是超級(jí)編輯還是實(shí)習(xí)編輯都是有權(quán)限進(jìn)入文章編輯頁面的。所以前端和后端權(quán)限的劃分是不太一致。
          • 還有一點(diǎn)是就vue2.2.0之前異步掛載路由是很麻煩的一件事!不過好在官方也出了新的api,雖然本意是來解決ssr的痛點(diǎn)的。。。

          addRoutes

          在之前通過后端動(dòng)態(tài)返回前端路由一直很難做的,因?yàn)関ue-router必須是要vue在實(shí)例化之前就掛載上去的,不太方便動(dòng)態(tài)改變。不過好在vue2.2.0以后新增了router.addRoutes

          Dynamically add more routes to the router. The argument must be an Array using the same route config format with the routes constructor option.

          有了這個(gè)我們就可相對(duì)方便的做權(quán)限控制了。(樓主之前在權(quán)限控制也走了不少歪路,可以在項(xiàng)目的commit記錄中看到,重構(gòu)了很多次,最早沒用addRoute整個(gè)權(quán)限控制代碼里都是各種if/else的邏輯判斷,代碼相當(dāng)?shù)鸟詈虾蛷?fù)雜)


          具體實(shí)現(xiàn)

          1. 創(chuàng)建vue實(shí)例的時(shí)候?qū)ue-router掛載,但這個(gè)時(shí)候vue-router掛載一些登錄或者不用權(quán)限的公用的頁面。
          2. 當(dāng)用戶登錄后,獲取用role,將role和路由表每個(gè)頁面的需要的權(quán)限作比較,生成最終用戶可訪問的路由表。
          3. 調(diào)用router.addRoutes(store.getters.addRouters)添加用戶可訪問的路由。
          4. 使用vuex管理路由表,根據(jù)vuex中可訪問的路由渲染側(cè)邊欄組件。

          router.js

          首先我們實(shí)現(xiàn)router.js路由表,這里就拿前端控制路由來舉例(后端存儲(chǔ)的也差不多,稍微改造一下就好了)

          // router.js
          import Vue from 'vue';
          import Router from 'vue-router';
          
          import Login from '../views/login/';
          const dashboard = resolve => require(['../views/dashboard/index'], resolve);
          //使用了vue-routerd的[Lazy Loading Routes
          ](https://router.vuejs.org/en/advanced/lazy-loading.html)
          
          //所有權(quán)限通用路由表 
          //如首頁和登錄頁和一些不用權(quán)限的公用頁面
          export const constantRouterMap = [
            { path: '/login', component: Login },
            {
              path: '/',
              component: Layout,
              redirect: '/dashboard',
              name: '首頁',
              children: [{ path: 'dashboard', component: dashboard }]
            },
          ]
          
          //實(shí)例化vue的時(shí)候只掛載constantRouter
          export default new Router({
            routes: constantRouterMap
          });
          
          //異步掛載的路由
          //動(dòng)態(tài)需要根據(jù)權(quán)限加載的路由表 
          export const asyncRouterMap = [
            {
              path: '/permission',
              component: Layout,
              name: '權(quán)限測(cè)試',
              meta: { role: ['admin','super_editor'] }, //頁面需要的權(quán)限
              children: [
              { 
                path: 'index',
                component: Permission,
                name: '權(quán)限測(cè)試頁',
                meta: { role: ['admin','super_editor'] }  //頁面需要的權(quán)限
              }]
            },
            { path: '*', redirect: '/404', hidden: true }
          ];
          
          復(fù)制代碼

          這里我們根據(jù) vue-router官方推薦 的方法通過meta標(biāo)簽來標(biāo)示改頁面能訪問的權(quán)限有哪些。如meta: { role: ['admin','super_editor'] }表示該頁面只有admin和超級(jí)編輯才能有資格進(jìn)入。

          注意事項(xiàng):這里有一個(gè)需要非常注意的地方就是 404 頁面一定要最后加載,如果放在constantRouterMap一同聲明了404,后面的所以頁面都會(huì)被攔截到404,詳細(xì)的問題見addRoutes when you've got a wildcard route for 404s does not work

          main.js

          關(guān)鍵的main.js

          // main.js
          router.beforeEach((to, from, next) => {
            if (store.getters.token) { // 判斷是否有token
              if (to.path === '/login') {
                next({ path: '/' });
              } else {
                if (store.getters.roles.length === 0) { // 判斷當(dāng)前用戶是否已拉取完user_info信息
                  store.dispatch('GetInfo').then(res => { // 拉取info
                    const roles = res.data.role;
                    store.dispatch('GenerateRoutes', { roles }).then(() => { // 生成可訪問的路由表
                      router.addRoutes(store.getters.addRouters) // 動(dòng)態(tài)添加可訪問路由表
                      next({ ...to, replace: true }) // hack方法 確保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
                    })
                  }).catch(err => {
                    console.log(err);
                  });
                } else {
                  next() //當(dāng)有用戶權(quán)限的時(shí)候,說明所有可訪問路由已生成 如訪問沒權(quán)限的全面會(huì)自動(dòng)進(jìn)入404頁面
                }
              }
            } else {
              if (whiteList.indexOf(to.path) !== -1) { // 在免登錄白名單,直接進(jìn)入
                next();
              } else {
                next('/login'); // 否則全部重定向到登錄頁
              }
            }
          });
          
          復(fù)制代碼

          這里的router.beforeEach也結(jié)合了上一章講的一些登錄邏輯代碼。


          上面一張圖就是在使用addRoutes方法之前的權(quán)限判斷,非常的繁瑣,因?yàn)槲沂前阉械穆酚啥紥煸诹松先ィ形乙鞣N判斷當(dāng)前的用戶是否有權(quán)限進(jìn)入該頁面,各種if/else的嵌套,維護(hù)起來相當(dāng)?shù)睦щy。但現(xiàn)在有了addRoutes之后就非常的方便,我只掛載了用戶有權(quán)限進(jìn)入的頁面,沒權(quán)限,路由自動(dòng)幫我跳轉(zhuǎn)的404,省去了不少的判斷。


          這里還有一個(gè)小hack的地方,就是router.addRoutes之后的next()可能會(huì)失效,因?yàn)榭赡躰ext()的時(shí)候路由并沒有完全add完成,好在查閱文檔發(fā)現(xiàn)

          next('/') or next({ path: '/' }): redirect to a different location. The current navigation will be aborted and a new one will be started.

          這樣我們就可以簡(jiǎn)單的通過next(to)巧妙的避開之前的那個(gè)問題了。這行代碼重新進(jìn)入router.beforeEach這個(gè)鉤子,這時(shí)候再通過next()來釋放鉤子,就能確保所有的路由都已經(jīng)掛在完成了。

          store/permission.js

          就來就講一講 GenerateRoutes Action

          // store/permission.js
          import { asyncRouterMap, constantRouterMap } from 'src/router';
          
          function hasPermission(roles, route) {
            if (route.meta && route.meta.role) {
              return roles.some(role => route.meta.role.indexOf(role) >= 0)
            } else {
              return true
            }
          }
          
          const permission = {
            state: {
              routers: constantRouterMap,
              addRouters: []
            },
            mutations: {
              SET_ROUTERS: (state, routers) => {
                state.addRouters = routers;
                state.routers = constantRouterMap.concat(routers);
              }
            },
            actions: {
              GenerateRoutes({ commit }, data) {
                return new Promise(resolve => {
                  const { roles } = data;
                  const accessedRouters = asyncRouterMap.filter(v => {
                    if (roles.indexOf('admin') >= 0) return true;
                    if (hasPermission(roles, v)) {
                      if (v.children && v.children.length > 0) {
                        v.children = v.children.filter(child => {
                          if (hasPermission(roles, child)) {
                            return child
                          }
                          return false;
                        });
                        return v
                      } else {
                        return v
                      }
                    }
                    return false;
                  });
                  commit('SET_ROUTERS', accessedRouters);
                  resolve();
                })
              }
            }
          };
          
          export default permission;
          
          復(fù)制代碼

          這里的代碼說白了就是干了一件事,通過用戶的權(quán)限和之前在router.js里面asyncRouterMap的每一個(gè)頁面所需要的權(quán)限做匹配,最后返回一個(gè)該用戶能夠訪問路由有哪些。


          側(cè)邊欄

          最后一個(gè)涉及到權(quán)限的地方就是側(cè)邊欄,不過在前面的基礎(chǔ)上已經(jīng)很方便就能實(shí)現(xiàn)動(dòng)態(tài)顯示側(cè)邊欄了。這里側(cè)邊欄基于element-ui的NavMenu來實(shí)現(xiàn)的。 代碼有點(diǎn)多不貼詳細(xì)的代碼了,有興趣的可以直接去github上看地址,或者直接看關(guān)于側(cè)邊欄的文檔。

          說白了就是遍歷之前算出來的permission_routers,通過vuex拿到之后動(dòng)態(tài)v-for渲染而已。不過這里因?yàn)橛幸恍I(yè)務(wù)需求所以加了很多判斷 比如我們?cè)诙x路由的時(shí)候會(huì)加很多參數(shù)

          /**
          * hidden: true                   if `hidden:true` will not show in the sidebar(default is false)
          * redirect: noredirect           if `redirect:noredirect` will no redirct in the breadcrumb
          * name:'router-name'             the name is used by <keep-alive> (must set!!!)
          * meta : {
             role: ['admin','editor']     will control the page role (you can set multiple roles)
             title: 'title'               the name show in submenu and breadcrumb (recommend set)
             icon: 'svg-name'             the icon show in the sidebar,
             noCache: true                if fasle ,the page will no be cached(default is false)
           }
          **/
          復(fù)制代碼

          這里僅供參考,而且本項(xiàng)目為了支持無限嵌套路由,所有側(cè)邊欄這塊使用了遞歸組件。如需要請(qǐng)大家自行改造,來打造滿足自己業(yè)務(wù)需求的側(cè)邊欄。

          側(cè)邊欄高亮問題:很多人在群里問為什么自己的側(cè)邊欄不能跟著自己的路由高亮,其實(shí)很簡(jiǎn)單,element-ui官方已經(jīng)給了default-active所以我們只要

          :default-active="$route.path" 將default-active一直指向當(dāng)前路由就可以了,就是這么簡(jiǎn)單


          按鈕級(jí)別權(quán)限控制

          有很多人一直在問關(guān)于按鈕級(jí)別粒度的權(quán)限控制怎么做。我司現(xiàn)在是這樣的,真正需要按鈕級(jí)別控制的地方不是很多,現(xiàn)在是通過獲取到用戶的role之后,在前端用v-if手動(dòng)判斷來區(qū)分不同權(quán)限對(duì)應(yīng)的按鈕的。理由前面也說了,我司顆粒度的權(quán)限判斷是交給后端來做的,每個(gè)操作后端都會(huì)進(jìn)行權(quán)限判斷。而且我覺得其實(shí)前端真正需要按鈕級(jí)別判斷的地方不是很多,如果一個(gè)頁面有很多種不同權(quán)限的按鈕,我覺得更多的應(yīng)該是考慮產(chǎn)品層面是否設(shè)計(jì)合理。當(dāng)然你強(qiáng)行說我想做按鈕級(jí)別的權(quán)限控制,你也可以參照路由層面的做法,搞一個(gè)操作權(quán)限表。。。但個(gè)人覺得有點(diǎn)多此一舉。或者將它封裝成一個(gè)指令都是可以的。


          axios攔截器

          這里再說一說 axios 吧。雖然在上一篇系列文章中簡(jiǎn)單介紹過,不過這里還是要在嘮叨一下。如上文所說,我司服務(wù)端對(duì)每一個(gè)請(qǐng)求都會(huì)驗(yàn)證權(quán)限,所以這里我們針對(duì)業(yè)務(wù)封裝了一下請(qǐng)求。首先我們通過request攔截器在每個(gè)請(qǐng)求頭里面塞入token,好讓后端對(duì)請(qǐng)求進(jìn)行權(quán)限驗(yàn)證。并創(chuàng)建一個(gè)respone攔截器,當(dāng)服務(wù)端返回特殊的狀態(tài)碼,我們統(tǒng)一做處理,如沒權(quán)限或者token失效等操作。

          import axios from 'axios'
          import { Message } from 'element-ui'
          import store from '@/store'
          import { getToken } from '@/utils/auth'
          
          // 創(chuàng)建axios實(shí)例
          const service = axios.create({
            baseURL: process.env.BASE_API, // api的base_url
            timeout: 5000 // 請(qǐng)求超時(shí)時(shí)間
          })
          
          // request攔截器
          service.interceptors.request.use(config => {
            // Do something before request is sent
            if (store.getters.token) {
              config.headers['X-Token'] = getToken() // 讓每個(gè)請(qǐng)求攜帶token--['X-Token']為自定義key 請(qǐng)根據(jù)實(shí)際情況自行修改
            }
            return config
          }, error => {
            // Do something with request error
            console.log(error) // for debug
            Promise.reject(error)
          })
          
          // respone攔截器
          service.interceptors.response.use(
            response => response,
            /**
            * 下面的注釋為通過response自定義code來標(biāo)示請(qǐng)求狀態(tài),當(dāng)code返回如下情況為權(quán)限有問題,登出并返回到登錄頁
            * 如通過xmlhttprequest 狀態(tài)碼標(biāo)識(shí) 邏輯可寫在下面error中
            */
            //  const res = response.data;
            //     if (res.code !== 20000) {
            //       Message({
            //         message: res.message,
            //         type: 'error',
            //         duration: 5 * 1000
            //       });
            //       // 50008:非法的token; 50012:其他客戶端登錄了;  50014:Token 過期了;
            //       if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
            //         MessageBox.confirm('你已被登出,可以取消繼續(xù)留在該頁面,或者重新登錄', '確定登出', {
            //           confirmButtonText: '重新登錄',
            //           cancelButtonText: '取消',
            //           type: 'warning'
            //         }).then(() => {
            //           store.dispatch('FedLogOut').then(() => {
            //             location.reload();// 為了重新實(shí)例化vue-router對(duì)象 避免bug
            //           });
            //         })
            //       }
            //       return Promise.reject('error');
            //     } else {
            //       return response.data;
            //     }
            error => {
              console.log('err' + error)// for debug
              Message({
                message: error.message,
                type: 'error',
                duration: 5 * 1000
              })
              return Promise.reject(error)
            })
          
          export default service
          
          復(fù)制代碼

          兩步驗(yàn)證




          文章一開始也說了,后臺(tái)的安全性是很重要的,簡(jiǎn)簡(jiǎn)單單的一個(gè)賬號(hào)+密碼的方式是很難保證安全性的。所以我司的后臺(tái)項(xiàng)目都是用了兩步驗(yàn)證的方式,之前我們也嘗試過使用基于 google-authenticator 或者youbikey這樣的方式但難度和操作成本都比較大。后來還是準(zhǔn)備借助騰訊爸爸,這年代誰不用微信。。。安全性騰訊爸爸也幫我做好了保障。 樓主建議兩步驗(yàn)證要支持多個(gè)渠道不要只微信或者QQ,前段時(shí)間QQ第三方登錄就出了bug,官方兩三天才修好的,害我背了鍋/(ㄒoㄒ)/~~ 。

          這里的兩部驗(yàn)證有點(diǎn)名不副實(shí),其實(shí)就是賬號(hào)密碼驗(yàn)證過之后還需要一個(gè)綁定的第三方平臺(tái)登錄驗(yàn)證而已。 寫起來也很簡(jiǎn)單,在原有登錄得邏輯上改造一下就好。

          this.$store.dispatch('LoginByEmail', this.loginForm).then(() => {
            //this.$router.push({ path: '/' });
            //不重定向到首頁
            this.showDialog = true //彈出選擇第三方平臺(tái)的dialog
          }).catch(err => {
            this.$message.error(err); //登錄失敗提示錯(cuò)誤
          });
          復(fù)制代碼

          登錄成功之后不直接跳到首頁而是讓用戶兩步登錄,選擇登錄得平臺(tái)。 接下來就是所有第三方登錄一樣的地方通過 OAuth2.0 授權(quán)。這個(gè)各大平臺(tái)大同小異,大家自行查閱文檔,不展開了,就說一個(gè)微信授權(quán)比較坑的地方。注意你連參數(shù)的順序都不能換,不然會(huì)驗(yàn)證不通過。具體代碼,同時(shí)我也封裝了openWindow方法大家自行看吧。 當(dāng)?shù)谌绞跈?quán)成功之后都會(huì)跳到一個(gè)你之前有一個(gè)傳入redirect——uri的頁面

          如微信還必須是你授權(quán)賬號(hào)的一級(jí)域名。所以你授權(quán)的域名是vue-element-admin.com,你就必須重定向到vue-element-admin.com/xxx/下面,所以你需要寫一個(gè)重定向的服務(wù),如vue-element-admin.com/auth/redirect?a.com 跳到該頁面時(shí)會(huì)再次重定向給a.com。


          所以我們后臺(tái)也需要開一個(gè)authredirect頁面:代碼。他的作用是第三方登錄成功之后會(huì)默認(rèn)跳到授權(quán)的頁面,授權(quán)的頁面會(huì)再次重定向回我們的后臺(tái),由于是spa,改變路由的體驗(yàn)不好,我們通過window.opener.location.href的方式改變hash,在login.js里面再監(jiān)聽hash的變化。當(dāng)hash變化時(shí),獲取之前第三方登錄成功返回的code與第一步賬號(hào)密碼登錄之后返回的uid一同發(fā)送給服務(wù)端驗(yàn)證是否正確,如果正確,這時(shí)候就是真正的登錄成功。

           created() {
               window.addEventListener('hashchange', this.afterQRScan);
             },
             destroyed() {
               window.removeEventListener('hashchange', this.afterQRScan);
             },
             afterQRScan() {
               const hash = window.location.hash.slice(1);
               const hashObj = getQueryObject(hash);
               const originUrl = window.location.origin;
               history.replaceState({}, '', originUrl);
               const codeMap = {
                 wechat: 'code',
                 tencent: 'code'
               };
               const codeName = hashObj[codeMap[this.auth_type]];
               this.$store.dispatch('LoginByThirdparty', codeName).then(() => {
                 this.$router.push({
                   path: '/'
                 });
               });
             }
          復(fù)制代碼

          到這里涉及登錄權(quán)限的東西也差不多講完了,這里樓主只是給了大家一個(gè)實(shí)現(xiàn)的思路(都是樓主不斷摸索的血淚史),每個(gè)公司實(shí)現(xiàn)的方案都有些出入,請(qǐng)謹(jǐn)慎選擇適合自己業(yè)務(wù)形態(tài)的解決方案。如果有什么想法或者建議歡迎去本項(xiàng)目下留言,一同討論。


          主站蜘蛛池模板: 国产亚洲一区二区在线观看| 日韩一区二区三区射精| 精品国产一区二区三区麻豆 | 欧美亚洲精品一区二区| 日韩精品无码一区二区三区AV| 中文字幕人妻第一区| 男人的天堂精品国产一区| 亚洲国产精品一区| 色天使亚洲综合一区二区| 少妇无码一区二区二三区| 中文字幕在线观看一区二区三区| 韩国美女vip福利一区| 三上悠亚亚洲一区高清| 一区二区在线播放视频| 乱码人妻一区二区三区| 51视频国产精品一区二区| 精品一区二区三区免费 | 一区二区三区在线观看| 无码人妻一区二区三区在线视频| 国产福利电影一区二区三区| 国产一区二区三区免费在线观看| 成人区人妻精品一区二区三区| 日韩精品无码中文字幕一区二区 | 亚洲中文字幕一区精品自拍| 久久久久人妻精品一区二区三区| 国产综合精品一区二区三区| 国产综合一区二区在线观看| 99精品国产一区二区三区| 精品国产一区二区三区久久蜜臀 | 一区二区三区四区免费视频 | 一区二区三区四区在线观看视频| 国产精品久久久久一区二区| 国产乱码精品一区二区三区香蕉| 亚洲色精品VR一区区三区| 亚洲熟妇AV一区二区三区浪潮| 亚洲欧美国产国产一区二区三区| 成人免费视频一区二区三区| 国产在线一区二区三区在线| 日本韩国黄色一区二区三区 | 日本一区二区三区不卡视频中文字幕| 国产成人一区二区精品非洲|