mthoutai

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

sa-token 官方文档

基础登录功能

  1. 引入依赖

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

        
        
            cn.dev33
            sa-token-spring-boot3-starter
            1.44.0
        
  1. yml配置

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

# 用户鉴权
sa-token:
# token 名称
# Authorization: Bearer token值
token-name: Authorization
token-prefix: Bearer       # 设置Token前缀 【注意有空格】
is-read-header: true          # 从Header读取Token
is-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 {
@Override
public 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并保存
@Override
public 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. 返回信息并携带 token
LoginUserVO 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: localhost
port: 6379
password:
  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 {
@Resource
private UserService userService;
@Resource
private RoleService roleService;
/**
*  一个账号拥有的权限码 集合
*
* @return
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
  Long userId = Long.parseLong(loginId.toString());
  String roleKey = userService.getRoleKeyByUserId(userId);
  return roleService.getPermKeyListByRoleKey(roleKey);
  }
  /**
  *  一个账号拥有的角色
  *
  * @return
  */
  @Override
  public 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

@TableLogic
private Integer isDelete;
// yml配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-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;

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

posted on 2025-11-08 12:32  mthoutai  阅读(157)  评论(0)    收藏  举报