文章目录

  • 1.简介
  • 2.登陆流程分析
  • 3.获取用户id分析
  • 4.向浏览器写入cookie分析
  • 5.权限验证流程分析
  • 6.路由拦截分析
  • 7.侦听器分析

1.简介

​ Sa-Token是一个轻量级Java权限认证框架,是国产开源框架,引入非常简单,丰富的自定义扩展,主要解决:登陆认证、权限认证、单点登陆、Oauth2.0、分布式Session会话、微服务网关鉴权等一系列权限相关问题。更多的功能点可以结合官网:https://sa-token.cc/doc.html#/进行学习,这里主要对部分功能点进行分析说明。

集成到spring boot非常简单,集成步骤为:

(1)pom.xml引入依赖

        <!-- Sa-Token 权限认证,在线文档:https://sa-token.cc --><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-boot-starter</artifactId><version>1.34.0</version></dependency>

(2)application.yml配置文件

server:port: 8090############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
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

2.登陆流程分析

​ 登陆信息验证通过后,会话登陆特别简单,一行代码解决:StpUtil.login(userId),具体来看下此登陆方法做的处理。StpUtil是一个工具类,提供了一个登陆的方法login,看源码①:

    //StpUtil类里创建了一个StpLogin类public static StpLogic stpLogic = new StpLogic("login");//登陆方法public static void login(Object id) {//调用StpLogin类的login方法stpLogic.login(id);}

