2.SpringBoot集成初体验
依赖引入
SpringBoot 3.x版本引入
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.35.0.RC</version>
</dependency>
3版本以下引入
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.35.0.RC</version>
</dependency>
yml配置
在application.yml
中加入以下配置
server:
# 端口
port: 8081
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: satoken
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-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: uuid
# 是否输出操作日志
is-log: true
controller测试
package com.wl.satoken.controller;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author NoDreamJava
* @Date 2023-08-02 16:43
* @Version 1.0
*/
@RestController
@RequestMapping("/auth")
public class UserController {
@RequestMapping("/login")
public String login(String username, String password) {
if (username.equals("admin") && password.equals("123456")) {
StpUtil.login(10001);
return "登录成功";
} else {
return "登录失败";
}
}
// 查询登陆状态
@RequestMapping("/isLogin")
public String isLogin() {
return "当前会话是否登录:" + StpUtil.isLogin();
}
}
访问地址:http://localhost:8081/auth/login?username=admin&password=123456
检查是否登陆:http://localhost:8081/auth/isLogin
3.全局异常处理
创建一个全局异常拦截器,统一返回给前端的格式
@RestControllerAdvice
public class GlobalExceptionHandler {
// 全局异常拦截
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
}
3.登录认证
在完成登陆后会发现,token被自动写入到浏览器中了
这是因为 sa-Token用了 Cookie 自动注入的特性,省略了手写返回 token 的代码
登录和注销
// 当前会话注销登录
StpUtil.logout();
// 获取当前会话是否已经登录,返回true=已登录,false=未登录
StpUtil.isLogin();
// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.checkLogin();
会话查询
// 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.getLoginId();
// 类似查询API还有:
StpUtil.getLoginIdAsString(); // 获取当前会话账号id, 并转化为`String`类型
StpUtil.getLoginIdAsInt(); // 获取当前会话账号id, 并转化为`int`类型
StpUtil.getLoginIdAsLong(); // 获取当前会话账号id, 并转化为`long`类型
// ---------- 指定未登录情形下返回的默认值 ----------
// 获取当前会话账号id, 如果未登录,则返回 null
StpUtil.getLoginIdDefaultNull();
// 获取当前会话账号id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型)
StpUtil.getLoginId(T defaultValue);
token查询
// 获取当前会话的 token 值
StpUtil.getTokenValue();
// 获取当前`StpLogic`的 token 名称
StpUtil.getTokenName();
// 获取指定 token 对应的账号id,如果未登录,则返回 null
StpUtil.getLoginIdByToken(String tokenValue);
// 获取当前会话剩余有效期(单位:s,返回-1代表永久有效)
StpUtil.getTokenTimeout();
// 获取当前会话的 token 信息参数
StpUtil.getTokenInfo();
测试
加入两个接口测试
// 查询token信息
@RequestMapping("/getTokenInfo")
public SaResult getTokenInfo() {
return SaResult.data(StpUtil.getTokenInfo());
}
// 注销
@RequestMapping("/logout")
public SaResult logout() {
StpUtil.logout();
return SaResult.ok();
}
访问:http://localhost:8081/auth/getTokenInfo查询token信息
访问:http://localhost:8081/auth/logout注销当前用户
4.权限认证
权限认证本质就是来控制一个账号是否拥有操作权限,深入到底层数据中,就是每个账号都会拥有一组权限码集合,框架来校验这个集合中是否包含指定的权限码。
例如:当前账号拥有权限码集合 ["user-add", "user-delete", "user-get"]
,这时候我来校验权限 "user-update"
,则其结果就是:验证失败,禁止访问。
问题的核心就是两个:
- 如何获取一个账号所拥有的权限码集合?
- 本次操作需要验证的权限码是哪个?
获取当前账号权限码集合
/**
* 自定义权限加载接口实现类
*/
@Component // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询权限
List<String> list = new ArrayList<String>();
list.add("101");
list.add("user.add");
list.add("user.update");
list.add("user.get");
// list.add("user.delete");
list.add("art.*");
return list;
}
/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询角色
List<String> list = new ArrayList<String>();
list.add("admin");
list.add("super-admin");
return list;
}
}
校验权限
// 获取:当前账号所拥有的权限集合
StpUtil.getPermissionList();
// 判断:当前账号是否含有指定权限, 返回 true 或 false
StpUtil.hasPermission("user.add");
// 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException
StpUtil.checkPermission("user.add");
// 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过]
StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get");
// 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
StpUtil.checkPermissionOr("user.add", "user.delete", "user.get");
角色校验
// 获取:当前账号所拥有的角色集合
StpUtil.getRoleList();
// 判断:当前账号是否拥有指定角色, 返回 true 或 false
StpUtil.hasRole("super-admin");
// 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
StpUtil.checkRole("super-admin");
// 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
StpUtil.checkRoleAnd("super-admin", "shop-admin");
// 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可]
StpUtil.checkRoleOr("super-admin", "shop-admin");
权限通配符
sa-Token允许你根据通配符指定泛权限,例如当一个账号拥有art.*
的权限时,art.add
、art.delete
、art.update
都将匹配通过
// 当拥有 art.* 权限时
StpUtil.hasPermission("art.add"); // true
StpUtil.hasPermission("art.update"); // true
StpUtil.hasPermission("goods.add"); // false
// 当拥有 *.delete 权限时
StpUtil.hasPermission("art.delete"); // true
StpUtil.hasPermission("user.delete"); // true
StpUtil.hasPermission("user.update"); // false
// 当拥有 *.js 权限时
StpUtil.hasPermission("index.js"); // true
StpUtil.hasPermission("index.css"); // false
StpUtil.hasPermission("index.html"); // false
测试
建一张用户表
CREATE TABLE `user` (
`id` int NOT NULL,
`user_name` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`permissions` varchar(255) DEFAULT NULL,
`role` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
插入一条数据
INSERT INTO `sys`.`user`(`id`, `user_name`, `password`, `permissions`, `role`) VALUES (1001, 'wl', '123456', 'user.add,user.update', 'leader');
实现获取账号权限码集合接口
package com.wl.sa.auth;
/**
* @Author NoDreamJava
* @Date 2023-08-15 10:13
* @Version 1.0
*/
import cn.dev33.satoken.stp.StpInterface;
import com.wl.sa.mapper.UserMapper;
import com.wl.sa.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 自定义权限加载接口实现类
*/
@Component // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {
@Autowired
private UserMapper userMapper;
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
User user = userMapper.selectById(Long.parseLong(loginId.toString()));
return Arrays.stream(user.getPermissions().split(",")).collect(Collectors.toList());
}
/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
User user = userMapper.selectById(Long.parseLong(loginId.toString()));
return Arrays.stream(user.getRole().split(",")).collect(Collectors.toList());
}
}
添加测试接口
@RequestMapping("/getPermission")
public SaResult getPermission() {
// 查询权限信息 ,如果当前会话未登录,会返回一个空集合
List<String> permissionList = StpUtil.getPermissionList();
System.out.println("当前登录账号拥有的所有权限:" + permissionList);
return SaResult.ok()
.set("permissionList", permissionList);
}
访问地址:http://localhost:8081/auth/getPermission验证
踢人下线
踢人下线,核心操作就是找到指定 loginId
对应的 Token
,并设置其失效。
强制注销
StpUtil.logout(10001); // 强制指定账号注销下线
StpUtil.logout(10001, "PC"); // 强制指定账号指定端注销下线
StpUtil.logoutByTokenValue("token"); // 强制指定 Token 注销下线
踢人下线
StpUtil.kickout(10001); // 将指定账号踢下线
StpUtil.kickout(10001, "PC"); // 将指定账号指定端踢下线
StpUtil.kickoutByTokenValue("token"); // 将指定 Token 踢下线
强制注销 和 踢人下线 的区别在于:
- 强制注销等价于对方主动调用了注销方法,再次访问会提示:Token无效。
- 踢人下线不会清除Token信息,而是将其打上特定标记,再次访问会提示:Token已被踢下线。
小案例测试
添加查询角色接口,只有登录才可以访问
// 登录校验:只有登录之后才能进入该方法
@SaCheckLogin
@RequestMapping("/getUserInfo")
public SaResult getUserInfo() {
List<User> userList = userMapper.selectList(null);
return SaResult.ok()
.set("userList", userList);
}
// 根据id踢人下线
@RequestMapping("/kickById")
public SaResult kickById(String loginId) {
StpUtil.kickout(loginId);
return SaResult.ok();
}
先访问接口 /kickById
踢人下线,然后访问 /getUserInfo
,发现提示已经被提下线
注解鉴权
注解鉴权 —— 优雅的将鉴权与业务代码分离!
@SaCheckLogin
: 登录校验 —— 只有登录之后才能进入该方法。@SaCheckRole("admin")
: 角色校验 —— 必须具有指定角色标识才能进入该方法。@SaCheckPermission("user:add")
: 权限校验 —— 必须具有指定权限才能进入该方法。@SaCheckSafe
: 二级认证校验 —— 必须二级认证之后才能进入该方法。@SaCheckBasic
: HttpBasic校验 —— 只有通过 Basic 认证后才能进入该方法。@SaIgnore
:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。@SaCheckDisable("comment")
:账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。
注册拦截器
package com.wl.sa.interceptor;
import cn.dev33.satoken.interceptor.SaInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @Author NoDreamJava
* @Date 2023-08-15 15:17
* @Version 1.0
*/
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
}
使用注解鉴权
// 权限校验:必须具有指定权限才能进入该方法
@SaCheckPermission("user.add")
@RequestMapping("/addByPermission")
public String add() {
return "用户增加";
}
// 角色校验:必须具有指定角色才能进入该方法
@SaCheckRole("admin")
@RequestMapping("/addByRole")
public String addByRole() {
return "用户增加";
}
目前数据库表中的 权限列表为 user.add,user.update 角色列表为leader
访问权限接口:http://localhost:8081/auth/addByPermission
访问角色接口:http://localhost:8081/auth/addByRole
设定校验模式
@SaCheckRole
与@SaCheckPermission
注解可设置校验模式
// 注解式鉴权:只要具有其中一个权限即可通过校验
@RequestMapping("/atJurOr")
@SaCheckPermission(value = {"user.add", "user.all", "user.delete"}, mode = SaMode.OR)
public SaResult atJurOr() {
return SaResult.data("用户信息");
}
访问接口:http://localhost:8081/auth/atJurOr
mode有两种取值:
SaMode.AND
,标注一组权限,会话必须全部具有才可通过校验。SaMode.OR
,标注一组权限,会话只要具有其一即可通过校验。
角色权限双重“or校验”
一个接口在具有权限 user.add
或角色 admin
时可以调通。怎么写?
// 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验
@RequestMapping("userAdd")
@SaCheckPermission(value = "user.add", orRole = "admin")
public SaResult userAdd() {
return SaResult.data("用户信息");
}
访问:http://localhost:8081/auth/userAdd
orRole 字段代表权限校验未通过时的次要选择,两者只要其一校验成功即可进入请求方法,其有三种写法:
- 写法一:
orRole = "admin"
,代表需要拥有角色 admin 。 - 写法二:
orRole = {"admin", "manager", "staff"}
,代表具有三个角色其一即可。 - 写法三:
orRole = {"admin, manager, staff"}
,代表必须同时具有三个角色。