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

前言

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

作為一名從業(yè)已達(dá)六年的老碼農(nóng),我的工作主要是開(kāi)發(fā)后端Java業(yè)務(wù)系統(tǒng),包括各種管理后臺(tái)和小程序等。在這些項(xiàng)目中,我設(shè)計(jì)過(guò)單/多租戶(hù)體系系統(tǒng),對(duì)接過(guò)許多開(kāi)放平臺(tái),也搞過(guò)消息中心這類(lèi)較為復(fù)雜的應(yīng)用,但幸運(yùn)的是,我至今還沒(méi)有遇到過(guò)線上系統(tǒng)由于代碼崩潰導(dǎo)致資損的情況。這其中的原因有三點(diǎn):一是業(yè)務(wù)系統(tǒng)本身并不復(fù)雜;二是我一直遵循某大廠代碼規(guī)約,在開(kāi)發(fā)過(guò)程中盡可能按規(guī)約編寫(xiě)代碼;三是經(jīng)過(guò)多年的開(kāi)發(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)的資料,出來(lái)的就是整合Shrio、Spring Security等各種框架,然后下面一頓貼配置和代碼,看得人云里霧里。實(shí)際上接口的權(quán)限控制是整個(gè)系統(tǒng)權(quán)限控制里面很小的一環(huán),沒(méi)有設(shè)計(jì)好底層數(shù)據(jù)結(jié)構(gòu),是無(wú)法做好接口的權(quán)限控制的。那么怎么做一個(gè)系統(tǒng)的權(quán)限控制呢?我認(rèn)為有以下幾步:

image.png

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

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

由于文章經(jīng)常被抄襲,開(kāi)源的代碼甚至被當(dāng)成收費(fèi)項(xiàng),所以源碼里面不是全部代碼,有需要的同學(xué)可以留個(gè)郵箱,我給你單獨(dú)發(fā)!

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

第一,只要一個(gè)系統(tǒng)是給人用的,那么這個(gè)系統(tǒng)就一定會(huì)有一張用戶(hù)表;第二,只要有人的地方,就一定會(huì)有角色權(quán)限的劃分,最簡(jiǎn)單的就是超級(jí)管理員、普通用戶(hù);第三,如此常見(jiàn)的設(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ò)角色關(guān)聯(lián)用戶(hù),角色關(guān)聯(lián)權(quán)限的方式間接賦予用戶(hù)權(quán)限。

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


image.png

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

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

image.png

2. 建表語(yǔ)句

(1) t_user

DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
  `user_id` bigint(20) unsigned zerofill NOT NULL AUTO_INCREMENT COMMENT '用戶(hù)ID',
  `user_name` varchar(32) DEFAULT NULL COMMENT '用戶(hù)名稱(chēng)',
  `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 '角色名稱(chēng)',
  `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)限名稱(chēng)',
  `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 '用戶(hù)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;

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

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

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

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

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

image.png

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

接下來(lái)且看如何實(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, '普通用戶(hù)', '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', '查詢(xún)', '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ù)名稱(chēng)查詢(xún)用戶(hù)信息
  UserDO userDO = userMapper.selectOne(new QueryWrapper<UserDO>().lambda().eq(UserDO::getUserName, userName));
  if (Objects.isNull(userDO)) {
    return ResponseEntity.ok("未查詢(xún)到用戶(hù)");
  }
  //查詢(xún)當(dāng)前用戶(hù)的角色信息
  List<UserRoleDO> userRoleDOList = userRoleMapper.selectList(
            new QueryWrapper<UserRoleDO>().lambda().eq(UserRoleDO::getUserId, userDO.getUserId()));
  if (CollectionUtils.isEmpty(userRoleDOList)) {
    return ResponseEntity.ok("當(dāng)前用戶(hù)沒(méi)有角色");
  }
  //查詢(xún)當(dāng)前用戶(hù)的權(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)前角色沒(méi)有對(duì)應(yīng)權(quán)限");
  }
  //查詢(xú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();
  //緩存用戶(hù)信息
  redisUtil.set(token, JSONObject.toJSONString(userDO), tokenTimeout);
  //緩存用戶(hù)權(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));
}

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


image.png

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

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

(沒(méi)畫(huà)前端界面,大家將就看下哈)

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

image.png

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

image.png

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

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

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

前面定義了4個(gè)權(quán)限:新增/刪除/修改/查詢(xú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)前用戶(hù)必須要有value中所有的權(quán)限才可以調(diào)用該接口,OR的意思是當(dāng)前用戶(hù)只需要有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 {
        //獲取用戶(hù)上下文
        UserContext userContext = GlobalUserContext.getUserContext();
        if (Objects.isNull(userContext)) {
            throw new RuntimeException("用戶(hù)認(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("用戶(hù)認(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("用戶(hù)權(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("用戶(hù)權(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      用戶(hù)ID
     * @param permissions 權(quán)限組
     * @return 沒(méi)有授予的權(quán)限
     */
    private String[] checkPermissionsByOr(Long userId, String[] permissions) {
        // 獲取用戶(hù)權(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      用戶(hù)ID
     * @param permissions 權(quán)限組
     * @return 沒(méi)有授予的權(quán)限
     */
    private String[] checkPermissionsByAnd(Long userId, String[] permissions) {
        // 獲取用戶(hù)權(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相同,那么說(shuō)明權(quán)限吻合
        if (permissions.length == tempPermissions.size()) {
            return null;
        }
        //否則取出當(dāng)前用戶(hù)沒(méi)有的權(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)的流程

image.png

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

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

小B的權(quán)限從查詢(xún)變?yōu)榱?code>查詢(xún)加更新


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

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


image.png

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

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

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

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

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

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

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

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

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

寫(xiě)在最后

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

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

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

后來(lái)我老大說(shuō),既然用不來(lái)人家的,那就自己寫(xiě)一套,想起來(lái)那時(shí)真單純,我就真的自己開(kāi)始寫(xiě)微服務(wù)架構(gòu)。最開(kāi)始我對(duì)微服務(wù)的唯一印象就是一個(gè)服務(wù)提供者、一個(gè)服務(wù)消費(fèi)者,肯定是兩個(gè)應(yīng)用,至于為啥是這樣,查的百度都是這樣寫(xiě)的。然后我就建了兩個(gè)應(yīng)用,一個(gè)網(wǎng)關(guān)應(yīng)用、一個(gè)業(yè)務(wù)應(yīng)用,自己寫(xiě)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í)候雖然代碼寫(xiě)的很爛、bug一堆,但我們老大也沒(méi)罵我們,每周四還會(huì)給我們上夜校,跟我們講一些大廠的框架和技術(shù)棧。他跟我們說(shuō),現(xiàn)在多用用人家的技術(shù),到時(shí)候出去面試大廠也容易一些。寫(xiě)博文也是老大讓我們做的,他說(shuō)現(xiàn)在一點(diǎn)點(diǎn)的積累,等到過(guò)幾年就會(huì)變成文庫(kù)了?,F(xiàn)在想來(lái),真是一個(gè)不錯(cuò)的老大!

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容