SaToken技术分享
本文已参与「新人创作礼」活动,一起开启掘金创作之路。Satoken
1.什么是SaToken
Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、Session会话、单点登录、OAuth2.0、微服务网关鉴权 等一系列权限相关问题,简化了相对Shiro或者SpringSecurity的复杂的前置配置,比如自定义的Realm或者全局过滤器这些。
SaToekn与Shiro以及SpringSecurity对比
相同点:都具备认证、授权、加密、会话、缓存、rememerMe等功能,都是一种安全认证框架。
SpringSecurity优点:
- SpringSecurity是基于Spring进行开发的 ,需要依赖Spring Shiro是需要和Spring进行整合开发
- SpringSecurity功能相对比Shiro更加丰富以及社区资源相对Shiro更多
Shiro优点:
- Shiro的配置和使用比较简单而SpringSecurity相对上手比较复杂
- Shiro是一种相对独立的容器,依赖性比较低,可以独立运行但是 Spring Security 依赖Spring容器;
saToken的优点
- 功能相对Shiro和SpringSecurity更加全面和完整
- 更加独立,整合项目过程中只需要引入依赖添加配置即可简化了部署的操作
- 入门以及操作更加简单简化了类似Shiro这种需要自定义Relam的操作,官方文档是中文版的,更好理解
2.常用功能介绍
- 登录认证 —— 单端登录、多端登录、同端互斥登录、七天内免登录
- 权限认证 —— 权限认证、角色认证、会话二级认证
- 踢人下线 —— 根据账号id踢人下线、根据Token值踢人下线
- 账号封禁 —— 指定天数封禁、永久封禁、设定解封时间
- 持久层扩展 —— 可集成Redis等专业缓存中间件,重启数据不丢失
- 分布式会话 —— 提供jwt集成、共享数据中心两种分布式会话方案
- 独立Redis —— 将权限缓存与业务缓存分离
- 同端互斥登录 —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录
- 多账号认证体系 —— 比如一个商城项目的user表和admin表分开鉴权
- 花式token生成 —— 内置六种Token风格,还可:自定义Token生成策略、自定义Token前缀
- 注解式鉴权 —— 优雅的将鉴权与业务代码分离
- 路由拦截式鉴权 —— 根据路由拦截鉴权,可适配restful模式
- 密码加密 —— 提供密码加密模块,可快速MD5、SHA1、SHA256、AES、RSA加密
- 全局侦听器 —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作
StpUtil.login(10001); // 标记当前会话登录的账号id
StpUtil.getLoginId(); // 获取当前会话登录的账号id
StpUtil.isLogin(); // 获取当前会话是否已经登录, 返回true或false
StpUtil.logout(); // 当前会话注销登录
StpUtil.kickout(10001); // 将账号为10001的会话踢下线
StpUtil.hasRole("super-admin"); // 查询当前账号是否含有指定角色标识, 返回true或false
StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权限, 返回true或false
StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌值
StpUtil.login(10001, "PC"); // 指定设备标识登录,常用于“同端互斥登录”
StpUtil.kickout(10001, "PC"); // 指定账号指定设备标识踢下线 (不同端不受影响)
StpUtil.openSafe(120); // 在当前会话开启二级认证,有效期为120秒
StpUtil.checkSafe(); // 校验当前会话是否处于二级认证有效期内,校验失败会抛出异常
3.使用操作流程
1.创建SpringBoot项目
2.添加依赖
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.29.0</version>
</dependency>
3.编写配置文件
server:
# 端口
port: 8081
# Sa-Token配置
sa-token:
# token名称
token-name: token
# token有效期,单位s 默认30天, -1代表永不过期
timeout: 2592000
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: false
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
is-share: false
# token风格
token-style: uuid
# 是否输出操作日志
is-log: true
3.启动SpringBoot项目 进行启动
4.编写测试
@Api(tags = "用户基础操作")
@RestController
@RequestMapping("/acc/")
public class LoginController {
public static final String LJX = "ljx";
public static final String PASSWORD = "123456";
@GetMapping("doLogin")
@ApiOperation("登录操作")
public SaResult doLogin(String name, String pwd) {
//todo 这个地方放置具体的数据库操作
if (LJX.equals(name) && PASSWORD.equals(pwd)) {
// 存放的是登录用户的账户account
StpUtil.login(10001);
String tokenName = StpUtil.getTokenName();
System.out.println("token名称是"+tokenName);
String tokenValue = StpUtil.getTokenValue();
System.out.println("token的值"+tokenValue);
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
System.out.println("token的信息参数"+tokenInfo);
return SaResult.ok("登录成功");
}
return SaResult.error("登录失败");
}
@ApiOperation("判断当前的登录状态")
@GetMapping("isLogin")
public SaResult isLogin() {
return SaResult.ok(StpUtil.isLogin() + "");
}
@RequestMapping("tokenInfo")
@GetMapping("获取当前的token信息")
public SaResult tokenInfo() {
return SaResult.data(StpUtil.getTokenInfo());
}
@ApiOperation("账号注销登录")
@GetMapping("logout")
public SaResult logout() {
StpUtil.logout();
return SaResult.ok();
}
}
4.注解式鉴权操作流程
1.编写实现StpInterface接口的类 实现获取当前登录用户的权限集合以及角色集合的操作
@Component
public class StpInterfaceImpl implements StpInterface {
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// todo 根据从数据库中进行查询
List<String> list = new ArrayList<String>();
list.add("user-add");
// list.add("user-delete");
// list.add("user-update");
list.add("user-get");
list.add("article-get");
return list;
}
/**
* 返回一个账号所拥有的角色标识集合
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// todo 根据从数据库中进行查询
List<String> list = new ArrayList<String>();
list.add("admin");
list.add("super-admin");
return list;
}
}
2.定义全局异常拦截
/**
* 全局异常处理
*/
@ControllerAdvice
public class GlobalException {
// 全局异常拦截(拦截项目中的所有异常)
@ResponseBody
@ExceptionHandler
public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response)
throws Exception {
// 不同异常返回不同状态码
AjaxJson aj;
if (e instanceof NotLoginException) {
// 如果是未登录异常
NotLoginException ee = (NotLoginException) e;
aj = AjaxJson.getNotLogin().setMsg(ee.getMessage());
} else if (e instanceof NotRoleException) {
// 如果是角色异常
NotRoleException ee = (NotRoleException) e;
aj = AjaxJson.getNotJur("无此角色:" + ee.getRole());
} else if (e instanceof NotPermissionException) {
// 如果是权限异常
NotPermissionException ee = (NotPermissionException) e;
aj = AjaxJson.getNotJur("无此权限:" + ee.getCode());
} else if (e instanceof DisableLoginException) {
// 如果是被封禁异常
DisableLoginException ee = (DisableLoginException) e;
aj = AjaxJson.getNotJur("账号被封禁:" + ee.getDisableTime() + "秒后解封");
} else {
// 普通异常, 输出:500 + 异常信息
aj = AjaxJson.getError(e.getMessage());
}
// 返回给前端
return aj;
}
}
3.使用鉴权式注解
@Api(tags = "注解鉴权测试")
@RestController
@RequestMapping("/at/")
public class AtController {
@SaCheckLogin
@ApiOperation("检验之后登录的用户才可以进行操作")
@GetMapping("checkLogin")
public SaResult checkLogin() {
return SaResult.ok();
}
@ApiOperation("具有指定权限操作")
@SaCheckPermission("user-add")
@GetMapping("checkPermission")
public SaResult checkPermission() {
return SaResult.ok();
}
@ApiOperation("同时具有指定权限操作")
@SaCheckPermission({"user-add", "user-delete", "user-update"})
@GetMapping("checkPermissionAnd")
public SaResult checkPermissionAnd() {
return SaResult.ok();
}
@ApiOperation("具有多个指定权限中的一个操作")
@SaCheckPermission(value = {"user-add", "user-delete", "user-update"}, mode = SaMode.OR)
@GetMapping("checkPermissionOr")
public SaResult checkPermissionOr() {
return SaResult.ok();
}
@ApiOperation("具有指定角色才可以进行操作")
@SaCheckRole("admin")
@GetMapping("checkRole")
public SaResult checkRole() {
return SaResult.ok();
}
@ApiOperation("开启二级认证")
@GetMapping("openSafe")
public SaResult openSafe() {
// todo 在这个地方添加校验是否开启二级认证
// 打开二级认证,有效期为200秒
StpUtil.openSafe(200);
return SaResult.ok();
}
@ApiOperation("通过二级认证之后才可以进行操作")
@SaCheckSafe
@GetMapping("checkSafe")
public SaResult checkSafe() {
// todo 添加一些需要二级认证之后才可以进行的操作
return SaResult.ok();
}
}
5.路由拦截鉴权
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
/**
* 注册Sa-Token 的拦截器,打开注解式鉴权功能
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// todo 注册注解拦截器 可以用来存放允许放行的路径
// 除了/user/doLogin 可以放行 其余的操作都需要进行登录之后才可以进行操作
registry.addInterceptor(new SaAnnotationInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/user/doLogin");
}
@Override
// public void addInterceptors(InterceptorRegistry registry) {
// // 注册路由拦截器,自定义认证规则
// registry.addInterceptor(new SaRouteInterceptor((req, res, handler)->{
// // 根据路由划分模块,不同模块不同鉴权
// SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
// SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
// SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
// SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
// SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice"));
// SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment"));
// })).addPathPatterns("/**");
// }
}
6.技术点
1.自定义token类型
token-style=simple-uuid
2.同端互斥登录 指定客户端登录与退出
StpUtil.login(2345, "PC");
StpUtil.logout(2345, "PC");
3.密码加密
SaSecureUtil.md5("123456");
SaSecureUtil.md5BySalt("123456", "salt");
4.全局监听器
/**
* 自定义侦听器的实现
*/
@Component
public class MySaTokenListener implements SaTokenListener {
/** 每次登录时触发 */
@Override
public void doLogin(String loginType, Object loginId, SaLoginModel loginModel) {
// ...
}
/** 每次注销时触发 */
@Override
public void doLogout(String loginType, Object loginId, String tokenValue) {
// ...
}
/** 每次被顶下线时触发 */
@Override
public void doReplaced(String loginType, Object loginId, String tokenValue) {
// ...
}
...
}
5.与jwt继承
引入依赖
<!-- Sa-Token 整合 jwt -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>1.29.0</version>
</dependency>
配置
sa-token:
# jwt秘钥
jwt-secret-key: 456789123asdfghjk
注入指定Token方式
@Configuration
public class SaTokenConfigure {
// Sa-Token 整合 jwt (Style模式)
@Bean
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForStyle();
}
}