Java认证鉴权框架:Sa-Token

目录

框架文档地址:Sa-Token

一、Springboot集成Satoken

二、登录注销等功能

三、权限校验功能

四、记住我功能

五、侦听器功能

六、单点登录


框架文档地址:Sa-Token

Java认证鉴权框架:Sa-Token_第1张图片

一、Springboot集成Satoken

添加依赖

注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。



    cn.dev33
    sa-token-spring-boot-starter
    1.34.0

设置配置文件

# 端口
server.port=8081
    
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############


# token名称 (同时也是cookie名称)
sa-token.token-name=satoken
# token有效期,单位s 默认30天, -1代表永不过期 
sa-token.timeout=2592000
# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
sa-token.activity-timeout=-1
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) 
sa-token.is-concurrent=true
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) 
sa-token.is-share=true
# token风格
sa-token.token-style=uuid
# 是否输出操作日志 
sa-token.is-log=false

启动类

在项目中新建包 com.pj ,在此包内新建主类 SaTokenDemoApplication.java,复制以下代码:

@SpringBootApplication
public class SaTokenDemoApplication {
    public static void main(String[] args) throws JsonProcessingException {
        SpringApplication.run(SaTokenDemoApplication.class, args);
        System.out.println("启动成功:Sa-Token配置如下:" + SaManager.getConfig());
    }
}

Controller类

@RestController
@RequestMapping("/user/")
public class UserController {


    // 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456
    @RequestMapping("doLogin")
    public String doLogin(String username, String password) {
        // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 
        if("zhang".equals(username) && "123456".equals(password)) {
            StpUtil.login(10001);
            return "登录成功";
        }
        return "登录失败";
    }


    // 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin
    @RequestMapping("isLogin")
    public String isLogin() {
        return "当前会话是否登录:" + StpUtil.isLogin();
    }
    
}

运行

启动代码,从浏览器依次访问上述测试接口:

Java认证鉴权框架:Sa-Token_第2张图片

二、登录注销等功能

设计思路

对于一些登录之后才能访问的接口(例如:查询我的账号资料),我们通常的做法是增加一层接口校验:

  • 如果校验通过,则:正常返回数据。
  • 如果校验未通过,则:抛出异常,告知其需要先进行登录。

那么,判断会话是否登录的依据是什么?我们先来简单分析一下登录访问流程:

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

所谓登录认证,指的就是服务器校验账号密码,为用户颁发 Token 会话凭证的过程,这个 Token 也是我们后续判断会话是否登录的关键所在。

登录

// 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 
// 不可以传入复杂类型,如:User、Admin 等等
StpUtil.login(Object id);     
//另外在登录的时候可以把用户的登录时间、权限列表或者角色列表等信息存到redis中;

只此一句代码,便可以使会话登录成功,实际上,Sa-Token 在背后做了大量的工作,包括但不限于:

  1. 检查此账号是否之前已有登录
  2. 为账号生成 Token 凭证与 Session 会话
  3. 通知全局侦听器,xx 账号登录成功
  4. 将 Token 注入到请求上下文
  5. 等等其它工作……

你暂时不需要完整的了解整个登录过程,你只需要记住关键一点:Sa-Token 为这个账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端。(Sa-Token默认的token生成策略是uuid,可以在配置文件中将token生成设置为其他策略)StpUtil.login(id) 方法利用了 Cookie 自动注入的特性,省略了你手写返回 Token 的代码。

所以如果在前后端分离的模式下(比如app或者小程序)没有cookie的时候,我们就要手动的传递和接收token。所谓 Cookie ,本质上就是一个特殊的header参数而已Java认证鉴权框架:Sa-Token_第3张图片

// 登录接口
@RequestMapping("doLogin")
public SaResult doLogin() {
    // 第1步,先登录上 
    StpUtil.login(10001);
    // 第2步,获取 Token  相关参数 
    SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
    // 第3步,返回给前端 
    return SaResult.data(tokenInfo);
}

Java认证鉴权框架:Sa-Token_第4张图片

// 1、首先在登录时,将 tokenValue 存储在本地,例如:
uni.setStorageSync('tokenValue', tokenValue);


