前言
大家好!我是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)為有以下幾步:

那么接下來(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)該是下面這樣的要求:

上圖可以看出,用戶(hù)
多對(duì)多角色多對(duì)多權(quán)限
用表結(jié)構(gòu)展示的話(huà)就是這樣,一共5張表,3張實(shí)體表,2張關(guān)聯(lián)表

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));
}
上面代碼用流程圖表示如下

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)用返回如下

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

三、用戶(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)的流程

四、用戶(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è)信息嗎

對(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)始幫到我了,如果它們也能幫助到你,榮幸之至!

