一、前言

在开发者日常工作中,每次开启新项目,技术选型都至关重要,就像在未知丛林中选择正确的指南针。而众所周知的的 SpringSecurity 功能强大,在大型项目中表现卓越。但对于中小型项目而言,其复杂配置却成为难题。想象你接手一个中小型项目,想尽快完成权限管理模块,引入 SpringSecurity 后,仅配置就耗时费力,文件复杂、策略易出错,简单需求变得繁琐,拉长开发周期,还可能埋下安全隐患。

于是乎国产安全框架---Sa-Token 出现了。它秉持 “零配置开箱即用” 理念,专为中小型项目开发者设计。Sa-Token 将 RBAC 权限模型实现成本降低 60%,能让开发者摆脱复杂配置,专注业务逻辑,快速搭建安全可靠的权限管理体系。

例如,在项目中实现用户角色和权限管理,用 Sa-Token,几行代码就能完成登录认证、权限校验等基础功能,且与常见框架兼容性出色,如 Spring Boot 等都能无缝集成。今天,咱们聚焦权限管理领域,以权限管理为例,来简单介绍一下SaToken的权限认证。

二、权限管理数据模型介绍 

在Java项目中,认证(Authentication)和授权(Authorization)是两个常见的安全概念。

认证指验证用户的身份,并确认其是否被允许访问系统的特定资源。认证通常涉及验证用户提供的凭据(例如用户名和密码)是否有效。认证的目标是确保用户是其所声称的身份。

授权指确定经过认证的用户是否具有访问特定资源的权限。授权涉及分配或管理用户在系统中的角色和权限,以便限制其对资源的访问。通过授权,系统可以根据用户的身份和角色来限制或允许其执行特定操作或访问特定数据。

2.1.RBAC模型

授权的数据模型通常用来定义和管理用户、角色和权限之间的关系。一种常见的数据模型是RBAC(Role-Based Access Control,基于角色的访问控制)。RBAC 模型的核心是将用户与角色关联,角色与权限关联。用户通过被分配的角色间接获得相应的权限。

例如,在一个企业信息系统中,“部门经理” 是一个角色,该角色可能拥有 “查看部门预算”“审批员工请假” 等权限,而具体的员工被分配到 “部门经理” 这个角色后,就具备了这些权限。

数据结构设计

  • 用户表(Users):存储用户的基本信息,如用户 ID、用户名、密码等。
  • 角色表(Roles):记录角色的相关信息,包括角色 ID、角色名称、角色描述。
  • 权限表(Permissions):包含权限的具体信息,如权限 ID、权限名称、权限描述。
  • 用户 - 角色关联表(User_Roles):通过用户 ID 和角色 ID 建立关联,表明某个用户属于哪些角色。
  • 角色 - 权限关联表(Role_Permissions):使用角色 ID 和权限 ID 进行关联,说明某个角色拥有哪些权限。

优点

  • 易于管理:通过角色对权限进行分组管理,减少了直接给用户分配权限的工作量。例如,当需要为新入职的部门经理赋予权限时,只需将其分配到 “部门经理” 角色即可,无需逐个为其添加权限。
  • 灵活性较好:可以方便地添加、修改和删除角色以及角色对应的权限。比如,当企业业务调整,需要为 “部门经理” 角色增加 “审核部门项目计划” 的权限时,只需在角色 - 权限关联表中添加相应记录。
  • 符合组织架构:能够很好地映射现实组织中的角色和职责,便于理解和实施。

缺点

  • 动态性不足:对于一些需要根据用户的实时状态或环境因素动态调整权限的场景,RBAC 模型的适应性较差。例如,在一个电商系统中,根据用户的消费等级动态分配不同的商品折扣权限,RBAC 难以直接实现。
  • 角色膨胀问题:随着系统功能的增加和业务的复杂化,角色数量可能会不断增多,导致角色管理变得复杂。

一般而言,RBAC适用于组织结构相对稳定、业务规则较为固定的系统,如企业资源规划(ERP)系统、办公自动化系统等。这些系统中的用户角色和权限相对明确,通过 RBAC 可以方便地进行管理。

除了RBAC,还有其他的授权数据模型,如ABAC(Attribute-Based Access Control,基于属性的访问控制)和PBAC(Policy-Based Access Control,基于策略的访问控制)。

2.2.ABAC模型

ABAC(Attribute - Based Access Control,基于属性的访问控制)模型基于用户属性、资源属性和环境属性来决定是否授予访问权限。例如,在一个文件管理系统中,用户的部门、职位是用户属性,文件的敏感级别、所属项目是资源属性,访问时间、访问地点是环境属性。系统会根据这些属性的组合来判断用户是否有权限访问文件。

数据结构设计

  • 用户属性表(User_Attributes):存储用户的各种属性信息,如用户 ID、属性名称、属性值。
  • 资源属性表(Resource_Attributes):记录资源的属性,包括资源 ID、属性名称、属性值。
  • 环境属性表(Environment_Attributes):保存环境相关的属性,如时间、地点等。
  • 策略表(Policies):定义访问控制策略,包含策略 ID、策略规则(基于各种属性的条件表达式)、策略结果(允许或拒绝访问)。

优点

  • 高度灵活:可以根据各种属性的组合制定复杂的访问控制策略,适应复杂多变的业务场景。例如,在一个医疗系统中,可以根据医生的职称、患者的病情、就诊时间等多方面属性来控制医生对患者病历的访问权限。
  • 动态性强:能够实时根据属性的变化调整访问权限。比如,当用户的部门发生变更时,系统可以根据新的用户属性自动调整其访问权限。

缺点

  • 实现复杂:需要对各种属性进行定义、管理和维护,并且要设计复杂的规则引擎来评估访问请求。这对开发人员的技术要求较高,开发成本也较大。
  • 性能开销大:由于每次访问请求都需要对多个属性进行评估和规则匹配,可能会影响系统的性能。

ABAC则在需要根据动态属性进行访问控制的场景中表现出色,如云计算环境、物联网系统等。这些场景中用户、资源和环境的属性变化频繁,ABAC 能够根据实时情况进行灵活的访问控制。

2.3.PBAC模型

PBAC(Policy - Based Access Control,基于策略的访问控制)模型将访问控制决策基于预定义的策略。策略可以是基于规则、条件或业务逻辑的组合。例如,在一个金融交易系统中,策略可以规定 “只有在工作日的工作时间内,交易金额小于 100 万的操作才允许执行”。

数据结构设计

  • 策略表(Policies):包含策略 ID、策略名称、策略描述、策略规则(可以是复杂的逻辑表达式)。
  • 策略执行表(Policy_Executions):记录策略的执行情况,如执行时间、执行结果、相关的用户和资源信息。

优点

  • 灵活性和可定制性高:可以根据不同的业务需求和安全要求制定各种复杂的策略,并且可以随时修改和调整策略。例如,当金融市场出现异常波动时,可以及时调整交易系统的访问策略。
  • 集中管理:所有的访问控制决策都基于策略,便于集中管理和维护安全策略。

缺点

  • 策略定义复杂:需要专业的安全人员来定义和维护策略,策略的合理性和有效性对系统安全至关重要。如果策略定义不当,可能会导致安全漏洞或影响系统的正常运行。
  • 性能影响:复杂的策略评估和执行可能会带来一定的性能开销,特别是在高并发的场景下。

这个模型对于安全要求较高、业务规则复杂多变的系统,如金融系统、政府信息系统等,PBAC 可以通过制定复杂的策略来满足严格的安全需求。 

下面笔者就以最常见的PBAC模型的开发集成方式在项目中的使用进行基础介绍。