// 2、在发起ajax请求的地方,获取这个值,并塞到header里 
uni.request({
    url: 'https://www.example.com/request', // 仅为示例,并非真实接口地址。
    header: {
        "content-type": "application/x-www-form-urlencoded",
        "satoken": uni.getStorageSync('tokenValue')        // 关键代码, 注意参数名字是 satoken 
    },
    success: (res) => {
        console.log(res.data);    
    }
});

所以一般情况下,我们的登录接口代码,会大致类似如下:

// 会话登录接口 
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {
    // 第一步:比对前端提交的账号名称、密码
    if("zhang".equals(name) && "123456".equals(pwd)) {
        // 第二步:根据账号id,进行登录 
        StpUtil.login(10001);
        return SaResult.ok("登录成功");
    }
    return SaResult.error("登录失败");
}

注销、判断是否登录、获取登录id

// 当前会话注销登录
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`类型

从源码可以看出处理的功能为:从HttpServletRequest的getAttribute里面根据token名称获取token值,获取不到再根据配置的token存放方式从HttpServletRequest中获取(存放方式分为参数传递、head传递、cookie传递),token值是从接口访问请求中获取到的。获取到token之后,再根据此token值找到它对应的身份id

三、权限校验功能

设计思路

所谓权限认证,核心逻辑就是判断一个账号是否拥有指定权限:

  • 有,就让你通过。
  • 没有?那么禁止访问!

深入到底层数据中,就是每个账号都会拥有一个权限码集合,框架来校验这个集合中是否包含指定的权限码。

例如:当前账号拥有权限码集合 ["user-add", "user-delete", "user-get"],这时候我来校验权限 "user-update",则其结果就是:验证失败,禁止访问。

实现获取权限集合接口

接口的调用往往需要权限的校验,一般的系统会给用户绑定某种角色,再给此角色分配权限,设置权限码,具有此角色或者权限码才放行请求。sa-token实现权限需要进行自定义扩展,下面是获取一个用户权限码集合和角色标识集合的类:

/**
 * 自定义权限验证接口扩展
 */
@Component    // 保证此类被SpringBoot扫描,完成Sa-Token的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {


    /**
     * 返回一个账号所拥有的权限码集合
     */
    @Override
    public List getPermissionList(Object loginId, String loginType) {
        // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限
        //1.先从redis中根据loginId取该用户的权限,有则直接返回
        //2.若是redis中没有,则从数据库中查询,再把结果添加到redis中
        List list = new ArrayList();
        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 getRoleList(Object loginId, String loginType) {
        // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色
        List list = new ArrayList();
        list.add("admin");
        list.add("super-admin");
        return list;
    }


}

权限校验的方法

(调用角色校验和权限校验方法的时候,satoken会调用上面我们实现的StpInterface接口类的对应方法,拿到此用户的权限码集合后,再将权限码和待校验的字符串进行判断是否有权限)

// 获取:当前账号所拥有的权限集合
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");    

扩展:NotPermissionException 对象可通过 getLoginType() 方法获取具体是哪个 StpLogic 抛出的异常

Sa-Token允许你根据通配符指定泛权限,例如当一个账号拥有art.*的权限时,art.addart.deleteart.update都将匹配通过

角色校验的方法

(调用角色校验和权限校验方法的时候,satoken会调用上面我们实现的StpInterface接口类的对应方法,拿到此用户的权限码集合后,再将权限码和待校验的字符串进行判断是否有权限)

// 获取:当前账号所拥有的角色集合
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");        

扩展:NotPermissionException 对象可通过 getLoginType() 方法获取具体是哪个 StpLogic 抛出的异常

路由拦截鉴权

在调用后台服务时,我们可以在路由时做一些拦截,例如添加登陆权限拦截、放开一些接口白名单等。看下官网给出的案例:

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
    // 注册 Sa-Token 的拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册路由拦截器,自定义认证规则
        registry.addInterceptor(new SaInterceptor(handler -> {


//SaRouter.match(参数一:需要拦截的路由,参数二:可排除的路由,参数三:用来检验是否通过拦截的方法)
            
            // 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
            SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin());


            // 角色校验 -- 拦截以 admin 开头的路由,必须具备 admin 角色或者 super-admin 角色才可以通过认证
            SaRouter.match("/admin/**", r -> StpUtil.checkRoleOr("admin", "super-admin"));


            // 权限校验 -- 不同模块校验不同权限
            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"));


            // 甚至你可以随意的写一个打印语句
            SaRouter.match("/**", r -> System.out.println("----啦啦啦----"));


            // 连缀写法
            SaRouter.match("/**").check(r -> System.out.println("----啦啦啦----"));


        })).addPathPatterns("/**")
           .excludePathPatterns("/login");
    }
}

从此案例中可以看到:自定义路由拦截器需要实现WebMvcConfigurer接口,重写addInterceptors方法,定义拦截规则。例如:拦截所有接口,对login登陆接口开放,都需要校验用户已经登陆了才进行放行。SaRouter配置的拦截器可以细化到某个接口需要有某种权限才进行放行,也可以通过HandlerInterceptor的addPathPatterns加入拦截规则、excludePathPatterns加白某些接口。

四、记住我功能

Java认证鉴权框架:Sa-Token_第5张图片

Sa-Token的登录授权,默认就是[记住我]模式,为了实现[非记住我]模式,你需要在登录时如下设置:

// 设置登录账号id为10001,第二个参数指定是否为[记住我],当此值为false后,关闭浏览器后再次打开需要重新登录
StpUtil.login(10001, false);

实现原理

Cookie作为浏览器提供的默认会话跟踪机制,其生命周期有两种形式,分别是:

  • 临时Cookie:有效期为本次会话,只要关闭浏览器窗口,Cookie就会消失。
  • 持久Cookie:有效期为一个具体的时间,在时间未到期之前,即使用户关闭了浏览器Cookie也不会消失。

利用Cookie的此特性,我们便可以轻松实现 [记住我] 模式:

  • 勾选 [记住我] 按钮时:调用StpUtil.login(10001, true),在浏览器写入一个持久Cookie储存 Token,此时用户即使重启浏览器 Token 依然有效。
  • 不勾选 [记住我] 按钮时:调用StpUtil.login(10001, false),在浏览器写入一个临时Cookie储存 Token,此时用户在重启浏览器后 Token 便会消失,导致会话失效。

(前后端分离模式下没有cookie就只能在前端设置过期时间)

五、侦听器功能

当系统用户登录、登出、被踢下线等操作时,系统希望记录下日志信息,此时就可以使用侦听器来实现。看下官网给的案例:

/**
 * 自定义侦听器的实现 
 */
@Component
public class MySaTokenListener implements SaTokenListener {


    /** 每次登录时触发 */
    @Override
    public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
        System.out.println("---------- 自定义侦听器实现 doLogin");
    }


    /** 每次注销时触发 */
    @Override
    public void doLogout(String loginType, Object loginId, String tokenValue) {
        System.out.println("---------- 自定义侦听器实现 doLogout");
    }


    /** 每次被踢下线时触发 */
    @Override
    public void doKickout(String loginType, Object loginId, String tokenValue) {
        System.out.println("---------- 自定义侦听器实现 doKickout");
    }


    /** 每次被顶下线时触发 */
    @Override
    public void doReplaced(String loginType, Object loginId, String tokenValue) {
        System.out.println("---------- 自定义侦听器实现 doReplaced");
    }


    /** 每次被封禁时触发 */
    @Override
    public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {
        System.out.println("---------- 自定义侦听器实现 doDisable");
    }


    /** 每次被解封时触发 */
    @Override
    public void doUntieDisable(String loginType, Object loginId, String service) {
        System.out.println("---------- 自定义侦听器实现 doUntieDisable");
    }


    /** 每次二级认证时触发 */
    @Override
    public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {
        System.out.println("---------- 自定义侦听器实现 doOpenSafe");
    }


    /** 每次退出二级认证时触发 */
    @Override
    public void doCloseSafe(String loginType, String tokenValue, String service) {
        System.out.println("---------- 自定义侦听器实现 doCloseSafe");
    }


    /** 每次创建Session时触发 */
    @Override
    public void doCreateSession(String id) {
        System.out.println("---------- 自定义侦听器实现 doCreateSession");
    }


    /** 每次注销Session时触发 */
    @Override
    public void doLogoutSession(String id) {
        System.out.println("---------- 自定义侦听器实现 doLogoutSession");
    }


    /** 每次Token续期时触发 */
    @Override
    public void doRenewTimeout(String tokenValue, Object loginId, long timeout) {
        System.out.println("---------- 自定义侦听器实现 doRenewTimeout");
    }
}

从此案例中可以看到:自定义侦听器需要实现SaTokenListener接口,然后重写里面的方法,当对应的方法有调用时会触发监听方法,使用观察者设计模式实现。

六、单点登录

设计思路

举个场景,假设我们的系统被切割为N个部分:商城、论坛、直播、社交…… 如果用户每访问一个模块都要登录一次,那么用户将会疯掉, 为了优化用户体验,我们急需一套机制将这N个系统的认证授权互通共享,让用户在一个系统登录之后,便可以访问其它所有系统。

我们需要搭建一个sso认证中心,就是不管那个系统需要登录认证都跳转到这个认证中心进行登录和认证。把SSO看作是一个单独的认证服务。不处理业务逻辑,只是做用户信息的管理以及授权给第三方应用。

单点登录的问题:

首先我们分析一下多个系统之间,为什么无法同步登录状态?实现单点登录就要解决这两个问题

  1. 前端的 Token 无法在多个系统下共享。
  2. 后端的 Session 无法在多个系统间共享。

前端同域情况下:

  1. 使用 共享Cookie 来解决 Token 共享问题。
  2. 使用 Redis 来解决 Session 共享问题。

所谓共享Cookie,就是主域名Cookie在二级域名下的共享,举个例子:写在父域名stp.com下的Cookie,在s1.stp.coms2.stp.com等子域名都是可以共享访问的。 所以后端在设置cookie的作用域的时候可以将其写入父级域名比如stp.com

我们在satoken框架配置文件中设置cookie的作用域为主域,同时后端服务器连接同一个redis

# 配置 Cookie 作用域 
sa-token.cookie.domain=stp.com

之后,比如在s1.stp.com 的子系统1登录之后返回一个在父域名stp.com下的Cookie 的token,在s2.stp.com 的子系统2去访问的时候带着这个token,就不需要再进行登录,从而实现了单点登录

前端不同域情况下:

  1. 使用 url重定向传播 来解决 Token 共享问题。
  2. 使用 Redis 来解决 Session 共享问题。

Java认证鉴权框架:Sa-Token_第6张图片

  • 用户进入 A 系统,没有登录凭证(ticket),A 系统给他跳到 SSO
  • SSO 没登录过,也就没有 sso 系统下没有凭证(注意这个和前面 A ticket 是两回事),输入账号密码登录
  • SSO 账号密码验证成功,通过接口返回做两件事:一是种下 sso 系统下凭证(记录用户在 SSO 登录状态);二是下发一个 ticket
  • 客户端拿到 ticket,保存起来,带着请求系统 A 接口
  • 系统 A 校验 ticket,成功后正常处理业务请求
  • 此时用户第一次进入系统 B,没有登录凭证(ticket),B 系统给他跳到 SSO
  • SSO 登录过,系统下有凭证,不用再次登录,只需要下发 ticket
  • 客户端拿到 ticket,保存起来,带着请求系统 B 接口

对浏览器来说,SSO 域下返回的数据要怎么存,才能在访问 A 的时候带上?浏览器对跨域有严格限制,cookie、localStorage 等方式都是有域限制的。

Java认证鉴权框架:Sa-Token_第7张图片

  • 在 SSO 域下,SSO 不是通过接口把 ticket 直接返回,而是通过一个带 code 的 URL 重定向到系统 A 的接口上,这个接口通常在 A 向 SSO 注册时约定
  • 浏览器被重定向到 A 域下,带着 code 访问了 A 的 callback 接口,callback 接口通过 code 换取 ticket
  • 这个 code 不同于 ticket,code 是一次性的,暴露在 URL 中,只为了传一下换 ticket,换完就失效
  • callback 接口拿到 ticket 后,在自己的域下 set cookie 成功
  • 在后续请求中,只需要把 cookie 中的 ticket 解析出来,去 SSO 验证就好
  • 访问 B 系统也是一样

你可能感兴趣的:(java,spring,boot,spring)