当前位置:   article > 正文

Sa-Token简单学习教程

sa-token

Sa-Token学习笔记

一、Sa-Token介绍

sa-token 是什么

Sa-Token 是一个轻量级 Java 权限认证框架

​ 主要解决:登录认证权限认证Session会话单点登录OAuth2.0微服务网关鉴权 等一系列权限相关问题。

sa-token 的特点和优势

​ Sa-Token 特点是以简单、优雅的方式完成系统的权限认证部分,如果你学过SpringSecurity 、Shiro之后,你就会明白,相对于这些传统老牌框架,Sa-Token 的 API 设计是多么的简单、优雅!

​ 在Sa-Token中,绝大多数功能都能只用一行代码解决,例如:

登录认证:

StpUtil.login(Account)
  • 1

踢人下线

StpUtil.kickout(Account)
  • 1

权限认证

@SaCheckPermission("user:add")
public String insert(SysUser user) {
    return "用户增加";
}
  • 1
  • 2
  • 3
  • 4
功能一览

Sa-Token 目前主要五大功能模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。

  • 登录认证 —— 单端登录、多端登录、同端互斥登录、七天内免登录
  • 权限认证 —— 权限认证、角色认证、会话二级认证
  • Session会话 —— 全端共享Session、单端独享Session、自定义Session
  • 踢人下线 —— 根据账号id踢人下线、根据Token值踢人下线
  • 账号封禁 —— 登录封禁、按照业务分类封禁、按照处罚阶梯封禁
  • 持久层扩展 —— 可集成Redis、Memcached等专业缓存中间件,重启数据不丢失
  • 分布式会话 —— 提供jwt集成、共享数据中心两种分布式会话方案
  • 微服务网关鉴权 —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证
  • 单点登录 —— 内置三种单点登录模式:无论是否跨域、是否共享Redis,都可以搞定
  • OAuth2.0认证 —— 轻松搭建 OAuth2.0 服务,支持openid模式
  • 二级认证 —— 在已登录的基础上再次认证,保证安全性
  • Basic认证 —— 一行代码接入 Http Basic 认证
  • 独立Redis —— 将权限缓存与业务缓存分离
  • 临时Token认证 —— 解决短时间的Token授权问题
  • 模拟他人账号 —— 实时操作任意用户状态数据
  • 临时身份切换 —— 将会话身份临时切换为其它账号
  • 前后端分离 —— APP、小程序等不支持Cookie的终端
  • 同端互斥登录 —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录
  • 多账号认证体系 —— 比如一个商城项目的user表和admin表分开鉴权
  • Token风格定制 —— 内置六种Token风格,还可:自定义Token生成策略、自定义Token前缀
  • 注解式鉴权 —— 优雅的将鉴权与业务代码分离
  • 路由拦截式鉴权 —— 根据路由拦截鉴权,可适配restful模式
  • 自动续签 —— 提供两种Token过期策略,灵活搭配使用,还可自动续签
  • 会话治理 —— 提供方便灵活的会话查询接口
  • 记住我模式 —— 适配[记住我]模式,重启浏览器免验证
  • 密码加密 —— 提供密码加密模块,可快速MD5、SHA1、SHA256、AES、RSA加密
  • 全局侦听器 —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作
  • 开箱即用 —— 提供SpringMVC、WebFlux等常见web框架starter集成包,真正的开箱即用

二、安装和配置Sa-Token

​ pom依赖:

<!-- Sa-Token 权限认证 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.34.0</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

更多内测版本了解:Sa-Token最新版本

​ 配置文件,这里只推荐yml风格,其他风格访问 在 SpringBoot 环境集成 (sa-token.cc) 查看:

server:
    # 端口
    port: 8081
    
sa-token: 
    # token名称 (同时也是cookie名称)
    token-name: satoken
    # token有效期,单位s 默认30天, -1代表永不过期 
    timeout: 2592000
    # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
    activity-timeout: -1
    # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) 
    is-concurrent: true
    # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) 
    is-share: true
    # token风格
    token-style: uuid
    # 是否输出操作日志 
    is-log: false
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

三、Sa-Token使用

过程:拦截器 ==> 登录认证 ==> 权限验证 ==>

接下来我会正常的构建一个登录:

1. 路由拦截

​ 一般项目中都会有这样一个需求,在没有登陆的情况下,仅允许访问登录的接口,只有在登陆之后才可以访问其他的接口,那么此时就需要用到路由拦截了

注册Sa-Token拦截器

新建一个配置类,实现 WebMvcConfigurer:

// 注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
    // 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
    registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
        //拦截的路径
        .addPathPatterns("/**")
        //开放的登录接口,除此之外的接口都需要登陆才可以访问
        .excludePathPatterns("/login"); 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这里注册了一个基于 StpUtil.checkLogin() 的登录校验拦截器,进行测试就会发现只有通过login接口登录成功后,才能访问其他接口