三、SpringBoot3集成SaToken、JWT实现功能权限校验流程

Step1:以权限模型创建数据表

首先以RBAC模型的搭建方式创建数据表,最基础的五张表,并导入相应的数据,这里为了更好查看权限管理的作用因此引入的额外的一张sys_book表用户信息查看以及权限限制情况:

/*
 Navicat Premium Dump SQL

 Source Server         : Mysql
 Source Server Type    : MySQL
 Source Server Version : 80013 (8.0.13)
 Source Host           : localhost:3306
 Source Schema         : satoken_demo

 Target Server Type    : MySQL
 Target Server Version : 80013 (8.0.13)
 File Encoding         : 65001

 Date: 17/03/2025 16:54:35
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_book
-- ----------------------------
DROP TABLE IF EXISTS `sys_book`;
CREATE TABLE `sys_book`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '书名',
  `author` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '作者',
  `isbn` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'ISBN号',
  `price` decimal(10, 2) NOT NULL COMMENT '价格',
  `publish_date` date NULL DEFAULT NULL COMMENT '出版日期',
  `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '描述',
  `status` tinyint(4) NULL DEFAULT 1 COMMENT '状态:0-下架,1-上架',
  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` tinyint(1) NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `isbn`(`isbn` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 22 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '书本表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_book
-- ----------------------------
INSERT INTO `sys_book` VALUES (1, '我在测试数据', 'test', '', 1.00, '2025-03-16', '测试数据', 1, '2025-03-16 21:40:53', '2025-03-16 21:40:53', 0);
INSERT INTO `sys_book` VALUES (2, 'Spring实战', 'Craig Walls', '9787115417305', 89.00, '2016-04-01', 'Spring框架实战指南,包含最新特性介绍', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (3, '算法导论', 'Thomas H.Cormen', '9787111187776', 128.00, '2006-09-01', '计算机算法经典教材,深入讲解各种算法原理', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (4, '深入理解Java虚拟机', '周志明', '9787111641247', 129.00, '2019-12-01', 'JVM原理与优化,深入理解Java虚拟机', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (5, 'MySQL必知必会', 'Ben Forta', '9787115191120', 59.00, '2009-01-01', 'MySQL数据库入门与进阶指南', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (6, 'Redis设计与实现', '黄健宏', '978711557975', 79.00, '2014-06-01', 'Redis源码剖析,深入理解Redis实现原理', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (7, '设计模式', 'Erich Gamma', '9787111346365', 35.00, '2009-10-01', '软件设计模式经典著作', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (8, '计算机网络', '谢希仁', '9787115547615', 49.00, '2021-06-01', '计算机网络原理与协议详解', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (9, '操作系统概念', 'Abraham Silberschatz', '9787111641248', 99.00, '2018-03-01', '操作系统核心概念与原理', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (10, '数据结构与算法分析', 'Mark Allen Weiss', '9787111641249', 89.00, '2019-05-01', '数据结构与算法分析经典教材', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (11, '微服务架构设计模式', 'Chris Richardson', '9787111641250', 119.00, '2020-01-01', '微服务架构设计模式与实践', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (12, 'Docker容器与容器云', '张磊', '9787111641251', 89.00, '2016-08-01', 'Docker容器技术详解', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (13, 'Kubernetes权威指南', '龚正', '9787111641252', 139.00, '2019-10-01', 'Kubernetes容器编排技术指南', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (14, 'Elasticsearch实战', 'Radu Gheorghe', '9787111641253', 99.00, '2018-12-01', 'Elasticsearch搜索引擎实战指南', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (15, 'RabbitMQ实战指南', '朱忠华', '9787111641254', 79.00, '2017-06-01', 'RabbitMQ消息队列实战', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (16, 'Nginx高性能Web服务器详解', '陶辉', '9787111641255', 89.00, '2016-03-01', 'Nginx服务器配置与优化', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (17, 'Linux系统编程', 'Robert Love', '9787111641256', 109.00, '2015-09-01', 'Linux系统编程指南', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (18, 'Python机器学习', 'Sebastian Raschka', '9787111641257', 119.00, '2017-12-01', 'Python机器学习算法与实践', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (19, 'Go语言实战', 'William Kennedy', '9787111641258', 89.00, '2016-11-01', 'Go语言编程实战指南', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (20, '区块链技术指南', '邹均', '9787111641259', 129.00, '2018-05-01', '区块链技术原理与应用', 1, '2025-03-14 18:55:46', '2025-03-14 18:55:46', 0);
INSERT INTO `sys_book` VALUES (21, 'Hospitalization', '雪白', 'Notwithstanding', 200.00, '2025-02-17', 'Self-confidence', 7, NULL, NULL, 0);

-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) NULL DEFAULT NULL COMMENT '父权限ID',
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '权限名称',
  `code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '权限编码',
  `type` tinyint(4) NOT NULL COMMENT '类型:1-菜单,2-按钮',
  `path` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '路由路径',
  `component` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组件路径',
  `icon` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图标',
  `sort` int(11) NULL DEFAULT 0 COMMENT '排序',
  `status` tinyint(4) NULL DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` tinyint(1) NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `code`(`code` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统权限表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_permission
-- ----------------------------
INSERT INTO `sys_permission` VALUES (1, 0, '系统管理', 'system', 1, '/system', 'Layout', NULL, 0, 1, '2025-03-14 18:12:42', '2025-03-14 18:12:42', 0);
INSERT INTO `sys_permission` VALUES (2, 1, '用户管理', 'system:user', 1, '/system/user', 'system/user/index', NULL, 0, 1, '2025-03-14 18:12:42', '2025-03-14 18:12:42', 0);
INSERT INTO `sys_permission` VALUES (3, 1, '角色管理', 'system:role', 1, '/system/role', 'system/role/index', NULL, 0, 1, '2025-03-14 18:12:42', '2025-03-14 18:12:42', 0);
INSERT INTO `sys_permission` VALUES (4, 2, '用户查询', 'system:user:query', 2, '', '', NULL, 0, 1, '2025-03-14 18:12:42', '2025-03-14 18:12:42', 0);
INSERT INTO `sys_permission` VALUES (5, 2, '用户新增', 'system:user:add', 2, '', '', NULL, 0, 1, '2025-03-14 18:12:42', '2025-03-14 18:12:42', 0);
INSERT INTO `sys_permission` VALUES (6, 2, '用户修改', 'system:user:edit', 2, '', '', NULL, 0, 1, '2025-03-14 18:12:42', '2025-03-14 18:12:42', 0);
INSERT INTO `sys_permission` VALUES (7, 2, '用户删除', 'system:user:delete', 2, '', '', NULL, 0, 1, '2025-03-14 18:12:42', '2025-03-14 18:12:42', 0);
INSERT INTO `sys_permission` VALUES (8, 0, '图书管理', 'book', 1, '/book', 'Layout', NULL, 0, 1, '2025-03-14 18:12:42', '2025-03-14 18:12:42', 0);
INSERT INTO `sys_permission` VALUES (9, 8, '图书查询', 'book:query', 2, '', '', NULL, 0, 1, '2025-03-14 18:12:42', '2025-03-14 18:12:42', 0);
INSERT INTO `sys_permission` VALUES (10, 8, '图书新增', 'book:add', 2, '', '', NULL, 0, 1, '2025-03-14 18:12:42', '2025-03-14 18:12:42', 0);
INSERT INTO `sys_permission` VALUES (11, 8, '图书修改', 'book:edit', 2, '', '', NULL, 0, 1, '2025-03-14 18:12:42', '2025-03-14 18:12:42', 0);
INSERT INTO `sys_permission` VALUES (12, 8, '图书删除', 'book:delete', 2, '', '', NULL, 0, 1, '2025-03-14 18:12:42', '2025-03-14 18:12:42', 0);

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名称',
  `role_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色编码',
  `description` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '角色描述',
  `status` tinyint(4) NULL DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` tinyint(1) NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `role_code`(`role_code` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统角色表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, '普通用户', 'ROLE_USER', '只能进行基本操作', 1, '2025-03-14 18:12:42', '2025-03-14 18:12:42', 0);
INSERT INTO `sys_role` VALUES (2, '管理员', 'ROLE_ADMIN', '可以进行所有操作', 1, '2025-03-14 18:12:42', '2025-03-14 18:12:42', 0);

-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission`  (
  `role_id` bigint(20) NOT NULL COMMENT '角色ID',
  `permission_id` bigint(20) NOT NULL COMMENT '权限ID',
  PRIMARY KEY (`role_id`, `permission_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色权限关联表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role_permission
-- ----------------------------
INSERT INTO `sys_role_permission` VALUES (1, 4);
INSERT INTO `sys_role_permission` VALUES (1, 5);
INSERT INTO `sys_role_permission` VALUES (1, 8);
INSERT INTO `sys_role_permission` VALUES (1, 9);
INSERT INTO `sys_role_permission` VALUES (2, 4);
INSERT INTO `sys_role_permission` VALUES (2, 5);
INSERT INTO `sys_role_permission` VALUES (2, 6);
INSERT INTO `sys_role_permission` VALUES (2, 7);
INSERT INTO `sys_role_permission` VALUES (2, 8);
INSERT INTO `sys_role_permission` VALUES (2, 9);
INSERT INTO `sys_role_permission` VALUES (2, 10);
INSERT INTO `sys_role_permission` VALUES (2, 11);

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
  `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
  `real_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '真实姓名',
  `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号',
  `status` tinyint(4) NULL DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` tinyint(1) NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `username`(`username` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统用户表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'zhangsan', '5380d8acb6a12b67e59e06c235c3d77b', '张三', 'zhangsan@example.com', '13800138000', 1, '2025-03-14 21:16:12', '2025-03-15 18:20:21', 0);
INSERT INTO `sys_user` VALUES (2, 'lisi', '5380d8acb6a12b67e59e06c235c3d77b', '李四', 'lisi@example.com', '13800138001', 1, '2025-03-14 21:16:12', '2025-03-15 18:20:21', 0);

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role`  (
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `role_id` bigint(20) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`user_id`, `role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户角色关联表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (1, 1);
INSERT INTO `sys_user_role` VALUES (2, 2);

SET FOREIGN_KEY_CHECKS = 1;

这样我们基础的访问权限就通过权限code设置完成了,一共两个测试用户分别拥有如下权限:

普通用户(如张三):
用户查询与新增
只能查看图书列表(book:query)
不能新增、修改、删除图书

管理员(如李四):
用户所有权限
可以查看图书列表(book:query)
可以新增图书(book:add)
可以修改图书(book:edit)
可以删除图书(book:delete)

Step2:创建SpringBoot工程引入依赖

这里笔者用SpringBoot3.4.2框架进行搭建,主要的依赖基本围绕satoken,mybatisplus,satoken-jwt(集成jwt)。除此以外还有一些常见的工具包比如hutool等等,根据项目实际情况导入即可:

<dependency>
                <groupId>cn.dev33</groupId>
                <artifactId>sa-token-spring-boot3-starter</artifactId>
                <version>${sa-token.version}</version>
            </dependency>
            <!-- Sa-Token 整合 jwt -->
            <dependency>
                <groupId>cn.dev33</groupId>
                <artifactId>sa-token-jwt</artifactId>
                <version>${sa-token.version}</version>
                <exclusions>
<!--                    避免依赖冲突-->
                    <exclusion>
                        <groupId>cn.hutool</groupId>
                        <artifactId>hutool-all</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-bom</artifactId>
                <version>${hutool.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>cn.dev33</groupId>
                <artifactId>sa-token-core</artifactId>
                <version>${sa-token.version}</version>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
                <version>${mybatis-plus.version}</version>
            </dependency>

然后根据项目需求补充一下application.yml的配置信息:

server:
  port: 8082

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:satoken_demo}?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: ${MYSQL_USERNAME:root}
    password: ${MYSQL_PASSWORD:123456}

mybatis-plus:
  mapper-locations: classpath*:/mapper/*.xml
  type-aliases-package: com.yy.satokenapplication.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0

sa-token:
  # 基础配置
  token-name: Authorization                    # token名称
  timeout: 2592000                      # token有效期(30天)
  active-timeout: -1                  # 临时有效期(-1表示永不过期)(指定时间内无操作就视为token过期) 单位: 秒
  is-concurrent: true                   # 允许并发登录
  is-share: false                      # 不共享token
  token-style: uuid                       # token风格
  is-log: true                         # 输出操作日志
  
  # 请求头配置
  is-read-cookie: false                # 不从cookie读取token
  is-read-header: true                 # 从请求头读取token
#  token-prefix: "Bearer"               # token前缀

  # jwt秘钥
  jwt-secret-key: abcdefghijklmnopqrstuvwxyz

# security配置
security:
  # 排除路径
  excludes:
    - /auth/*
    
encryption:
  key: ${KEY:9876543210123456}       # 16位字符的加密密钥

除了首先需要配置mysql数据源和mybatisplus外主要就是satoken的相关信息了:

  • token - name: Authorization:设置用于存储 token 的请求头名称为Authorization。在进行认证相关操作时,系统会从这个请求头中读取 token 来验证用户身份。​
  • timeout: 2592000:设置 token 的有效期为 2592000 秒,换算后是 30 天。这意味着从 token 生成开始,在 30 天内该 token 都是有效的,超过这个时间,token 将失效,用户需要重新获取 token 进行认证。​
  • active - timeout: - 1:表示临时有效期设置为永不过期。它的含义是在指定时间内如果用户无操作,token 是否过期。这里设置为 - 1,即无论用户多长时间没有操作,token 都不会因为无操作而过期。如果设置为一个具体的秒数,比如 3600,那么用户在 1 小时内无操作,token 就会过期。​
  • is - concurrent: true:允许并发登录。即同一用户账号可以在多个设备或不同会话中同时登录,每个登录会话都会生成一个独立有效的 token。​
  • is - share: false:设置为不共享 token。这意味着不同的登录会话之间的 token 是相互独立的,不会出现一个 token 在多个会话中通用的情况。​
  • token - style: uuid:指定 token 的生成风格为 uuid。uuid 是一种通用唯一识别码,具有全球唯一性,以这种风格生成的 token 能保证在系统中的唯一性,降低 token 冲突的风险。​
  • is - log: true:开启操作日志输出。sa - token 在运行过程中的各种操作,比如 token 的生成、验证、续签等操作都会记录到日志中,方便开发人员进行调试和问题排查。

至于如有其他需要的satoken配置,大家可参考SaToken官网进行补充,这个框架已经出来很久了,在目前市场上很多开源项目上比如若依,Vben的后端上基本都有使用。

Step3:基础业务搭建

笔者这块项目基本按照MVC模式进行搭建,关于通用的实体、dao、mapper、service及实现类就通过MyBatisPlus实现了,只有几个注意点说明一下:

这里为了方便笔者对于登录的用户对象并没有在做封装,而是直接使用的是用户表实体:

package com.yy.satokenapplication.entity;

import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serial;
import java.time.LocalDateTime;
import java.util.List;

@Data
@TableName("sys_user")
@NoArgsConstructor
public class SysUser implements java.io.Serializable {
    @Serial
    private static final long serialVersionUID = -1291541503453934539L;
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private String username;
    
    private String password;
    
    private String realName;
    
    private String email;
    
    private String phone;
    
    private Integer status;
    
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    
    @TableLogic
    private Integer deleted;
    
    @TableField(exist = false)
    private List<SysPermission> permissionsList;


    @TableField(exist = false)
    private List<SysRole> rolesList;

    public SysUser(Long id, List<SysPermission> permissionsList, List<SysRole> rolesList) {
        this.id = id;
        this.permissionsList = permissionsList;
        this.rolesList = rolesList;
    }
} 

可以看到除了表以外我还加上了用户自身的角色集合以及权限集合,这个是需要后面satoken在进行角色,功能权限校验时所需要的。 

为此我们这至少需要一个业务去通过用户id(或者其他属性)去数据查询登录用户本身具备的数据权限及角色权限。所以在SysUserMapper.xml上写一个集合查询来查看用户的权限信息:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yy.satokenapplication.mapper.SysUserMapper">

    <resultMap id="BaseResultMap" type="SysUser">
            <id property="id" column="id" jdbcType="BIGINT"/>
            <result property="username" column="username" jdbcType="VARCHAR"/>
            <result property="password" column="password" jdbcType="VARCHAR"/>
            <result property="realName" column="real_name" jdbcType="VARCHAR"/>
            <result property="email" column="email" jdbcType="VARCHAR"/>
            <result property="phone" column="phone" jdbcType="VARCHAR"/>
            <result property="status" column="status" jdbcType="TINYINT"/>
            <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
            <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
            <result property="deleted" column="deleted" jdbcType="TINYINT"/>
    </resultMap>

    <sql id="Base_Column_List">
        id,username,password,
        real_name,email,phone,
        status,create_time,update_time,
        deleted
    </sql>
    
    <resultMap id="UserWithRolesAndPermissionsResultMap" type="SysUser">
        <id property="id" column="user_id"/>
        <result property="username" column="username"/>
        <collection property="rolesList" ofType="SysRole" select="com.yy.satokenapplication.mapper.SysRoleMapper.findRolesByUserId" column="user_id"/>
        <collection property="permissionsList" ofType="SysPermission" select="com.yy.satokenapplication.mapper.SysPermissionMapper.findPermissionsByUserId" column="user_id"/>
    </resultMap>
    
    <select id="findUserWithRolesAndPermissionsById" resultMap="UserWithRolesAndPermissionsResultMap">
        SELECT u.id as user_id
        FROM sys_user u
        WHERE u.id = #{userId}
    </select>
</mapper>

第一个 <collection> 标签

  • property="rolesList" 表示 SysUser 类中的 rolesList 属性,该属性是一个 List<SysRole> 类型的集合。
  • ofType="SysRole" 表示集合中元素的类型为 SysRole
  • select="com.yy.satokenapplication.mapper.SysRoleMapper.findRolesByUserId" 表示通过调用 SysRoleMapper 接口中的 findRolesByUserId 方法来查询该用户关联的角色列表。
  • column="user_id" 表示将主查询结果集中的 user_id 列的值作为参数传递给 findRolesByUserId 方法。

第二个 <collection> 标签

  • property="permissionsList" 表示 SysUser 类中的 permissionsList 属性,该属性是一个 List<SysPermission> 类型的集合。
  • ofType="SysPermission" 表示集合中元素的类型为 SysPermission
  • select="com.yy.satokenapplication.mapper.SysPermissionMapper.findPermissionsByUserId" 表示通过调用 SysPermissionMapper 接口中的 findPermissionsByUserId 方法来查询该用户关联的权限列表。
  • column="user_id" 表示将主查询结果集中的 user_id 列的值作为参数传递给 findPermissionsByUserId 方法。 

 然后分别去对应的mapper接口中通过mapper.xml文件的SQL查找功能权限集合和角色集合。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yy.satokenapplication.mapper.SysRoleMapper">

    <select id="findRolesByUserId" resultType="SysRole">
        SELECT r.id, r.role_code
        FROM sys_role r
                 JOIN sys_user_role ur ON r.id = ur.role_id
        WHERE ur.user_id = #{userId}
    </select>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yy.satokenapplication.mapper.SysPermissionMapper">

    
    <select id="findPermissionsByUserId" resultType="SysPermission">
        SELECT p.id, p.code
        FROM sys_permission p
                 JOIN sys_role_permission rp ON p.id = rp.permission_id
                 JOIN sys_user_role ur ON rp.role_id = ur.role_id
        WHERE ur.user_id = #{userId}
    </select>
</mapper>

 除此以外,为了方便对satoken的权限校验异常以及后端接口响应数据进行处理,这里建议通过统一异常处理类来处理:

package com.yy.satokenapplication.exception;

import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.hutool.http.HttpStatus;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    @Data
    public static class Result<T> {
        private Integer code;
        private String message;
        private T data;
        
        public static <T> Result<T> fail(Integer code, String message) {
            Result<T> result = new Result<>();
            result.setCode(code);
            result.setMessage(message);
            return result;
        }
        
        public static <T> Result<T> success(T data) {
            Result<T> result = new Result<>();
            result.setCode(200);
            result.setMessage("success");
            result.setData(data);
            return result;
        }
    }
    

    /**
     * 权限码异常
     */
    @ExceptionHandler(NotPermissionException.class)
    public Result<Void> handleNotPermissionException(NotPermissionException e, HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',权限码校验失败'{}'", requestURI, e.getMessage());
        return Result.fail(HttpStatus.HTTP_FORBIDDEN, "没有访问权限,请联系管理员授权");
    }

    /**
     * 角色权限异常
     */
    @ExceptionHandler(NotRoleException.class)
    public Result<Void> handleNotRoleException(NotRoleException e, HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',角色权限校验失败'{}'", requestURI, e.getMessage());
        return Result.fail(HttpStatus.HTTP_FORBIDDEN, "没有访问权限,请联系管理员授权");
    }

    /**
     * 认证失败
     */
    @ExceptionHandler(NotLoginException.class)
    public Result<Void> handleNotLoginException(NotLoginException e, HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage());
        return Result.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源");
    }} 

