Sa-Token框架主要解决登录认证、权限认证、单点登录等一系列权限相关问题。
官网: https://sa-token.cc/doc.html#/
本次演示环境为SpringBoot3.2.5、mybatis-plus3.5.6,权限模型采用RBAC模型,简单来说就是什么用户有什么角色,对应又有哪些权限。
数据库表
建表语句
create table user
(
id int auto_increment
primary key,
username varchar(20) null comment '用户名',
password varchar(255) null comment '密码'
);
create table rule
(
id int auto_increment
primary key,
rule_name varchar(20) null comment '角色名'
);
create table power
(
id int auto_increment
primary key,
authority varchar(255) null comment '权限'
);
# 用户和角色关联表
create table user_rule
(
id int auto_increment
primary key,
user_id int null comment '用户id',
rule_id int null comment '角色id'
);
# 角色和权限关联表
create table rule_power
(
id int auto_increment
primary key,
rule_id int null comment '角色id',
power_id int null comment '权限id'
);
表数据
用户表
角色表
权限表
另外两个关系表就不展示了
依赖
<properties>
<java.version>17</java.version>
<mybatis.plus.version>3.5.6</mybatis.plus.version>
<sa-token.version>1.34.0</sa-token.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- mybatis-plus代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis.plus.version}</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis.plus.version}</version>
<exclusions>
<exclusion>
<artifactId>mybatis-spring</artifactId>
<groupId>org.mybatis</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式)-->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Sa-Token插件:权限缓存与业务缓存分离 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-alone-redis</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.12</version>
</dependency>
</dependencies>
注意版本sa-token相关的版本一致,也不要忘了commons-pool2这个依赖,不然就会报各种奇怪的错误。
常见问题报错可以在官网进行查看
快速访问: https://sa-token.cc/doc.html#/more/common-questions
配置文件
spring:
application:
name: sa-token-demo
banner:
location: classpath:banner.txt
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/your_scheme?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: 111111
data:
redis:
# Redis数据库索引(默认为0)
database: 0
host: 127.0.0.1
port: 6379
password: redis001
timeout: 10s
lettuce:
pool:
# 最大连接数
max-active: 100
# 最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 最大空闲连接
max-idle: 10
# 最小空闲连接
min-idle: 0
sa-token:
# token 名称(同时也是 cookie 名称),发请求的时候请求头名称,如 satoken:Bearer b12e2f2157034ac0aa32a7cc4c97b3e6
token-name: satoken
# token前缀
token-prefix: Bearer
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
activity-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
is-share: true
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
token-style: simple-uuid
# 是否输出操作日志
is-log: true
mybatis-plus:
configuration:
#开启sql日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
注意token-name上面写的注释
代码
角色、权限的查询
写一个类继承StpInterface这个接口,重写getRoleList、getPermissionList这两个方法,后续权限校验都会走这两个方法
@Slf4j
@Component
public class UserAuthority implements StpInterface {
@Autowired
private UserRuleMapper userRuleMapper;
/**
* 获取角色列表
*
* @param o 用户id
* @param s
* @return List
*/
@Override
public List<String> getRoleList(Object o, String s) {
List<Rule> rules = userRuleMapper.queryRuleByUserId(o);
List<String> ruleNames = rules.stream().map(Rule::getRuleName).toList();
log.info(">>>getRoleList");
log.info("用户:" + o);
log.info("角色:" + Arrays.toString(ruleNames.toArray()));
return ruleNames;
}
/**
* 获取权限列表
*
* @param o 用户id
* @param s
* @return List
*/
@Override
public List<String> getPermissionList(Object o, String s) {
List<Power> powers = userRuleMapper.queryPowerByUserId(o);
List<String> permissions = powers.stream().map(Power::getAuthority).toList();
log.info(">>>getPermissionList");
log.info("用户:" + o);
log.info("权限:" + Arrays.toString(permissions.toArray()));
return permissions;
}
}
mapper
@Mapper
public interface UserRuleMapper extends BaseMapper<UserRule> {
/**
* 查询某个用户所拥有的角色
*
* @param userId 用户id
* @return list
*/
List<Rule> queryRuleByUserId(@Param("id") Object userId);
/**
* 查询某个用户所拥有的所有权限
*
* @param userId 用户id
* @return list
*/
List<Power> queryPowerByUserId(@Param("id") Object userId);
}
mapper xml
<select id="queryRuleByUserId" resultType="org.example.satokendemo.entity.Rule" parameterType="string">
select rule.*
from rule
join user_rule on rule.id = user_rule.rule_id
where user_rule.user_id = #{id};
</select>
<select id="queryPowerByUserId" resultType="org.example.satokendemo.entity.Power" parameterType="string">
select power.*
from user_rule
join rule_power on user_rule.rule_id = rule_power.rule_id
join power on rule_power.power_id = power.id
where user_rule.user_id = #{id};
</select>
拦截器
@SpringBootConfiguration
@EnableWebMvc
public class RequestInterceptor implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
SaInterceptor saInterceptor = new SaInterceptor(
handler -> {
// 检验登录
StpUtil.checkLogin();
}
);
registry.addInterceptor(saInterceptor)
// 拦截所有接口
.addPathPatterns("/**")
//不拦截注册、登录接口
.excludePathPatterns("/user/register", "/user/doLogin");
}
}
全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 未登录异常
*
* @param e NotLoginException
* @return Result
*/
@ExceptionHandler(NotLoginException.class)
public Result handlerNotLoginException(NotLoginException e) {
e.printStackTrace();
String message = "";
if (e.getType().equals(NotLoginException.NOT_TOKEN)) {
message = "未能读取到有效 token";
} else if (e.getType().equals(NotLoginException.INVALID_TOKEN)) {
message = "token 无效";
} else if (e.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
message = "token 已过期";
} else if (e.getType().equals(NotLoginException.BE_REPLACED)) {
message = "token 已被顶下线";
} else if (e.getType().equals(NotLoginException.KICK_OUT)) {
message = "token 已被踢下线";
} else {
message = "当前会话未登录";
}
return Result.error(message);
}
/**
* 无角色异常
*
* @param e NotRoleException
* @return Result
*/
@ExceptionHandler(NotRoleException.class)
public Result handlerNotRoleException(Exception e) {
e.printStackTrace();
return Result.error("当前账号角色不符合!");
}
/**
* 无权限异常
*
* @param e NotPermissionException
* @return Result
*/
@ExceptionHandler(NotPermissionException.class)
public Result handlerNotPermissionException(Exception e) {
e.printStackTrace();
return Result.error("当前角色无权限!");
}
/**
* 系统异常
*
* @param e Exception
* @return Result
*/
@ExceptionHandler(Exception.class)
public Result handlerException(Exception e) {
e.printStackTrace();
return Result.error("系统异常");
}
}
UserController
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private UserMapper userMapper;
@RequestMapping(value = "register", method = RequestMethod.POST)
public Result register(String username, String password) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername, username);
User user = userService.getOne(queryWrapper);
if (ObjectUtil.isNotNull(user)) {
return Result.error("用户名已存在!");
}
User newUser = new User();
newUser.setUsername(username);
newUser.setPassword(SaSecureUtil.aesEncrypt(Constant.ENCODER_KEY, password));
userMapper.insert(newUser);
return Result.ok("注册成功!");
}
@RequestMapping("doLogin")
public Result doLogin(String username, String password) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername, username);
queryWrapper.eq(User::getPassword, SaSecureUtil.aesEncrypt(Constant.ENCODER_KEY, password));
User user = userService.getOne(queryWrapper);
if (ObjectUtil.isNotNull(user)) {
StpUtil.login(user.getId());
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
System.out.println(">>>tokenInfo = " + tokenInfo);
return Result.ok("登录成功", tokenInfo);
}
return Result.error("用户名或者密码错误!");
}
/**
* 查看当前账号是否登录
*/
@RequestMapping("isLogin")
public Result isLogin() {
return StpUtil.isLogin() ? Result.ok("已登录") : Result.error("是否登录:" + StpUtil.isLogin());
}
/**
* 查询 Token 信息
*/
@RequestMapping("tokenInfo")
public Result tokenInfo() {
return Result.ok(StpUtil.getTokenInfo());
}
/**
* 注销
*/
@RequestMapping("logout")
public Result logout() {
StpUtil.logout();
return Result.ok();
}
}
TestController
@RestController
@RequestMapping("/test")
public class TestController {
/**
* 加@SaIgnore可以不检查权限
*/
@SaIgnore
@RequestMapping("/getList")
public Result getList() {
return Result.ok("无需登录接口,查询成功");
}
/**
* 登录后可访问
*/
@SaCheckLogin
@RequestMapping("/select")
public Result select() {
return Result.ok("查询成功");
}
/**
* 检查角色,有admin角色可访问
*/
@RequestMapping("/checkRoleSelect")
@SaCheckRole("admin")
public Result checkRoleSelect() {
return Result.ok("查询成功");
}
/**
* 有admin、superAdmin其中一个角色可访问
*/
@RequestMapping("/delete")
@SaCheckRole(value = {"admin", "superAdmin"}, mode = SaMode.OR)
public Result delete() {
return Result.ok("删除成功");
}
/**
* 检查权限,有其中一个权限就可以访问
*/
@RequestMapping("/add")
@SaCheckPermission(value = {"add", "update", "delete"}, mode = SaMode.OR)
public Result add() {
return Result.ok("添加成功");
}
/**
* 有 更新/删除 权限 或者admin角色时可以访问
*/
@RequestMapping("/update")
@SaCheckPermission(value = {"update", "delete"}, orRole = "admin")
public Result update() {
return Result.ok("更新成功");
}
}
测试
未登录时访问,访问失败
使用saToken-normal用户登录,该用户角色为normal
访问删除接口,normal只有查询权限,访问失败(注意请求头中的参数值,Bearer和tokenValue直接之间有个空格)
换个admin角色登录
接口访问成功
其他情况就不一一测试了,可以看TestController中方法的注释,也可以看官网的解释
链接: https://sa-token.cc/doc.html#/use/at-check?id=%e6%b3%a8%e8%a7%a3%e9%89%b4%e6%9d%83-1
计科和软工做毕设的同学狂喜啊
该框架的更多功能请查看官网~