目录
框架文档地址:Sa-Token
一、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(); } }
运行
启动代码,从浏览器依次访问上述测试接口:
设计思路
对于一些登录之后才能访问的接口(例如:查询我的账号资料),我们通常的做法是增加一层接口校验:
那么,判断会话是否登录的依据是什么?我们先来简单分析一下登录访问流程:
name
+ password
参数,调用登录接口。所谓登录认证,指的就是服务器校验账号密码,为用户颁发 Token 会话凭证的过程,这个 Token 也是我们后续判断会话是否登录的关键所在。
登录
// 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, // 不可以传入复杂类型,如:User、Admin 等等 StpUtil.login(Object id); //另外在登录的时候可以把用户的登录时间、权限列表或者角色列表等信息存到redis中;
只此一句代码,便可以使会话登录成功,实际上,Sa-Token 在背后做了大量的工作,包括但不限于:
Token
凭证与 Session
会话Token
注入到请求上下文你暂时不需要完整的了解整个登录过程,你只需要记住关键一点:Sa-Token 为这个账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端
。(Sa-Token默认的token生成策略是uuid,可以在配置文件中将token生成设置为其他策略)StpUtil.login(id)
方法利用了 Cookie 自动注入的特性,省略了你手写返回 Token 的代码。
所以如果在前后端分离的模式下(比如app或者小程序)没有cookie的时候,我们就要手动的传递和接收token。所谓 Cookie ,本质上就是一个特殊的header
参数而已
// 登录接口 @RequestMapping("doLogin") public SaResult doLogin() { // 第1步,先登录上 StpUtil.login(10001); // 第2步,获取 Token 相关参数 SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); // 第3步,返回给前端 return SaResult.data(tokenInfo); }
// 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 ListgetPermissionList(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.add
、art.delete
、art.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加白某些接口。
Sa-Token的登录授权,默认就是[记住我]
模式,为了实现[非记住我]
模式,你需要在登录时如下设置:
// 设置登录账号id为10001,第二个参数指定是否为[记住我],当此值为false后,关闭浏览器后再次打开需要重新登录 StpUtil.login(10001, false);
实现原理
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看作是一个单独的认证服务。不处理业务逻辑,只是做用户信息的管理以及授权给第三方应用。
单点登录的问题:
首先我们分析一下多个系统之间,为什么无法同步登录状态?实现单点登录就要解决这两个问题
Token
无法在多个系统下共享。Session
无法在多个系统间共享。前端同域情况下:
共享Cookie
来解决 Token 共享问题。Redis
来解决 Session 共享问题。所谓共享Cookie,就是主域名Cookie在二级域名下的共享,举个例子:写在父域名stp.com
下的Cookie,在s1.stp.com
、s2.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,就不需要再进行登录,从而实现了单点登录
前端不同域情况下:
url重定向传播
来解决 Token 共享问题。Redis
来解决 Session 共享问题。对浏览器来说,SSO 域下返回的数据要怎么存,才能在访问 A 的时候带上?浏览器对跨域有严格限制,cookie、localStorage 等方式都是有域限制的。