当前位置: 首页 > news >正文

【鉴权架构】SpringBoot + Sa-Token + MyBatis + MySQL + Redis 实现用户鉴权、角色管理、权限管理

sa-token 官方文档

基础登录功能

  1. 引入依赖

由于使用SpringBoot3,故使用较新版

        <!-- Sa-Token 权限认证,在线文档:https://sa-token.cc --><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-boot3-starter</artifactId><version>1.44.0</version></dependency>
  1. yml配置

前端小程序使用,故禁用session,使用token模式

# 用户鉴权
sa-token:# token 名称# Authorization: Bearer token值token-name: Authorizationtoken-prefix: Bearer       # 设置Token前缀 【注意有空格】is-read-header: true          # 从Header读取Tokenis-read-cookie: false         # 禁用Cookie(纯Token模式)# token 有效期(单位:秒) 默认30天,-1 代表永久有效timeout: 2592000# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结active-timeout: -1# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)is-concurrent: true# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)is-share: false# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)token-style: uuid# 是否输出操作日志is-log: true
  1. 全局拦截器 拦截未登录用户

新建全局拦截器 实现 WebMvcConfigurer 接口

@Configuration
public class SaTokenConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册Sa-Token拦截器registry.addInterceptor(new SaInterceptor(handler -> {SaRouter.match("/**").notMatch(// 用户认证"/user/login","/user/register",// Swagger/Knife4j 接口文档"/doc.html","/webjars/**","/swagger-resources/**","/v3/api-docs","/v3/api-docs/**","/favicon.ico",// 其他需要放行的路径"/error").check(StpUtil::checkLogin); // 校验是否登录 (调试阶段可放开)})).addPathPatterns("/**");}
}
  1. 在用户登录服务中使用 StpUtil 服务进行登录

satoken 会在内存中保存 id 和 token,标记用户登录

StpUtil.login(user.getId()); // 生成Token并保存
    @Overridepublic LoginUserVO userLogin(String userAccount, String userPassword) {// 1. 校验if (StrUtil.hasBlank(userAccount, userPassword)) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");}if (userAccount.length() < 4) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号错误");}if (userPassword.length() < 8) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");}// 2. 加密String encryptPassword = getEncryptPassword(userPassword);// 查询用户是否存在QueryWrapper queryWrapper = new QueryWrapper();queryWrapper.eq("userAccount", userAccount);queryWrapper.eq("userPassword", encryptPassword);User user = this.mapper.selectOneByQuery(queryWrapper);// 用户不存在if (user == null) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户不存在或密码错误");}// 3. SaToken登录StpUtil.login(user.getId()); // 生成Token并保存// 4. 返回信息并携带 tokenLoginUserVO loginUserVO = this.getLoginUserVO(user);loginUserVO.setToken(StpUtil.getTokenValue());return loginUserVO;}

引入Redis

由于satoken默认将数据存储与内存中,每次重启都需要重新登录,即不方便

故引入redis进行持久化存储

  1. 引入Redis
<!-- Redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
  1. 引入 Satoken 的redis相关依赖
        <!-- Sa-Token 整合 Redis --><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-redis-jackson</artifactId><version>1.44.0</version></dependency><!-- 提供Redis连接池 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency>
  1. yml配置redis

注:spring3 的redis配置在 spring/data 下

srping:# Redis 配置data:redis:host: localhostport: 6379password:
  1. 启动redis
redis-server

无需修改代码,可以看到登录后 satoken会自动将数据存储在redis中

SaToken基础注解鉴权

  1. sa-token 接口设置权限就是使用 注解
@SaCheckLogin // 登录才能访问
@SaCheckPermission("user:create")  // 权限标识鉴权
@SaCheckRole("super-admin") // 角色标识鉴权// 注解式鉴权:只要具有其中一个权限即可通过校验
@SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR) 
    @ApiOperationSupport(order = 4)@SaCheckLogin // 登录才能访问@Operation(summary = "获取当前登录用户信息")@GetMapping("/get/login")public BaseResponse<LoginUserVO> getLoginUser() {User loginUser = userService.getLoginUser();return ResultUtils.success(userService.getLoginUserVO(loginUser));}
  1. 实现 StpInterface 接口,在用户登录时 获取其权限并注入

重写 getPermissionList 和 getRoleList 方法,在用户登录时

sa-token 会调用这两个方法,注入用户角色和权限

我这里读取数据库,可以写死数据进行模拟

@Component
public class StpInterfaceConfig implements StpInterface {@Resourceprivate UserService userService;@Resourceprivate RoleService roleService;/***  一个账号拥有的权限码 集合** @return*/@Overridepublic List<String> getPermissionList(Object loginId, String loginType) {Long userId = Long.parseLong(loginId.toString());String roleKey = userService.getRoleKeyByUserId(userId);return roleService.getPermKeyListByRoleKey(roleKey);}/***  一个账号拥有的角色** @return*/@Overridepublic List<String> getRoleList(Object loginId, String loginType) {Long userId = Long.parseLong(loginId.toString());return List.of(userService.getRoleKeyByUserId(userId));}
}
  1. 只要注入的角色/权限与 注解对应,即可访问相应接口

Sa-Token + Mysql 鉴权

这部分是数据库相关内容,之前sa-token相关配置已经完成

这部分主要是sql设计、相关服务、接口

数据库设计

设计 用户表、角色表、权限表、港口表(部门)

-- 用户表
CREATE TABLE IF NOT EXISTS user (id BIGINT AUTO_INCREMENT COMMENT 'id' PRIMARY KEY,user_account VARCHAR(256) NOT NULL COMMENT '账号',user_password VARCHAR(512) NOT NULL COMMENT '密码',user_name VARCHAR(256) NULL COMMENT '用户昵称',user_avatar VARCHAR(1024) NULL COMMENT '用户头像',user_profile VARCHAR(512) NULL COMMENT '用户简介',
#     department_id BIGINT NULL COMMENT '部门id',role_key VARCHAR(256) NOT NULL COMMENT '绑定的角色标识',edit_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '编辑时间',create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',is_delete TINYINT DEFAULT 0 NOT NULL COMMENT '是否删除',UNIQUE KEY uk_user_account (user_account),INDEX idx_user_name (user_name),INDEX idx_role_key (role_key)
) COMMENT '用户表' COLLATE = utf8mb4_unicode_ci;-- 港口表
CREATE TABLE IF NOT EXISTS port (id BIGINT AUTO_INCREMENT COMMENT 'id' PRIMARY KEY,port_name VARCHAR(256) NOT NULL COMMENT '港口名称',port_type TINYINT NOT NULL COMMENT '港口类型: 0=普通港口 1=平台',create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',is_delete TINYINT DEFAULT 0 NOT NULL COMMENT '是否删除',UNIQUE KEY uk_port_name (port_name)
) COMMENT '港口表' COLLATE = utf8mb4_unicode_ci;-- 角色表
CREATE TABLE IF NOT EXISTS sa_role (id BIGINT AUTO_INCREMENT COMMENT 'id' PRIMARY KEY,role_key VARCHAR(256) NOT NULL COMMENT '角色标识(唯一,不可修改)',role_name VARCHAR(256) NOT NULL COMMENT '角色名称',role_level INT NOT NULL COMMENT '角色等级(越小权限越大)',perm_key_list TEXT COMMENT '权限key列表(JSON数组格式)',port_id BIGINT NOT NULL COMMENT '绑定的港口id',status TINYINT DEFAULT 1 NOT NULL COMMENT '状态:1启用 0停用',create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',is_delete TINYINT DEFAULT 0 NOT NULL COMMENT '是否删除',UNIQUE KEY uk_role_key (role_key),INDEX idx_port_id (port_id)
) COMMENT '角色表' COLLATE = utf8mb4_unicode_ci;-- 权限表 (菜单/接口二合一)
CREATE TABLE IF NOT EXISTS sa_permission (id BIGINT AUTO_INCREMENT COMMENT 'id' PRIMARY KEY,parent_id BIGINT DEFAULT 0 NOT NULL COMMENT '父ID (0=根节点)',perm_key VARCHAR(256) NOT NULL COMMENT '权限标识(唯一,接口权限/菜单路由)',perm_name VARCHAR(256) NOT NULL COMMENT '权限名称',type TINYINT NOT NULL COMMENT '类型:0目录 1菜单 2接口',request_path VARCHAR(512) NULL COMMENT '请求路径(接口用) 或 前端路由',icon VARCHAR(128) NULL COMMENT '前端图标',sort INT DEFAULT 0 NOT NULL COMMENT '排序字段',create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',is_delete TINYINT DEFAULT 0 NOT NULL COMMENT '是否删除',UNIQUE KEY uk_perm_key (perm_key),INDEX idx_parent_id (parent_id),INDEX idx_type (type)
) COMMENT '权限表' COLLATE = utf8mb4_unicode_ci;
  1. 用户表只绑定角色表 (role_key 属性对应 角色表的角色标识)

一个用户只能拥有一个角色,方便管理 n :1

  1. 角色表绑定 港口表 (port_id属性对应港口表id) n :1
  2. 角色表绑定 权限表列表 (perm_key_list 属性存储 perm_key权限标识数组)

不使用 角色-权限表 的原因是存储为json数组,方便存取,不用大规模读取表

  "permKeyList": ["sys","user","user:delete"]
  1. 权限表 存储两种类型:接口 和 菜单

接口权限 用于satoken接口级鉴权

菜单权限 用于前端路由使用,都支持前端管理员分配权限

  1. 港口表(部门表) 分为平台和港区

平台管理员可以管理全平台

港区管理员只能管理港区内相关人员、内容

业务开发

逻辑删除与唯一索引冲突

MyBatis 设置 is_delete 逻辑删除可能与 表的唯一依赖出现冲突

冲突的产生

● 假设你有一个 user 表,其中 username 字段有唯一索引。
● 现有用户 JohnDoe(username = ‘johndoe’, is_deleted = 0)。
● 当你“删除”这个用户后,这条记录变为 username = ‘johndoe’, is_deleted = 1。
● 现在,如果你想重新创建一个用户名为 johndoe 的新用户并执行插入操作,数据库会尝试插入 (‘johndoe’, 0)。
● 然而,唯一索引检查时会发现表中已经存在一条 (‘johndoe’, 1) 的记录。由于唯一索引并不感知 is_deleted 字段的业务逻辑,它只检查 username 的值,因此 ‘johndoe’ 已经存在,插入操作就会违反唯一索引约束,导致报错。

由于我的代码中每个表设置了唯一索引,故每个表 有数据删除后都会 出现唯一索引冲突

原始代码:

  1. 数据库设计

如下表设计:port_name 字段设置了唯一索引

-- 港口表
CREATE TABLE IF NOT EXISTS port (id BIGINT AUTO_INCREMENT COMMENT 'id' PRIMARY KEY,port_name VARCHAR(256) NOT NULL COMMENT '港口名称',port_type TINYINT NOT NULL COMMENT '港口类型: 0=普通港口 1=平台',create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',is_delete TINYINT DEFAULT 0 NOT NULL COMMENT '是否删除',UNIQUE KEY uk_port_name (port_name)
) COMMENT '港口表' COLLATE = utf8mb4_unicode_ci;
  1. MyBatis 设置逻辑删除

原来:直接使用 removeById() MyBatis会自动调用将逻辑删除字段设置为 1

@TableLogicprivate Integer isDelete;// yml配置
mybatis-plus:configuration:map-underscore-to-camel-case: falselog-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:logic-delete-field: isDelete # 全局逻辑删除的实体字段名logic-delete-value: 1 # 逻辑已删除值(默认为 1)logic-not-delete-value: 0 # 逻辑未删除值(默认为 0
// 无需全局配置
// MyBatis Felx 设置逻辑删除  默认 0 1 @Column(value = "is_delete", isLogicDelete = true)private Integer isDelete;
解决方案

方案一:

若使用MyBatis-plus 可以直接配置避免冲突,比较高效解决方案

  1. 原来的删除值为 1,可以将删除值设置为 null 空值
  2. 修改 is_delete 字段,删去 not null, 允许为空

方案二:

由于我使用的是 MyBatis Flex,参考官方文档,其值的设置不能直接配置

需要通过实现 FlexGlobalConfig globalConfig = FlexGlobalConfig.getDefaultConfig(); 来进行配置

尝试使用该类后发现 不能传入空值,也就是不支持将删除后的值设置为空

MyBatisFlex 不支持配置空值,业务开发大半不可能更换框架,只能另辟蹊径

若MySQL版本在** 8.0.13+ **,可以修改表结构使用函数索引

函数索引 (IF(is_delete = 0, 0, NULL)) 的工作原理:

  1. 当 is_delete = 0(未删除)时,索引值为 0
  2. 当 is_delete = 1(已删除)时,索引值为 NULL
  3. 在 MySQL 中,唯一索引允许有多个 NULL 值,但不允许多个相同的非 NULL 值
  4. 因此,这个索引确保了:
    ○ 未删除的记录中,user_account 必须是唯一的(因为索引值为 (user_account, 0))
    ○ 已删除的记录可以有多个相同的 user_account(因为索引值为 (user_account, NULL),而多个 NULL 值不违反唯一约束)
-- 港口表
CREATE TABLE IF NOT EXISTS port (id BIGINT AUTO_INCREMENT COMMENT 'id' PRIMARY KEY,port_name VARCHAR(256) NOT NULL COMMENT '港口名称',port_type TINYINT NOT NULL COMMENT '港口类型: 0=普通港口 1=平台',create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',is_delete TINYINT DEFAULT 0 NOT NULL COMMENT '是否删除',-- 使用函数索引替代普通唯一索引UNIQUE INDEX uk_port_name_is_delete (port_name, (IF(is_delete = 0, 0, NULL)))
) COMMENT '港口表' COLLATE = utf8mb4_unicode_ci;

如此,也能有效解决 唯一索引冲突问题!

http://www.dtcms.com/a/469175.html

相关文章:

  • 三星S25Ultra/S24安卓16系统Oneui8成功获取完美root权限+LSP框架
  • ffmpeg 播放视频 暂停
  • 老题新解|大整数的因子
  • Eureka的自我保护机制
  • 探索颜色科学:从物理现象到数字再现
  • AirSim_SimJoyStick
  • 第五部分:VTK高级功能模块(第149章 Remote模块 - 远程模块类)
  • 道可云人工智能每日资讯|《政务领域人工智能大模型部署应用指引》发布
  • 自己做网站哪家好win10 wordpress安装教程视频
  • wordpress整体搬迁宁波seo深度优化平台有哪些
  • 4K Wallpaper mac v2.7.dmg 安装教程(Mac电脑详细安装步骤4K壁纸Mac下载安装)
  • Mac 软件出现「应用程序“xxx”不能打开」的解决办法
  • 东航集团客户网站是哪家公司建设4k高清视频素材网站
  • Compose 在Row、Column上使用focusRestorer修饰符失效原因
  • Sora 2:当AI视频“以假乱真”,内容创作进入新纪元,体验AI创作能力
  • 推荐一个浏览器代理插件(Tajang Proxy),支持Chrome和Edge
  • conda|如何通过命令行在mac上下载conda
  • VS Code 二次开发:跨平台图标定制全攻略
  • 关于微信小程序开发几点总结
  • 杭州建站价格邢台wap网站建设费用
  • kafka4使用记录
  • 2100AI智能生活
  • 网站开发交流群做网站线上线下价格混乱
  • AI:让驾驶体验个性化!
  • 由Nacos允许配置访问代理启发的Node前端部署路径转发探究
  • vue - JS 判断客户端是苹果 iOS 还是安卓 Android(封装好的方法直接调用)二种解决方案
  • 路由器如何判断数据转发目标
  • BEM命名规范
  • 12V-24V转3.2V-10V600mA恒流驱动芯片WT7018
  • 远程MCP的调用和阿里云生态的知识库和工作流的使用