Step4:SaToken集成JWT配置

除了yml文件中的satoken配置外我们还需要在项目中补充satoken集成jwt的配置:

package com.yy.satokenapplication.config;

import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.extra.spring.SpringUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author young
 * Date 2025/3/14 下午8:49
 * Description: sa-token 配置类
 */
@Slf4j
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
@RequiredArgsConstructor
public class SaTokenConfig implements WebMvcConfigurer {

    private final SecurityProperties securityProperties;
    
    @Bean
    public StpLogic getStpLogicJwt() {
        // Sa-Token 整合 jwt (简单模式)
        return new StpLogicJwtForSimple();
    }

    /**
     * 权限接口实现
     */
    @Bean
    public StpInterface stpInterface() {
        return new SaPermissionImpl();
    }


    /**
     * 注册sa-token的拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册路由拦截器,自定义验证规则
        registry.addInterceptor(new SaInterceptor(handler -> {
                    AllUrlHandler allUrlHandler = SpringUtil.getBean(AllUrlHandler.class);
                    // 登录验证 -- 排除多个路径
                    // 检查是否登录 是否有token
                    SaRouter
                            // 获取所有的
                            .match(allUrlHandler.getUrls())
                            // 对未排除的路径进行检查
                            .check(StpUtil::checkLogin);
                })).addPathPatterns("/**")
                // 排除不需要拦截的路径
                .excludePathPatterns(securityProperties.getExcludes());
    }
}

首先要配置satoken集成jwt的模式策略,这里笔者以简单模式为例子,当然除了这个模式外,satoken还支持混入以及无状态等模式,注入不同模式会让框架具有不同的行为策略,以下是三种模式的差异点(为方便叙述,以下比较以同时引入 jwt 与 Redis 作为前提):

功能点 Simple 简单模式 Mixin 混入模式 Stateless 无状态模式
Token风格 jwt风格 jwt风格 jwt风格
登录数据存储 Redis中存储 Token中存储 Token中存储
Session存储 Redis中存储 Redis中存储 无Session
注销下线 前后端双清数据 前后端双清数据 前端清除数据
踢人下线API 支持 不支持 不支持
顶人下线API 支持 不支持 不支持
登录认证 支持 支持 支持
角色认证 支持 支持 支持
权限认证 支持 支持 支持
timeout 有效期 支持 支持 支持
active-timeout 有效期 支持 支持 不支持
id反查Token 支持 支持 不支持
会话管理 支持 部分支持 不支持
注解鉴权 支持 支持 支持
路由拦截鉴权 支持 支持 支持
账号封禁 支持 支持 不支持
身份切换 支持 支持 支持
二级认证 支持 支持 支持
模式总结 Token风格替换 jwt 与 Redis 逻辑混合 完全舍弃Redis,只用jwt

大家可根据使用需求自行调整。然后需要定义一个权限管理接口SaPermissionImpl:

package com.yy.satokenapplication.config;

import cn.dev33.satoken.stp.StpInterface;
import cn.hutool.core.util.ObjectUtil;
import com.yy.satokenapplication.entity.SysPermission;
import com.yy.satokenapplication.entity.SysRole;
import com.yy.satokenapplication.entity.SysUser;
import com.yy.satokenapplication.mapper.SysUserMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;

import java.util.Collections;
import java.util.List;

/**
 * @author young
 * Date 2025/3/15 下午9:57
 * Description: 权限管理实现类
 */
@Slf4j
public class SaPermissionImpl implements StpInterface {