从源码可以看出StpUtil类里创建了一个StpLogin类,login方法最终调用的是StpLogic类的login方法,看StpLogic类的login源码②:

 public void login(Object id) {//调用类里的login方法,并且创建一个用于记录登陆相关配置信息的类SaLoginModelthis.login(id, new SaLoginModel());}

从源码可以看出调用类里的login方法,并且创建一个用于记录登陆相关配置信息的类SaLoginModel,看下this.login(id, new SaLoginModel())源码③:

 public void login(Object id, SaLoginModel loginModel) {//创建登陆的Session并且获取token值String token = this.createLoginSession(id, loginModel);//把token信息写入HttpServletRequest、HttpServletResponse中this.setTokenValue(token, loginModel);}

从源码中可以看出处理功能为:创建登陆的Session并且获取token值、把token信息写入HttpServletRequest、HttpServletResponse中。看下this.createLoginSession(id, loginModel)的源码④:

 public String createLoginSession(Object id, SaLoginModel loginModel) {SaTokenException.throwByNull(id, "账号id不能为空", 11002);//获取yml或者properties中配置的信息SaTokenConfig config = this.getConfig();//使用SaLoginModel接收config的timeout、isWriteHeader字段值loginModel.build(config);//获取token值String tokenValue = this.distUsableToken(id, loginModel);//创建sessionSaSession session = this.getSessionByLoginId(id, true);//设置session属性session.updateMinTimeout(loginModel.getTimeout());session.addTokenSign(tokenValue, loginModel.getDeviceOrDefault());//把token信息保存到dataMap中this.saveTokenToIdMapping(tokenValue, id, loginModel.getTimeout());this.setLastActivityToNow(tokenValue);//向事件中心添加事件监听SaTokenEventCenter.doLogin(this.loginType, id, tokenValue, loginModel);if (config.getMaxLoginCount() != -1) {this.logoutByMaxLoginCount(id, session, (String)null, config.getMaxLoginCount());}return tokenValue;}

从源码可以看出处理的功能为:根据yml或者properties配置信息创建token值,创建Session,把token信息保存到SaTokenDao的一个Map集合里面。看一下获取token方法this.distUsableToken的源码⑤:

 protected String distUsableToken(Object id, SaLoginModel loginModel) {//获取配置变量中是否并发登陆Boolean isConcurrent = this.getConfig().getIsConcurrent();if (!isConcurrent) {this.replaced(id, loginModel.getDevice());}if (SaFoxUtil.isNotEmpty(loginModel.getToken())) {return loginModel.getToken();} else {//多人登陆相同账号时,是否共享tokenif (isConcurrent && this.getConfigOfIsShare()) {String tokenValue = this.getTokenValueByLoginId(id, loginModel.getDeviceOrDefault());if (SaFoxUtil.isNotEmpty(tokenValue)) {return tokenValue;}}//创建token值return this.createTokenValue(id, loginModel.getDeviceOrDefault(), loginModel.getTimeout(), loginModel.getExtraData());}}

从源码可以看出处理的功能为:获取是否可以并发登陆、多人登陆相同账号时,是否共享token,然后再创建token。看一下生成token的方法createTokenValue源码⑥:

    public String createTokenValue(Object loginId, String device, long timeout, Map<String, Object> extraData) {//传递参数生成token值,createToken是一个函数return (String)SaStrategy.me.createToken.apply(loginId, this.loginType);}

从源码可以看出处理的功能为:调用createToken函数,传递参数,生成token。看一下生成token的函数createToken源码⑦:

 public BiFunction<Object, String, String> createToken = (loginId, loginType) -> {//获取配置变量中指定的token生成方式sa-token.token-styleString tokenStyle = SaManager.getConfig().getTokenStyle();if ("uuid".equals(tokenStyle)) {//使用uuid的方式return UUID.randomUUID().toString();} else if ("simple-uuid".equals(tokenStyle)) {//uuid去掉中杠的字符串return UUID.randomUUID().toString().replaceAll("-", "");} else if ("random-32".equals(tokenStyle)) {//生成32位长度的随机字符串return SaFoxUtil.getRandomString(32);} else if ("random-64".equals(tokenStyle)) {return SaFoxUtil.getRandomString(64);} else if ("random-128".equals(tokenStyle)) {return SaFoxUtil.getRandomString(128);} else {return "tik".equals(tokenStyle) ? SaFoxUtil.getRandomString(2) + "_" + SaFoxUtil.getRandomString(14) + "_" + SaFoxUtil.getRandomString(16) + "__" : UUID.randomUUID().toString();}};//根据指定长度从数组字母组成的字符串中生成一个随机组合的新字符串public static String getRandomString(int length) {String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";StringBuilder sb = new StringBuilder();for(int i = 0; i < length; ++i) {int number = ThreadLocalRandom.current().nextInt(62);sb.append(str.charAt(number));}return sb.toString();}

从源码可以看出处理的功能为:createToken是一个函数,根据配置的token生成方式sa-token.token-style,进行对应token的生成,到此token的生成结束。

生成的token存放到Map集合中,看下源码④处的this.saveTokenToIdMapping方法源码⑧:

    public void saveTokenToIdMapping(String tokenValue, Object loginId, long timeout) {//保存token到map集合中,this.getSaTokenDao()是一个SaTokenDao的接口,只有一个默认实现类SaTokenDaoDefaultImpl,调用它的保存方法this.getSaTokenDao().set(this.splicingKeyTokenValue(tokenValue), String.valueOf(loginId), timeout);}//this.getSaTokenDao()方法,获取SatokenDaopublic SaTokenDao getSaTokenDao() {return SaManager.getSaTokenDao();}//SaManager.getSaTokenDao()的方法,获取SatokenDaopublic static SaTokenDao getSaTokenDao() {if (saTokenDao == null) {Class var0 = SaManager.class;synchronized(SaManager.class) {if (saTokenDao == null) {//SaTokenDaoDefaultImpl是SaTokenDao接口的唯一实现类setSaTokenDaoMethod(new SaTokenDaoDefaultImpl());}}}return saTokenDao;}//this.splicingKeyTokenValue方法,生成把token值存放到map中的key值public String splicingKeyTokenValue(String tokenValue) {//this.getConfig().getTokenName():sa-token.token-name配置的值return this.getConfig().getTokenName() + ":" + this.loginType + ":token:" + tokenValue;}

从源码可以看出处理的功能为:从管理器中获取SatokenDao接口类,SaTokenDaoDefaultImpl是SaTokenDao接口的唯一实现类,获取到的最终类型为SaTokenDaoDefaultImpl类,token需要存放到Map集合中,调用方法splicingKeyTokenValue生成map的key。看下SaTokenDaoDefaultImpl存储token信息的源码⑨:

public class SaTokenDaoDefaultImpl implements SaTokenDao {//记录值的mappublic Map<String, Object> dataMap = new ConcurrentHashMap();//记录过期时间的mappublic Map<String, Long> expireMap = new ConcurrentHashMap();//根据key获取值的方法public String get(String key) {//校验此key是否已经过期,过期则删除this.clearKeyByTimeout(key);//返回值return (String)this.dataMap.get(key);}//把值设置到map中public void set(String key, String value, long timeout) {//判断过期时间if (timeout != 0L && timeout > -2L) {//值设置到Map中this.dataMap.put(key, value);//记录此key的过期时间:当前时间加上设置的过期时间this.expireMap.put(key, timeout == -1L ? -1L : System.currentTimeMillis() + timeout * 1000L);}}//根据key判断是否过期,过期则从map中把值删除void clearKeyByTimeout(String key) {//获取到key对应的过期时间Long expirationTime = (Long)this.expireMap.get(key);//过期时间不为空,且不等于-1,并且过期时间要小于当前系统时间,则说明此key过期,删除对应的map值if (expirationTime != null && expirationTime != -1L && expirationTime < System.currentTimeMillis()) {this.dataMap.remove(key);this.expireMap.remove(key);}}
}

从源码可以看出处理的功能为:数据都是用map进行存储的,一个dataMap存储具体值,一个expireMap存储此key的过期时间,set进来记录的这两个map的key都是同一个,过期时间=当前时间加上设置的过期时间;当根据key取值时,先判断是否过期,过期则从dataMap中把值删除,从expireMap把过期时间删除。

3.获取用户id分析

​ 上面通过StpUtil.login(userId)设置进去的id值,在接口访问中需要查询当前用户的id,也可以通过一行代码StpUtil.getLoginId()进行获取。看getLoginId源码⑩:

   //获取登录用户的idpublic static Object getLoginId() {return stpLogic.getLoginId();}//stpLogic.getLoginId()方法public Object getLoginId() {//是否是切换用户if (this.isSwitch()) {//返回切换用户的idreturn this.getSwitchLoginId();} else {//获取token值String tokenValue = this.getTokenValue();if (tokenValue == null) {throw NotLoginException.newInstance(this.loginType, "-1").setCode(11011);} else {//根据token值获取id值String loginId = this.getLoginIdNotHandle(tokenValue);if (loginId == null) {throw NotLoginException.newInstance(this.loginType, "-2", tokenValue).setCode(11012);} else if (loginId.equals("-3")) {throw NotLoginException.newInstance(this.loginType, "-3", tokenValue).setCode(11013);} else if (loginId.equals("-4")) {throw NotLoginException.newInstance(this.loginType, "-4", tokenValue).setCode(11014);} else if (loginId.equals("-5")) {throw NotLoginException.newInstance(this.loginType, "-5", tokenValue).setCode(11015);} else {this.checkActivityTimeout(tokenValue);if (this.getConfig().getAutoRenew()) {this.updateLastActivityToNow(tokenValue);}return loginId;}}}}

从源码可以看出处理的功能为:先判断当前是否处于切换用户阶段(sa-token提供临时切换用户,具体可以看官网),是则返回切换用户的id;先获取token值,在使用token值获取用户id。看下获取token值的源码⑪:

  public String getTokenValue() {//获取token值String tokenValue = this.getTokenValueNotCut();//看token是否配置了前缀String tokenPrefix = this.getConfig().getTokenPrefix();if (!SaFoxUtil.isEmpty(tokenPrefix)) {//配置了前缀,则进行token值的截取if (!SaFoxUtil.isEmpty(tokenValue) && tokenValue.startsWith(tokenPrefix + " ")) {tokenValue = tokenValue.substring(tokenPrefix.length() + " ".length());} else {tokenValue = null;}}return tokenValue;}

从源码可以看出处理的功能为:获取token值,看token是否配置了前缀,配置了前缀,则进行token值的截取。看下获取token的方法getTokenValueNotCut()源码⑫:

   public String getTokenValueNotCut() {//获取SaStorageSaStorage storage = SaHolder.getStorage();//获取HttpServletRequestSaRequest request = SaHolder.getRequest();//获取配置信息类SaTokenConfig config = this.getConfig();//获取token的名称String keyTokenName = this.getTokenName();String tokenValue = null;//先从HttpServletRequest的getAttribute里面先获取if (storage.get(this.splicingKeyJustCreatedSave()) != null) {tokenValue = String.valueOf(storage.get(this.splicingKeyJustCreatedSave()));}//从HttpServletRequest中根据参数名获取token值if (tokenValue == null && config.getIsReadBody()) {tokenValue = request.getParam(keyTokenName);}//从HttpServletRequest中head参数名获取token值if (tokenValue == null && config.getIsReadHeader()) {tokenValue = request.getHeader(keyTokenName);}//从HttpServletRequest中Cookie参数名获取token值     if (tokenValue == null && config.getIsReadCookie()) {tokenValue = request.getCookieValue(keyTokenName);}return tokenValue;}

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

    //根据token获取用户id的方法public String getLoginIdNotHandle(String tokenValue) {//保存token到map集合中,this.getSaTokenDao()是一个SaTokenDao的接口,只有一个默认实现类SaTokenDaoDefaultImpl,调用它的获取数据return this.getSaTokenDao().get(this.splicingKeyTokenValue(tokenValue));}//this.splicingKeyTokenValue方法,生成token值存放到map的key值public String splicingKeyTokenValue(String tokenValue) {return this.getConfig().getTokenName() + ":" + this.loginType + ":token:" + tokenValue;}

从源码可以看出处理的功能为:根据token值,调用splicingKeyTokenValue生成key值,生成方式与存入的时候调用的是同一个方法,然后调用SaTokenDaoDefaultImpl类的get方法获取到用户id。看下SaTokenDaoDefaultImpl类的get方法源码⑭:

public class SaTokenDaoDefaultImpl implements SaTokenDao {//记录值的mappublic Map<String, Object> dataMap = new ConcurrentHashMap();//记录t过期时间的mappublic Map<String, Long> expireMap = new ConcurrentHashMap();//根据key获取值的方法public String get(String key) {//校验此key是否已经过期,过期则删除this.clearKeyByTimeout(key);//返回值return (String)this.dataMap.get(key);}//根据key判断是否过期,过期则从map中把值删除void clearKeyByTimeout(String key) {//获取到key对应的过期时间Long expirationTime = (Long)this.expireMap.get(key);//过期时间不为空,且不等于-1,并且过期时间要小于当前系统时间,则说明此key过期,删除对应的map值if (expirationTime != null && expirationTime != -1L && expirationTime < System.currentTimeMillis()) {this.dataMap.remove(key);this.expireMap.remove(key);}}
}

从源码可以看出处理的功能为:先判断是否过期,过期则从dataMap中把值删除,从expireMap把过期时间删除,然后再根据key从dataMap中返回value值。

4.向浏览器写入cookie分析

​ 当登陆成功调用StpUtil.login(userId)方法后,会向浏览器或者调用客户端写入带有token信息的cookie。

现在来分析下具体的实现原理。来看源码③处的this.setTokenValue方法源码⑮:

  public void setTokenValue(String tokenValue, SaLoginModel loginModel) {if (!SaFoxUtil.isEmpty(tokenValue)) {//把token值记录到Storage中this.setTokenValueToStorage(tokenValue);//参数isReadCookie默认是trueif (this.getConfig().getIsReadCookie()) {//向服务响应中写入记录token信息的cookiethis.setTokenValueToCookie(tokenValue, loginModel.getCookieTimeout());}if (loginModel.getIsWriteHeaderOrGlobalConfig()) {//把token信息写入响应头中this.setTokenValueToResponseHeader(tokenValue);}}}

从源码可以看出处理的功能为:参数isReadCookie默认是true,会向HttpServletResponse服务响应中写入记录token信息的cookie。看下this.setTokenValueToCookie的源码⑯:

   public void setTokenValueToCookie(String tokenValue, int cookieTimeout) {//获取cookie相关的配置SaCookieConfig cfg = this.getConfig().getCookie();//创建一个cookie,设置cookie的名称、过期时间、写入的域名等信息SaCookie cookie = (new SaCookie()).setName(this.getTokenName()).setValue(tokenValue).setMaxAge(cookieTimeout).setDomain(cfg.getDomain()).setPath(cfg.getPath()).setSecure(cfg.getSecure()).setHttpOnly(cfg.getHttpOnly()).setSameSite(cfg.getSameSite());//把cookie通过Response向客户端或者浏览器写回SaHolder.getResponse().addCookie(cookie);}//SaHolder.getResponse()调用到的方法,获取Responsepublic static SaResponse getResponse() {return SaManager.getSaTokenContextOrSecond().getResponse();}//SaManager.getSaTokenContextOrSecond().getResponse()调用到的方法public SaResponse getResponse() {return new SaResponseForServlet(SpringMVCUtil.getResponse());}//SpringMVCUtil.getResponse()调用到的方法public static HttpServletResponse getResponse() {ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();if (servletRequestAttributes == null) {throw (new NotWebContextException("非Web上下文无法获取Response")).setCode(20101);} else {return servletRequestAttributes.getResponse();}}//RequestContextHolder.getRequestAttributes()调用到的方法public static RequestAttributes getRequestAttributes() {RequestAttributes attributes = requestAttributesHolder.get();if (attributes == null) {attributes = inheritableRequestAttributesHolder.get();}return attributes;}//RequestContextHolder的内部变量类,使用ThreadLocal修饰,是线程内部变量,从此RequestAttributes中获取到的Response为此处请求处理线程的响应private static final ThreadLocal<RequestAttributes> requestAttributesHolder =new NamedThreadLocal<>("Request attributes");private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =new NamedInheritableThreadLocal<>("Request context");//SaHolder.getResponse().addCookie(cookie)方法,向Response设置cookiedefault void addCookie(SaCookie cookie) {this.addHeader("Set-Cookie", cookie.toHeaderValue());}

从源码可以看出处理的功能为:获取cookie相关的配置,创建一个cookie,设置cookie的名称、过期时间、写入的域名等信息;获取此次请求对应的HttpServletResponse,获取HttpServletResponse的流程为:先获取到此次请求的RequestAttributes,使用ThreadLocal来修饰它,被ThreadLocal修饰的为线程内部变量,在线程的生命周期内共享此变量;通过RequestAttributes调用getResponse()方法取到此次请求的HttpServletResponse;通过HttpServletResponse向客户端或者浏览器写入cookie。

5.权限验证流程分析

​ 接口的调用往往需要权限的校验,一般的系统会给用户绑定某种角色,再给此角色分配权限,设置权限码,具有此角色或者权限码才放行请求。sa-token实现权限需要进行自定义扩展,以下为官网给的案例:

/*** 自定义权限验证接口扩展*/
@Component    // 保证此类被SpringBoot扫描,完成Sa-Token的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {/*** 返回一个账号所拥有的权限码集合*/@Overridepublic List<String> getPermissionList(Object loginId, String loginType) {// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限//1.先从redis中取,有则直接返回//2.若是redis中没有,则从数据库中查询,再把结果添加到redis中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;}/*** 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)*/@Overridepublic List<String> getRoleList(Object loginId, String loginType) {// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色List<String> list = new ArrayList<String>();list.add("admin");list.add("super-admin");return list;}}

从此案例中可以看到,扩展类实现了StpInterface接口,重写它里面根据用户id来获取权限码、获取用户角色码的方法,方法里面的实现可以改为:根据用户id先从redis中取,有则直接返回;redis中没有,则从数据库查询,再把结果添加到redis中。

​ 判断用户是否有某个权限码,有此权限码才让访问接下来的流程,一行代码StpUtil.hasPermission(“user.add”)就能实现,看下StpUtil.hasPermission校验权限码的源码⑰:

   public static boolean hasPermission(String permission) {//校验是否有此权限码return stpLogic.hasPermission(permission);}//stpLogic.hasPermission调用的方法public boolean hasPermission(String permission) {//获取用户拥有的权限码集合,判断是否有此权限码return this.hasElement(this.getPermissionList(), permission);}//获取用户权限码集合public List<String> getPermissionList() {try {//传递当前用户id,查询他具备的权限码集合return this.getPermissionList(this.getLoginId());} catch (NotLoginException var2) {return SaFoxUtil.emptyList();}}//根据用户id获取此用户具有的权限码集合public List<String> getPermissionList(Object loginId) {//调用接口SaManager.getStpInterface()的类型为StpInterface,会调用到用户扩展的获取权限码的方法return SaManager.getStpInterface().getPermissionList(loginId, this.loginType);}//判断一个集合中是否包含另一个元素的方法public boolean hasElement(List<String> list, String element) {//hasElement是一个函数,判断是否包含某个元素return (Boolean)SaStrategy.me.hasElement.apply(list, element);}//hasElement函数判断集合中是否包含某个元素public BiFunction<List<String>, String, Boolean> hasElement = (list, element) -> {if (list != null && list.size() != 0) {//list中包含,则返回trueif (list.contains(element)) {return true;} else {//集合中不包含某个元素,迭代判断,若是有*号的权限码,则使用正则表达式判断Iterator var2 = list.iterator();String patt;do {if (!var2.hasNext()) {return false;}patt = (String)var2.next();} while(!SaFoxUtil.vagueMatch(patt, element)); //使用正则表达式判断return true;}} else {return false;}};//SaFoxUtil.vagueMatch的方法,使用正则表达式判断public static boolean vagueMatch(String patt, String str) {if (patt == null && str == null) {return true;} else if (patt != null && str != null) {//当不包括*号,则使用相等判断;包含*号,则使用正则表达式进行判断return patt.indexOf("*") == -1 ? patt.equals(str) : Pattern.matches(patt.replaceAll("\\*", ".*"), str);} else {return false;}}

从源码可以看出处理的功能为:先根据用户id获取此用户拥有的权限码集合,会调用到用户扩展实现StpInterface接口类的对应方法,拿到此用户的权限码集合后,再调用hasElement函数,权限码和待校验的字符串作为参数进行判断;当权限码集合中包含此字符串时,则返回true;权限码集合中不包含此字符串时,遍历权限码集合,若是权限码包含*号,则使用正则表达式进行判断,其它使用相等判断。

6.路由拦截分析

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

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {// 注册 Sa-Token 的拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册路由拦截器,自定义认证规则registry.addInterceptor(new SaInterceptor(handler -> {// 登录校验 -- 拦截所有路由,并排除/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方法,定义拦截规则。SaInterceptor类实现了HandlerInterceptor接口,是sa-token用来配置拦截器的类。配置的每一个路由规则SaRouter支持拦截配置、白名单配置、拦截要求配置,例如:拦截所有接口,对login登陆接口开放,都需要校验用户已经登陆了才进行放行。SaRouter配置的拦截器可以细化到某个接口需要有某种权限才进行放行,也可以通过HandlerInterceptor的addPathPatterns加入拦截规则、excludePathPatterns加白某些接口。看下SaRouter.match具体拦截验证的源码⑱:

   //传递拦截的配置、加白的配置,需要满足的要求public static SaRouterStaff match(String pattern, String excludePattern, SaParamFunction<SaRouterStaff> fun) {//创建一个SaRouterStaff类,调用match匹配方法return (new SaRouterStaff()).match(pattern, excludePattern, fun);}//匹配方法public SaRouterStaff match(String pattern, String excludePattern, SaParamFunction<SaRouterStaff> fun) {//校验是否匹配上,是否放行、检查函数运行结果return this.match(pattern).notMatch(excludePattern).check(fun);}//this.match(pattern)进行匹配的方法public SaRouterStaff match(String... patterns) {//isHit是否命中,初始为trueif (this.isHit) {//校验当前访问的请求地址是否与配置的地址匹配this.isHit = SaRouter.isMatchCurrURI(patterns);}return this;}//notMatch(excludePattern)调用的方法public SaRouterStaff notMatch(String... patterns) {if (this.isHit) {//校验当前访问的请求地址是否与配置的地址匹配,再取反this.isHit = !SaRouter.isMatchCurrURI(patterns);}return this;}//检查,运行函数public SaRouterStaff check(SaFunction fun) {//当match和notMatch都匹配检查后,isHit还是为true,则执行函数if (this.isHit) {fun.run();}return this;}//SaRouter.isMatchCurrURI(patterns)方法,public static boolean isMatchCurrURI(String[] patterns) {//获取当前访问的请求地址是否与配置的地址匹配return isMatch(patterns, SaHolder.getRequest().getRequestPath());}//当前请求地址能否与配置的地址集合匹配public static boolean isMatch(String[] patterns, String path) {if (patterns == null) {return false;} else {String[] var2 = patterns;int var3 = patterns.length;//遍历地址集合for(int var4 = 0; var4 < var3; ++var4) {String pattern = var2[var4];if (isMatch(pattern, path)) {return true;}}return false;}}//当前请求地址能否与配置的地址匹配public static boolean isMatch(String pattern, String path) {return SaManager.getSaTokenContextOrSecond().matchPath(pattern, path);}//匹配路径public boolean matchPath(String pattern, String path) {//使用PathMatcher路径匹配器匹配:当前请求地址能否与配置的地址匹配return SaPathMatcherHolder.getPathMatcher().match(pattern, path);}

从源码可以看出处理的功能为:match方法接收三个参数,第一个是匹配配置、第二个是不匹配配置、第三个是条件要求;从HttpServletRequest获取当前请求的地址path,然后使用PathMatcher路径匹配器匹配是否在拦截的配置里,以及不拦截的配置里,使用isHit字段来记录是否命中,当前面两个校验之后,isHit还是为true,则运行函数判断是否满足,满足了才算校验通过。

7.侦听器分析

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

/*** 自定义侦听器的实现 */
@Component
public class MySaTokenListener implements SaTokenListener {/** 每次登录时触发 */@Overridepublic void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {System.out.println("---------- 自定义侦听器实现 doLogin");}/** 每次注销时触发 */@Overridepublic void doLogout(String loginType, Object loginId, String tokenValue) {System.out.println("---------- 自定义侦听器实现 doLogout");}/** 每次被踢下线时触发 */@Overridepublic void doKickout(String loginType, Object loginId, String tokenValue) {System.out.println("---------- 自定义侦听器实现 doKickout");}/** 每次被顶下线时触发 */@Overridepublic void doReplaced(String loginType, Object loginId, String tokenValue) {System.out.println("---------- 自定义侦听器实现 doReplaced");}/** 每次被封禁时触发 */@Overridepublic void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {System.out.println("---------- 自定义侦听器实现 doDisable");}/** 每次被解封时触发 */@Overridepublic void doUntieDisable(String loginType, Object loginId, String service) {System.out.println("---------- 自定义侦听器实现 doUntieDisable");}/** 每次二级认证时触发 */@Overridepublic void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {System.out.println("---------- 自定义侦听器实现 doOpenSafe");}/** 每次退出二级认证时触发 */@Overridepublic void doCloseSafe(String loginType, String tokenValue, String service) {System.out.println("---------- 自定义侦听器实现 doCloseSafe");}/** 每次创建Session时触发 */@Overridepublic void doCreateSession(String id) {System.out.println("---------- 自定义侦听器实现 doCreateSession");}/** 每次注销Session时触发 */@Overridepublic void doLogoutSession(String id) {System.out.println("---------- 自定义侦听器实现 doLogoutSession");}/** 每次Token续期时触发 */@Overridepublic void doRenewTimeout(String tokenValue, Object loginId, long timeout) {System.out.println("---------- 自定义侦听器实现 doRenewTimeout");}
}

从此案例中可以看到:自定义侦听器需要实现SaTokenListener接口,然后重写里面的方法,当对应的方法有调用时会触发监听方法,使用观察者设计模式实现。实现此侦听器是基于SaTokenEventCenter事件处理中心类实现的,看下SaTokenEventCenter类的相关源码⑲:

//事件处理中心类
public class SaTokenEventCenter {//记录所有的侦听器类;实现了SaTokenListener接口,并使用@Component修饰的自定义侦听器,在程序启动的时候,会被spring boot扫描加载进来,此时listenerList里面会存放这些自定义侦听器private static List<SaTokenListener> listenerList = new ArrayList();//登陆方法的侦听方法public static void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {//使用迭代器遍历所有的侦听器类Iterator var4 = listenerList.iterator();while(var4.hasNext()) {SaTokenListener listener = (SaTokenListener)var4.next();//调用侦听器类的登陆方法,执行自定义的逻辑处理代码,此时会调用到SaTokenListener真正的实现类方法listener.doLogin(loginType, loginId, tokenValue, loginModel);}}//登出方法的侦听方法public static void doLogout(String loginType, Object loginId, String tokenValue) {Iterator var3 = listenerList.iterator();while(var3.hasNext()) {SaTokenListener listener = (SaTokenListener)var3.next();listener.doLogout(loginType, loginId, tokenValue);}}
}

从源码可以看出处理的功能为:SaTokenEventCenter类是事件处理中心,listenerList是一个类型为SaTokenListener接口类的集合。自定义的侦听器类,实现了SaTokenListener接口,并使用@Component修饰,在程序启动的时候,会被spring boot扫描加载进来,此时listenerList里面会存放这些自定义侦听器。SaTokenEventCenter类定义了很多需要监听的方法,当调用某个监听方法时,会向所有的侦听器传递此消息,调用侦听器对应的方法,使用了java的观察者模式。来分析下doLogin方法的调用来源,在用户登录源码④的SaTokenEventCenter.doLogin中进行的调用,当登陆完成后,会调用事件处理中心的方法,事件处理中心再遍历所有注册的侦听器,去执行侦听器对应的方法。

sa-token部分源码分析相关推荐

  1. 源码分析 - Spring Security OAuth2 生成 token 的执行流程

    说明 本文内容全部基于 Spring Security OAuth2(2.3.5.RELEASE). OAuth2.0 有四种授权模式, 本文会以 密码模式 来举例讲解源码. 阅读前, 需要对 OAu ...

  2. Kube Controller Manager 源码分析

    Kube Controller Manager 源码分析 Controller Manager 在k8s 集群中扮演着中心管理的角色,它负责Deployment, StatefulSet, Repli ...

  3. Hhadoop-2.7.0中HDFS写文件源码分析(二):客户端实现(1)

    一.综述 HDFS写文件是整个Hadoop中最为复杂的流程之一,它涉及到HDFS中NameNode.DataNode.DFSClient等众多角色的分工与合作. 首先上一段代码,客户端是如何写文件的: ...

  4. EOS智能合约:system系统合约源码分析

    链客,专为开发者而生,有问必答! 此文章来自区块链技术社区,未经允许拒绝转载. eosio.system 概览 笔者使用的IDE是VScode,首先来看eosio.system的源码结构.如下图所示. ...

  5. Nginx源码分析:master/worker工作流程概述

    nginx源码分析 nginx-1.11.1 参考书籍<深入理解nginx模块开发与架构解析> Nginx的master与worker工作模式 在生成环境中的Nginx启动模式基本都是以m ...

  6. tornado源码分析

    tornado源码分析 本源码为tornado1.0版本 源码附带例子helloworld import tornado.httpserver import tornado.ioloop import ...

  7. Spring Security 源码分析:Spring Security 授权过程

    Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring I ...

  8. kubeadm源码分析(内含kubernetes离线包,三步安装)

    k8s离线安装包 三步安装,简单到难以置信 kubeadm源码分析 说句实在话,kubeadm的代码写的真心一般,质量不是很高. 几个关键点来先说一下kubeadm干的几个核心的事: kubeadm ...

  9. Framework 源码解析知识梳理(5) startService 源码分析

    一.前言 最近在看关于插件化的知识,遇到了如何实现Service插件化的问题,因此,先学习一下Service内部的实现原理,这里面会涉及到应用进程和ActivityManagerService的通信, ...

  10. Handler机制的源码分析

    2019独角兽企业重金招聘Python工程师标准>>> Handler,MessageQueue,Looper的关系 Looper的作用是在线程中处理消息的 MessageQueue ...

最新文章

  1. 15分钟学会MyEclipse导出jar文件再装换成exe可执行文件
  2. 公司的费用报销系统【为什么不好用】?做业务系统软件的可以参考一下
  3. Python的深copy和浅copy
  4. scss支持的嵌套css规则
  5. linux中echo命令不输出换行,shell脚本echo输出不换行功能增强实例
  6. CSS Margin(外边距)
  7. (转)淘淘商城系列——SSM框架整合之Dao层整合
  8. 计算机网络的一大发展趋势是多维化,对口高考计算机网络概述复习.ppt
  9. 一文看懂搜狗招股书:90次提到AI,王小川持股5%,净利3.7亿
  10. word2010分页设置页眉
  11. js 京东关闭广告 pink
  12. (轉貼) 完全用Linux工作,摈弃Windows (OS) (Linux)
  13. OWI-PX Deq Credit: send blkd等待事件
  14. matlab cftool光滑曲线导出为什么就不光滑了_不会吧,还有人不知道MATLAB这8个小技巧?...
  15. HDFS java接口——实现目录增删文件读写
  16. DUET and updated DUET(2016 and 2019)
  17. python进行JB正态性检验
  18. 【深度】韦东山:一文看看尽linux对中断处理的前世今生
  19. Linux系统下工具软件的安装
  20. idea springboot启动报SLF4J:Failed to load class “org.slf4j.impl.StaticLoggerBinder

热门文章

  1. critic法计算_对于强化学习算法中的AC算法(Actor-Critic算法) 的一些理解
  2. 知名游戏公司高薪诚聘 Cocos 人才|第二弹
  3. win10图片打印提示出现了一个内部错误
  4. 利用电脑自带Xbox实现录制屏幕
  5. 判断两个时间是不是同一天
  6. R语言绘图—甜甜圈图
  7. 使用ascii码对字符串进行加密解密
  8. SHEIN内衣品牌如何做品牌营销?WotoHub更多可能
  9. star ccm java api_STAR-CCM+二次开发——User Code
  10. java 把jsp 保存成图片_将jsp页面转化为图片或pdf(一)(qq:1324981084)