那么如果我们想自定义一个认证规则怎么写:

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // 注册 Sa-Token 拦截器,定义详细认证规则 
    registry.addInterceptor(new SaInterceptor(handler -> {
        // 指定一条 match 规则
        SaRouter
            .match("/**")    // 拦截的 path 列表,可以写多个 */
            .notMatch("/login")        // 排除掉的 path 列表,可以写多个 
            .check(r -> StpUtil.checkLogin());        // 要执行的校验动作,可以写完整的 lambda 表达式

        // 根据路由划分模块,不同模块不同鉴权 
        //SaRouter.match() 匹配函数有两个参数  参数一:要匹配的path路由
        //									参数二:要执行的校验函数。
        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("/**");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

这里就做简单的示例,更多玩法:路由拦截鉴权 (sa-token.cc)

2. 登录认证

额… 看了一圈,好像是没有密码校验这一功能的(也可能是没找到),但是有密码加密,所以这里的代码就自己写一下校验了

@Autowired
private LoginService loginService;

@RequestMapping("/login")
protected String login(String acc,String pwd) {
    //查询数据库中的账号
    User user = loginService.getUser(acc);

    //判断账号是否存在
    //密码是否相同(加密后)
    if (user != null && SaSecureUtil.md5(pwd).equals(user.getPwd()) ){
        StpUtil.login(acc);
        return "登录成功";
    }

    return "登录失败";

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

常用的密码加密,更多密码加密方式 密码加密 (sa-token.cc)

// md5加密
SaSecureUtil.md5(“123456”);

// sha1加密
SaSecureUtil.sha1(“123456”);

// sha256加密
SaSecureUtil.sha256(“123456”);

这里我们再写一个 查看登陆状态 的接口测试一下我们拦截器和登录的效果:

@RequestMapping("/isLogin")
public String isLogin() {
    return "当前会话是否登录:" + StpUtil.isLogin();
}
  • 1
  • 2
  • 3
  • 4

此时我们访问 isLogin 接口:HTTP Status 500 – Internal Server Error.

访问 login 接口,并携带 acc 和 pwd:登录成功.

再访问 isLogin 接口:当前会话是否登录:true.

简单分析一下登录访问流程:

  1. 用户提交 name + password 参数,调用登录接口。
  2. 登录成功,返回这个用户的 Token 会话凭证。
  3. 用户后续的每次请求,都携带上这个 Token。
  4. 服务器根据 Token 判断此会话是否登录成功。

我们暂时不需要完整的了解整个登录过程,只需要记住关键一点:Sa-Token 为这个账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端

此处仅仅做了会话登录,但并没有主动向前端返回 Token 信息。 是因为不需要吗?严格来讲是需要的,只不过 StpUtil.login() 方法利用了 Cookie 自动注入的特性,省略了手写返回 Token 的代码。

至此,简单的登录认证就已经完成了,更多相关功能比如:退出登录、注销登录、踢人下线等可以访问 StpUtil-鉴权工具类 (sa-token.cc) 查看

3. 权限认证
3.1 添加权限

首先我们要做的就是去数据库中,把当前用户的权限、角色数据给获取出来,这一步就使用 假数据来代替了

那么现在应该做的就是实现赋权:新建一个类,实现 StpInterface接口,重写其中 获取权限列表 获取角色列表 两个方法

@Component
public class StpInterfaceImpl implements StpInterface {

    /**
     * 返回一个账号所拥有的权限码集合
     */
    @Override
    public List<String> getPermissionList(Object o, String s) {
        // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限
        List<String> list = new ArrayList<String>();
        list.add("101");
        list.add("user.add");
        list.add("user.update");
        list.add("art.*");
        return list;
    }

    /**
     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
     */
    @Override
    public List<String> getRoleList(Object o, String s) {
        // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色
        List<String> list = new ArrayList<String>();
        list.add("admin");
        list.add("super-admin");
        return list;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
3.2 StpUtil鉴权

写一个接口做简单测试:

@RequestMapping("/PermissionsCheck")
public void PermissionsCheck() {
	//返回类型boolean
    System.out.println("是否拥有权限user.add:" + StpUtil.hasPermission("user.add"));
	//返回类型List
    System.out.println("当前账号拥有的所有权限: " + StpUtil.getPermissionList());

    System.out.println("是否拥有角色admin:" + StpUtil.hasRole("admin"));

    System.out.println("当前账号拥有的所有角色: " + StpUtil.getRoleList());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
3.3 注解鉴权

学过 Shori 和 SpringSecurity 知道他们都可以使用注解鉴权,这里当然也是可以使用注解鉴权的

写一个接口,使用注解 @SaCheckRole("admin") 检验角色:

@RequestMapping("/role")
@SaCheckRole("admin")
public String role() {
    return ("角色匹配,允许访问");
}
  • 1
  • 2
  • 3
  • 4
  • 5

还有其他的注解,比如:

@SaIgnore 如果你的拦截拦截器里面写的是/**拦截所有,那么这个注解就能使被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权

@SaCheckPermission("user:add") 权限校验,不需要多解释

更多注解查看 注解鉴权 (sa-token.cc)

使用拦截器模式,只能在Controller层进行注解鉴权,如需在任意层级使用注解鉴权,请参考:AOP注解鉴权

3.4 权限异常拦截

经过上面的测试,权限正确能够正常的访问接口,但是当权限不够时会报出 NotRoleException ,此时我们可以创建一个全局异常拦截器,统一返回给前端

@RestControllerAdvice
public class GlobalException {

    // 拦截:未登录异常
    @ExceptionHandler(NotLoginException.class)
    public SaResult handlerException(NotLoginException e) {

        // 打印堆栈,以供调试
        e.printStackTrace();

        // 返回给前端
        return SaResult.error(e.getMessage());
    }

    // 拦截:缺少权限异常
    @ExceptionHandler(NotPermissionException.class)
    public SaResult handlerException(NotPermissionException e) {
        e.printStackTrace();
        return SaResult.error("缺少权限:" + e.getPermission());
    }

    // 拦截:缺少角色异常
    @ExceptionHandler(NotRoleException.class)
    public SaResult handlerException(NotRoleException e) {
        e.printStackTrace();
        return SaResult.error("缺少角色:" + e.getRole());
    }

    // 拦截:二级认证校验失败异常
    @ExceptionHandler(NotSafeException.class)
    public SaResult handlerException(NotSafeException e) {
        e.printStackTrace();
        return SaResult.error("二级认证校验失败:" + e.getService());
    }

    // 拦截:服务封禁异常
    @ExceptionHandler(DisableServiceException.class)
    public SaResult handlerException(DisableServiceException e) {
        e.printStackTrace();
        return SaResult.error("当前账号 " + e.getService() + " 服务已被封禁 (level=" + e.getLevel() + "):" + e.getDisableTime() + "秒后解封");
    }

    // 拦截:Http Basic 校验失败异常
    @ExceptionHandler(NotBasicAuthException.class)
    public SaResult handlerException(NotBasicAuthException e) {
        e.printStackTrace();
        return SaResult.error(e.getMessage());
    }

    // 拦截:其它所有异常
    @ExceptionHandler(Exception.class)
    public SaResult handlerException(Exception e) {
        e.printStackTrace();
        return SaResult.error(e.getMessage());
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
4. 集成Redis

Sa-Token 提供了扩展接口,可以轻松将会话数据存储在 RedisMemcached等专业的缓存中间件中, 做到重启数据不丢失,而且保证分布式环境下多节点的会话一致性。

依赖:

<!-- Sa-Token 整合 Redis (使用 jdk 默认序列化方式) -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-dao-redis</artifactId>
    <version>1.34.0</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

yml配置:

spring:
  # redis配置
  redis:
    # Redis数据库索引(默认为0)
    database: 0
    # Redis服务器地址
    host: **.***.**.***
    # Redis服务器连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    password: ******
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池最大连接数
        max-active: 200
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
        # 连接池中的最大空闲连接
        max-idle: 10
        # 连接池中的最小空闲连接
        min-idle: 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

接下来…

没有接下来了,因为集成 Redis 后,框架自动保存。集成 Redis 只需要引入对应的 pom依赖 即可,框架所有上层 API 保持不变。

非常好用

5. 前后端分离

从上面的认证我们可以得知,Sa-Token为当前账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端

其实这里sa-token做了两步操作:

  1. 后端控制写入 Cookie
  2. 每次请求自动携带 Cookie

那么如果在一些场景下后端没办法控制 Cookie 的写入了怎么办?

这些情况还是比较常见的,比如:

1. 小程序不支持 Cookie  
1. App端支持 Cookie  但是不支持自动保持
1. 浏览器禁用了 Cookie  的情况
  • 1
  • 2
  • 3

当使用非 H5 端协议(例如HTTP)时,服务器可以通过发送Set-Cookie头部来让客户端保存Cookie。然而,客户端不会自动将这些Cookie保存下来并自动发送回服务器。相反,客户端应该手动管理Cookie,并在需要时将它们添加到请求中发送给服务器。这意味着客户端需要使用编程语言和技术来处理和管理Cookie,而不是依赖于自动化的行为。

当出现这些情况时,我们就需要

  1. 在登陆时返回 Token
  2. 在访问后台时携带 Token
返回 Token

继续在登录代码的基础上修改:

...
    if (user != null && SaSecureUtil.md5(pwd).equals(user.getPwd())){
        //登录
        StpUtil.login(acc);
        //取Token
        String tokenValue = StpUtil.getTokenInfo().tokenValue
        //这里直接返回token值
        return tokenValue;
    }

    return "登录失败";
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
携带 Token

这里以 uni-app 举例

//将返回的 tokenValue 存起来,uni-app可以存到本地里:
uni.setStorageSync('tokenValue', tokenValue);

uni.request({
    url: 'http://localhost:8081/Course/getData', 
    data: {},
    header: {
        'tokenValue': uni.getStorageSync('tokenValue') 
    },
    success: (res) => {
        
    }
});

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

将这些代码封装起来就简单又好用了

这里有一点需要注意的是: ‘tokenValue’ 这个名字,token 的键名必须要是这一个,否则sa-token没办法自动读取到 token 值,进行鉴权

键名从哪里得知,其实 StpUtil.getTokenInfo() 获取出来的是一个 SaTokenInfo 对象,看一下就知道了

四、参考

官方文档

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/在线问答5/article/detail/933751
推荐阅读
相关标签