    @Resource
    private SysUserMapper sysUserMapper;


    private SysUser getUserInfo(Long userId) {
        SysUser user = sysUserMapper.findUserWithRolesAndPermissionsById(userId);
        if (ObjectUtil.isEmpty(user)) {
            log.error("用户信息为空,无法获取用户信息!");
        }
        return user;
    }


    /**
     * 获取菜单权限列表
     */
    @Override
    public List<String> getPermissionList(Object loginId, String s) {
        SysUser userInfo = getUserInfo(Long.parseLong(loginId.toString()));
        if (ObjectUtil.isEmpty(userInfo)){
            log.error("用户的菜单权限为空,无法获取菜单权限列表,用户登录ID: {}", loginId);
            return Collections.emptyList();
        }
        return userInfo.getPermissionsList().stream().map(SysPermission::getCode).toList();
    }

    /**
     * 获取角色权限列表
     */
    @Override
    public List<String> getRoleList(Object loginId, String s) {
        Long userId = Long.parseLong(loginId.toString());
        SysUser userInfo = getUserInfo(userId);
        if (ObjectUtil.isEmpty(userInfo)) {
            log.error("用户角色信息为空,无法获取角色列表,用户ID: {}", userId);
            return Collections.emptyList(); // 返回空列表
        }
        return userInfo.getRolesList().stream().map(SysRole::getRoleCode).toList();
    }
}

这样当使用 Sa-Token 进行权限验证时,Sa-Token 会自动调用 StpInterface 实现类中的方法获取当前登录用户的权限列表和角色列表,然后用户在访问带有权限标识的接口时,自动检查 用户权限标识或者角色是否拥有访问权限。

最后就是一般比较熟悉的拦截器配置了,对全局接口进行拦截,确保jwt和权限进行接口验证。

Sa-Token使用全局拦截器完成注解鉴权功能,为了不为项目带来不必要的性能负担,拦截器默认处于关闭状态。因此,为了使用注解鉴权,我们必须手动将 Sa-Token 的全局拦截器注册到你项目中。

这里笔者通过yml文件中的拦截配置进行设置,只放行了认证相关的接口(/auth/*),根据需求配置即可。

Step5:完成后端登录认证接口及数据校验接口

注册完拦截器后,我们可以在后端的controller接口中通过注解的方式指定接口的功能权限code。

SaToken的注解鉴权可以帮我们优雅的将鉴权与业务代码分离!

  • @SaCheckLogin: 登录校验 —— 只有登录之后才能进入该方法。
  • @SaCheckRole("admin"): 角色校验 —— 必须具有指定角色标识才能进入该方法。
  • @SaCheckPermission("user:add"): 权限校验 —— 必须具有指定权限才能进入该方法。
  • @SaCheckSafe: 二级认证校验 —— 必须二级认证之后才能进入该方法。
  • @SaCheckHttpBasic: HttpBasic校验 —— 只有通过 HttpBasic 认证后才能进入该方法。
  • @SaCheckHttpDigest: HttpDigest校验 —— 只有通过 HttpDigest 认证后才能进入该方法。
  • @SaIgnore:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。
  • @SaCheckDisable("comment"):账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。

然后将用户登录的认证接口和数据(数据库中的book信息)处理的接口完成:

认证接口(/auth/*)的控制器:

package com.yy.satokenapplication.controller;

import cn.dev33.satoken.stp.StpUtil;
import com.yy.satokenapplication.entity.SysUser;
import com.yy.satokenapplication.exception.GlobalExceptionHandler.Result;
import com.yy.satokenapplication.model.request.LoginRequest;
import com.yy.satokenapplication.model.response.LoginResponse;
import com.yy.satokenapplication.service.impl.SysUserServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
    
    private final SysUserServiceImpl userService;

    
    @PostMapping("/login")
    public Result<LoginResponse> login(@RequestBody LoginRequest request) {

        // 验证用户名和密码
        SysUser user = userService.getUserByUsername(request.getUsername());
        if (user == null || !userService.checkPassword(request.getPassword(), user.getPassword())) {
            return Result.fail(401, "用户名或密码错误");
        }
        
        // 登录成功,生成token
        StpUtil.login(user.getId());
        //如果想让token携带更多信息,可以使用SaLoginModel或者SaLoginConfig来设置更多信息,比如:
        // StpUtil.login(user.getId(), SaLoginConfig.setDevice("web").setExtra("ip", "127.0.0.1"));
        
        // 返回登录信息
        LoginResponse response = new LoginResponse();
        response.setToken(StpUtil.getTokenValue());
        return Result.success(response);
    }
    
    @PostMapping("/logout") 
    public Result<String> logout(@RequestParam(name = "userId") Long userId) {
        StpUtil.logout(userId);
        return Result.success("用户已退出");
    }
} 

 数据接口(/api/book/*)的控制器:

package com.yy.satokenapplication.controller;

import cn.dev33.satoken.annotation.SaCheckPermission;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yy.satokenapplication.entity.SysBook;
import com.yy.satokenapplication.service.SysBookService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/book")
@RequiredArgsConstructor
public class BookController {
    
    private final SysBookService bookService;
    
    @GetMapping("/list")
    @SaCheckPermission("book:query")
    public Page<SysBook> list(
            @RequestParam(name = "current", defaultValue = "1") Integer current,
            @RequestParam(name = "size", defaultValue = "10") Integer size,
            @RequestParam(name = "title", required = false) String title) {
        
        Page<SysBook> page = new Page<>(current, size);
        LambdaQueryWrapper<SysBook> wrapper = new LambdaQueryWrapper<>();
        if (title != null) {
            wrapper.like(SysBook::getTitle, title);
        }
        return bookService.page(page, wrapper);
    }
    
    @PostMapping("/add")
    @SaCheckPermission("book:add")
    public boolean add(@RequestBody SysBook book) {
        return bookService.save(book);
    }
    
    @PutMapping("/edit")
    @SaCheckPermission("book:edit")
    public boolean edit(@RequestBody SysBook book) {
        return bookService.updateById(book);
    }
    
    @DeleteMapping("/delete/{id}")
    @SaCheckPermission("book:delete")
    public boolean delete(@PathVariable(name = "id") Long id) {
        return bookService.removeById(id);
    }
} 

最后的项目结构树如下:

src
  └─ main
    └─ java
      └─ com
        └─ yy
          └─ satokenapplication
            └─ config
              └─ AllUrlHandler.java
              └─ SaPermissionImpl.java
              └─ SaTokenConfig.java
              └─ SecurityProperties.java
            └─ controller
              └─ AuthController.java
              └─ BookController.java
              └─ UserController.java
            └─ entity
              └─ SysBook.java
              └─ SysPermission.java
              └─ SysRole.java
              └─ SysRolePermission.java
              └─ SysUser.java
              └─ SysUserRole.java
            └─ exception
              └─ GlobalExceptionHandler.java
            └─ mapper
              └─ SysBookMapper.java
              └─ SysPermissionMapper.java
              └─ SysRoleMapper.java
              └─ SysUserMapper.java
            └─ model
              └─ request
                └─ LoginRequest.java
              └─ response
                └─ LoginResponse.java
            └─ SatokenApplication.java
            └─ service
              └─ impl
                └─ SysBookServiceImpl.java
                └─ SysPermissionServiceImpl.java
                └─ SysRoleServiceImpl.java
                └─ SysUserServiceImpl.java
              └─ SysBookService.java
              └─ SysPermissionService.java
              └─ SysRoleService.java
              └─ SysUserService.java
            └─ util
              └─ PasswordEncryptor.java
              └─ RedisUtils.java
    └─ resources
      └─ application.yml
      └─ db
        └─ schema.sql
      └─ mapper
        └─ SysPermissionMapper.xml
        └─ SysRoleMapper.xml
        └─ SysUserMapper.xml

Step6:权限验证测试

我们采用测试工具直接建立一条自动化测试,并且将相关接口添加进去,这里笔者加入了用户登录认证接口用于获取token,book数据的查询接口、编辑接口,用户数据的查看接口和删除接口用于测试:

需要注意的是,由于我们继承了JWT,并且在yml文件中设置了token-name为 Authorization,所以测试时要在认证后添加一个脚本或者后执行操作,将认证返回的token添加到后续接口的请求头,避免后续接口被JWT给拦截了。

这里我们先以拥有所有权限测试账户李四(登录id为2)来进行测试:

可以看到所有接口均测试完成并且响应正常。

然后用普通测试用户张三(登录id为1)测试:

可以看到虽然接口测试正常,但是由于这个账户并没有编辑权限所以他被权限管理给拦截了。查看一下控制台日志便可得出详细信息:

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25937085] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@201887029 wrapping com.mysql.cj.jdbc.ConnectionImpl@3c3a1f66] will not be managed by Spring
==>  Preparing: SELECT id,username,password,real_name,email,phone,status,create_time,update_time,deleted FROM sys_user WHERE deleted=0 AND (username = ?)
==> Parameters: zhangsan(String)
<==    Columns: id, username, password, real_name, email, phone, status, create_time, update_time, deleted
<==        Row: 1, zhangsan, 5380d8acb6a12b67e59e06c235c3d77b, 张三, zhangsan@example.com, 13800138000, 1, 2025-03-14 21:16:12, 2025-03-15 18:20:21, 0
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25937085]
SA [INFO] -->: SaSession [Authorization:login:session:1] 创建成功
SA [INFO] -->: 账号 1 登录成功 (loginType=login), 会话凭证 token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjEsInJuU3RyIjoiamFSNjBucU51VU5uOUdnakh5TmZ2YUpqdHlPYzRXalgifQ.z8hPv4ZlbfzMPgdKXiDUs674U6Lp0gIY7J_qX2cNgBY
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@60b4dea8] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@783341916 wrapping com.mysql.cj.jdbc.ConnectionImpl@3c3a1f66] will not be managed by Spring
==>  Preparing: SELECT u.id as user_id FROM sys_user u WHERE u.id = ?
==> Parameters: 1(Long)
<==    Columns: user_id
<==        Row: 1
====>  Preparing: SELECT r.id, r.role_code FROM sys_role r JOIN sys_user_role ur ON r.id = ur.role_id WHERE ur.user_id = ?
====> Parameters: 1(Long)
<====    Columns: id, role_code
<====        Row: 1, ROLE_USER
<====      Total: 1
====>  Preparing: SELECT p.id, p.code FROM sys_permission p JOIN sys_role_permission rp ON p.id = rp.permission_id JOIN sys_user_role ur ON rp.role_id = ur.role_id WHERE ur.user_id = ?
====> Parameters: 1(Long)
<====    Columns: id, code
<====        Row: 4, system:user:query
<====        Row: 5, system:user:add
<====        Row: 8, book
<====        Row: 9, book:query
<====      Total: 4
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@60b4dea8]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6b9c799b] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1199835256 wrapping com.mysql.cj.jdbc.ConnectionImpl@3c3a1f66] will not be managed by Spring
==>  Preparing: SELECT id,title,author,isbn,price,publish_date,description,status,create_time,update_time,deleted FROM sys_book WHERE deleted=0 AND (title LIKE ?)
==> Parameters: %%(String)
<==    Columns: id, title, author, isbn, price, publish_date, description, status, create_time, update_time, deleted
<==        Row: 1, , , , 1.00, 2025-03-16, <<BLOB>>, 1, 2025-03-16 21:40:53, 2025-03-16 21:40:53, 0
<==        Row: 2, Spring实战, Craig Walls, 9787115417305, 89.00, 2016-04-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 3, 算法导论, Thomas H.Cormen, 9787111187776, 128.00, 2006-09-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 4, 深入理解Java虚拟机, 周志明, 9787111641247, 129.00, 2019-12-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 5, MySQL必知必会, Ben Forta, 9787115191120, 59.00, 2009-01-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 6, Redis设计与实现, 黄健宏, 978711557975, 79.00, 2014-06-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 7, 设计模式, Erich Gamma, 9787111346365, 35.00, 2009-10-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 8, 计算机网络, 谢希仁, 9787115547615, 49.00, 2021-06-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 9, 操作系统概念, Abraham Silberschatz, 9787111641248, 99.00, 2018-03-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 10, 数据结构与算法分析, Mark Allen Weiss, 9787111641249, 89.00, 2019-05-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 11, 微服务架构设计模式, Chris Richardson, 9787111641250, 119.00, 2020-01-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 12, Docker容器与容器云, 张磊, 9787111641251, 89.00, 2016-08-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 13, Kubernetes权威指南, 龚正, 9787111641252, 139.00, 2019-10-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 14, Elasticsearch实战, Radu Gheorghe, 9787111641253, 99.00, 2018-12-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 15, RabbitMQ实战指南, 朱忠华, 9787111641254, 79.00, 2017-06-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 16, Nginx高性能Web服务器详解, 陶辉, 9787111641255, 89.00, 2016-03-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 17, Linux系统编程, Robert Love, 9787111641256, 109.00, 2015-09-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 18, Python机器学习, Sebastian Raschka, 9787111641257, 119.00, 2017-12-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 19, Go语言实战, William Kennedy, 9787111641258, 89.00, 2016-11-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 20, 区块链技术指南, 邹均, 9787111641259, 129.00, 2018-05-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 21, Hospitalization, 雪白, Notwithstanding, 200.00, 2025-02-17, <<BLOB>>, 7, null, null, 0
<==      Total: 21
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6b9c799b]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@24f624a7] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@41336765 wrapping com.mysql.cj.jdbc.ConnectionImpl@3c3a1f66] will not be managed by Spring
==>  Preparing: SELECT u.id as user_id FROM sys_user u WHERE u.id = ?
==> Parameters: 1(Long)
<==    Columns: user_id
<==        Row: 1
====>  Preparing: SELECT r.id, r.role_code FROM sys_role r JOIN sys_user_role ur ON r.id = ur.role_id WHERE ur.user_id = ?
====> Parameters: 1(Long)
<====    Columns: id, role_code
<====        Row: 1, ROLE_USER
<====      Total: 1
====>  Preparing: SELECT p.id, p.code FROM sys_permission p JOIN sys_role_permission rp ON p.id = rp.permission_id JOIN sys_user_role ur ON rp.role_id = ur.role_id WHERE ur.user_id = ?
====> Parameters: 1(Long)
<====    Columns: id, code
<====        Row: 4, system:user:query
<====        Row: 5, system:user:add
<====        Row: 8, book
<====        Row: 9, book:query
<====      Total: 4
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@24f624a7]
2025-03-17T18:36:40.293+08:00 ERROR 6728 --- [nio-8082-exec-9] c.y.s.exception.GlobalExceptionHandler   : 请求地址'/api/book/edit',权限码校验失败'无此权限:book:edit'
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@70fd2b1e] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@134722444 wrapping com.mysql.cj.jdbc.ConnectionImpl@3c3a1f66] will not be managed by Spring
==>  Preparing: SELECT u.id as user_id FROM sys_user u WHERE u.id = ?
==> Parameters: 1(Long)
<==    Columns: user_id
<==        Row: 1
====>  Preparing: SELECT r.id, r.role_code FROM sys_role r JOIN sys_user_role ur ON r.id = ur.role_id WHERE ur.user_id = ?
====> Parameters: 1(Long)
<====    Columns: id, role_code
<====        Row: 1, ROLE_USER
<====      Total: 1
====>  Preparing: SELECT p.id, p.code FROM sys_permission p JOIN sys_role_permission rp ON p.id = rp.permission_id JOIN sys_user_role ur ON rp.role_id = ur.role_id WHERE ur.user_id = ?
====> Parameters: 1(Long)
<====    Columns: id, code
<====        Row: 4, system:user:query
<====        Row: 5, system:user:add
<====        Row: 8, book
<====        Row: 9, book:query
<====      Total: 4
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@70fd2b1e]
2025-03-17T18:36:40.767+08:00  INFO 6728 --- [io-8082-exec-10] c.y.s.controller.UserController          : 查询用户列表
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3764a917] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@2027443151 wrapping com.mysql.cj.jdbc.ConnectionImpl@3c3a1f66] will not be managed by Spring
==>  Preparing: SELECT u.id as user_id FROM sys_user u WHERE u.id = ?
==> Parameters: 1(Long)
<==    Columns: user_id
<==        Row: 1
====>  Preparing: SELECT r.id, r.role_code FROM sys_role r JOIN sys_user_role ur ON r.id = ur.role_id WHERE ur.user_id = ?
====> Parameters: 1(Long)
<====    Columns: id, role_code
<====        Row: 1, ROLE_USER
<====      Total: 1
====>  Preparing: SELECT p.id, p.code FROM sys_permission p JOIN sys_role_permission rp ON p.id = rp.permission_id JOIN sys_user_role ur ON rp.role_id = ur.role_id WHERE ur.user_id = ?
====> Parameters: 1(Long)
<====    Columns: id, code
<====        Row: 4, system:user:query
<====        Row: 5, system:user:add
<====        Row: 8, book
<====        Row: 9, book:query
<====      Total: 4
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3764a917]
2025-03-17T18:36:41.190+08:00 ERROR 6728 --- [nio-8082-exec-1] c.y.s.exception.GlobalExceptionHandler   : 请求地址'/api/user/delete',权限码校验失败'无此权限:system:user:delete'

 普通用户仅查看和认证接口正常的,而编辑功能与删除功能由于权限不足均被拦截了。

Step7:细节优化

虽然整体权限集成流程完成了,但是细心地小伙伴应该不难发现这中权限验证中其实是有明显性能缺陷的:在上面的日志中不难发现--->在配置了Sa-Token拦截器后,每次请求接口时都需要验证权限和角色,内部会调用getPermissionList和getRoleList方法来获取用户的权限集合和角色集合。

如果这些数据不发生变化,每次从数据库中获取这些信息会导致不必要的数据库访问,影响系统的性能。

为了优化这个问题,我们可以引入缓存机制。具体来说,可以在获取用户的权限集合和角色集合后,将这些信息存储到缓存中。对于后续的请求,可以先从缓存中获取这些信息,如果缓存中有数据就直接使用,不再查询数据库。这样可以大大减少数据库访问次数,提高系统的响应速度。

一般可以通过redis或者Spring本地缓存来实现,这里笔者就以本地缓存引入Caffine来进行调整。Caffeine是一种基于Java的高性能缓存库,它提供了快速、高效的本地缓存解决方案。

先引入caffine对应的依赖:

            <!-- Caffeine -->
            <dependency>
                <groupId>com.github.ben-manes.caffeine</groupId>
                <artifactId>caffeine</artifactId>
                <version>${caffeine.version}</version>
            </dependency>

接着开启缓存并配置caffine初始化信息及缓存管理:

package com.yy.satokenapplication.config;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.interceptor.CacheResolver;
import org.springframework.cache.interceptor.SimpleCacheErrorHandler;
import org.springframework.cache.interceptor.SimpleCacheResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.github.benmanes.caffeine.cache.Caffeine;

import java.util.concurrent.TimeUnit;

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("permissions", "roles");
        cacheManager.setCaffeine(caffeineCacheBuilder());
        return cacheManager;
    }

    Caffeine<Object, Object> caffeineCacheBuilder() {
        return Caffeine.newBuilder()
            .expireAfterWrite(30, TimeUnit.MINUTES) // 设置缓存过期时间
            .maximumSize(1000); // 设置缓存最大容量
    }
    
}

至于多余的缓存管理细节这里就不多赘述了,自行根据需求配置即可。然后优化satoken的权限拦截,引入缓存处理:

package com.yy.satokenapplication.config;

import cn.dev33.satoken.stp.StpInterface;
import cn.hutool.core.util.ObjectUtil;
import com.yy.satokenapplication.entity.SysPermission;
import com.yy.satokenapplication.entity.SysRole;
import com.yy.satokenapplication.entity.SysUser;
import com.yy.satokenapplication.mapper.SysUserMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;

import java.util.Collections;
import java.util.List;

@Slf4j
public class SaPermissionImpl implements StpInterface {

    @Resource
    private SysUserMapper sysUserMapper;
    

    private SysUser getUserInfo(Long userId) {
        SysUser user = sysUserMapper.findUserWithRolesAndPermissionsById(userId);
        if (ObjectUtil.isEmpty(user)) {
            log.error("用户信息为空,无法获取用户信息!");
        }
        return user;
    }

    /**
     * 获取菜单权限列表
     */
    @Override
    @Cacheable(value = "permissions", key = "#loginId")
    public List<String> getPermissionList(Object loginId, String s) {
        SysUser userInfo = getUserInfo(Long.parseLong(loginId.toString()));
        if (ObjectUtil.isEmpty(userInfo)) {
            log.error("用户的菜单权限为空,无法获取菜单权限列表,用户登录ID: {}", loginId);
            return Collections.emptyList();
        }
        return userInfo.getPermissionsList().stream().map(SysPermission::getCode).toList();
    }

    /**
     * 获取角色权限列表
     */
    @Override
    @Cacheable(value = "roles", key = "#loginId")
    public List<String> getRoleList(Object loginId, String s) {
        Long userId = Long.parseLong(loginId.toString());
        SysUser userInfo = getUserInfo(userId);
        if (ObjectUtil.isEmpty(userInfo)) {
            log.error("用户角色信息为空,无法获取角色列表,用户ID: {}", userId);
            return Collections.emptyList(); // 返回空列表
        }
        return userInfo.getRolesList().stream().map(SysRole::getRoleCode).toList();
    }
    
}

最后可以将用户登出接口简单处理一下,使其清除本地缓存就OK了:

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {

    @Resource
    private CacheManager cacheManager;
    

    
    @PostMapping("/logout") 
    public Result<String> logout(@RequestParam(name = "userId") Long userId) {
        cacheManager.getCache("permissions").evict(userId);
        cacheManager.getCache("roles").evict(userId);
        StpUtil.logout(userId);
        return Result.success("用户已退出");
    }
} 

这样就能避免satoken拦截时频繁去请求数据库以获取用户权限信息了。

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@32006d5f] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1534437262 wrapping com.mysql.cj.jdbc.ConnectionImpl@497e3086] will not be managed by Spring
==>  Preparing: SELECT id,username,password,real_name,email,phone,status,create_time,update_time,deleted FROM sys_user WHERE deleted=0 AND (username = ?)
==> Parameters: lisi(String)
<==    Columns: id, username, password, real_name, email, phone, status, create_time, update_time, deleted
<==        Row: 2, lisi, 5380d8acb6a12b67e59e06c235c3d77b, 李四, lisi@example.com, 13800138001, 1, 2025-03-14 21:16:12, 2025-03-15 18:20:21, 0
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@32006d5f]
SA [INFO] -->: 账号 2 登录成功 (loginType=login), 会话凭证 token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjIsInJuU3RyIjoibEtuYXhuY3N0YWhiUFp6N2Z2WUlPNjNzVW5XSHhsOFMifQ.YeNB1H3oM8Snuuyf4ZeGURuOqT5S91azVTCd8SFaLrI
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4282ab2a] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@225990449 wrapping com.mysql.cj.jdbc.ConnectionImpl@497e3086] will not be managed by Spring
==>  Preparing: SELECT id,title,author,isbn,price,publish_date,description,status,create_time,update_time,deleted FROM sys_book WHERE deleted=0 AND (title LIKE ?)
==> Parameters: %%(String)
<==    Columns: id, title, author, isbn, price, publish_date, description, status, create_time, update_time, deleted
<==        Row: 1, , , , 1.00, 2025-03-16, <<BLOB>>, 1, 2025-03-16 21:40:53, 2025-03-16 21:40:53, 0
<==        Row: 2, Spring实战, Craig Walls, 9787115417305, 89.00, 2016-04-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 3, 算法导论, Thomas H.Cormen, 9787111187776, 128.00, 2006-09-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 4, 深入理解Java虚拟机, 周志明, 9787111641247, 129.00, 2019-12-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 5, MySQL必知必会, Ben Forta, 9787115191120, 59.00, 2009-01-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 6, Redis设计与实现, 黄健宏, 978711557975, 79.00, 2014-06-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 7, 设计模式, Erich Gamma, 9787111346365, 35.00, 2009-10-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 8, 计算机网络, 谢希仁, 9787115547615, 49.00, 2021-06-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 9, 操作系统概念, Abraham Silberschatz, 9787111641248, 99.00, 2018-03-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 10, 数据结构与算法分析, Mark Allen Weiss, 9787111641249, 89.00, 2019-05-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 11, 微服务架构设计模式, Chris Richardson, 9787111641250, 119.00, 2020-01-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 12, Docker容器与容器云, 张磊, 9787111641251, 89.00, 2016-08-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 13, Kubernetes权威指南, 龚正, 9787111641252, 139.00, 2019-10-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 14, Elasticsearch实战, Radu Gheorghe, 9787111641253, 99.00, 2018-12-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 15, RabbitMQ实战指南, 朱忠华, 9787111641254, 79.00, 2017-06-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 16, Nginx高性能Web服务器详解, 陶辉, 9787111641255, 89.00, 2016-03-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 17, Linux系统编程, Robert Love, 9787111641256, 109.00, 2015-09-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 18, Python机器学习, Sebastian Raschka, 9787111641257, 119.00, 2017-12-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 19, Go语言实战, William Kennedy, 9787111641258, 89.00, 2016-11-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 20, 区块链技术指南, 邹均, 9787111641259, 129.00, 2018-05-01, <<BLOB>>, 1, 2025-03-14 18:55:46, 2025-03-14 18:55:46, 0
<==        Row: 21, Hospitalization, 雪白, Notwithstanding, 200.00, 2025-02-17, <<BLOB>>, 7, null, null, 0
<==      Total: 21
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4282ab2a]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@626058b6] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1822927498 wrapping com.mysql.cj.jdbc.ConnectionImpl@497e3086] will not be managed by Spring
==>  Preparing: UPDATE sys_book SET title=?, author=?, isbn=?, price=?, publish_date=?, description=?, status=?, create_time=?,update_time=? WHERE id=? AND deleted=0
==> Parameters: (String), (String), (String), 1(BigDecimal), 2025-03-16(LocalDate), (String), 1(Integer), 2025-03-16T21:40:53(LocalDateTime), 2025-03-16T21:40:53(LocalDateTime), 1(Long)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@626058b6]
2025-03-17T19:21:07.077+08:00  INFO 16784 --- [nio-8082-exec-9] c.y.s.controller.UserController          : 查询用户列表
2025-03-17T19:21:07.550+08:00  INFO 16784 --- [io-8082-exec-10] c.y.s.controller.UserController          : 删除用户

四、总结

通过上述集成流程,基本上我们可以在项目中不仅实现了轻量化的权限认证,极大地减轻了系统负担,还能确保了功能权限校验的高度准确性和高效性,为应用的安全访问控制打造了坚不可摧的防线。

最后对于该框架的使用选型,有几点小建议可供大家参考:

团队技术栈与经验:若团队缺乏 SpringSecurity 深度使用经验,学习成本可能较高。而 Sa-Token 学习门槛低,上手快,能快速让团队成员投入开发,这种情况下 Sa-Token 是不错的选择。​

架构搭建效率需求:如果项目需要快速实现 RBAC(基于角色的访问控制)+JWT 的基础架构,Sa-Token 提供了丰富且易用的功能模块,能大大缩短开发周期,满足快速搭建架构的需求。​

性能考量:在高并发场景下,鉴权性能至关重要。Sa-Token 经过优化,具备出色的性能表现,能够很好地应对高并发场景下的鉴权性能要求,保障系统稳定运行。​

业务场景复杂度:当存在多端登录(如 Web 端、移动端同时登录)、临时权限(限时访问特定资源)等复杂场景时,Sa-Token 强大的扩展性和灵活的权限控制机制,使其能够轻松应对这些复杂场景,为系统提供可靠的权限管理支持。

希望文章对初步接触权限管理的小伙伴有所帮助~

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