> 文章列表 > Sa-Token源码简单阅读

Sa-Token源码简单阅读

Sa-Token源码简单阅读

一.权限登录模块包括几个基本子模块:

1.登录。

实现方式大致为:先检验用户名密码是否正确,如正确则在缓存中存入用户信息(一般必须要有用户标识和访问token,或再加一些附加信息如用户的角色权限),再返回访问token给客户端。

2.过滤器,主要通过客户端访问时带的token检验是否有访问url权限。

实现方式大致为:通过客户端访问的url匹配资源类型。一些资源可直接放行,一些资源需要校验登录,一些资源需要校验角色权限。

3.获取角色权限的方式。

因为取权限角色的方式不同,一般权限框架会提供一层抽象(接口),需要开发者实现。取角色权限未必需要在过滤器中调用,可以在任何需要的时候调用。

根据缓存方式不同可以做一层抽象(接口)。

Sa-Token框架的核心类是cn.dev33.satoken.stp.StpLogic,该类实现了大部分简单逻辑操做功能。

 二.登录。

1.Sa-token默认登录操作cn.dev33.satoken.stp.StpLogic#login(Object, SaLoginModel),第一个参数为自定义用户的凭证

cn.dev33.satoken.stp.StpLogic#login==>cn.dev33.satoken.stp.StpLogic#createLoginSession

/*** 创建指定账号id的登录会话* @param id 登录id,建议的类型:(long | int | String)* @param loginModel 此次登录的参数Model * @return 返回会话令牌 */public String createLoginSession(Object id, SaLoginModel loginModel) {// ------ 前置检查SaTokenException.throwByNull(id, "账号id不能为空");// ------ 1、初始化 loginModel SaTokenConfig config = getConfig();loginModel.build(config);// ------ 2、分配一个可用的 Token  String tokenValue = distUsableToken(id, loginModel);// ------ 3. 获取 User-Session , 续期 SaSession session = getSessionByLoginId(id, true);session.updateMinTimeout(loginModel.getTimeout());// 在 User-Session 上记录token签名 session.addTokenSign(tokenValue, loginModel.getDeviceOrDefault());// ------ 4. 持久化其它数据 // token -> id 映射关系  saveTokenToIdMapping(tokenValue, id, loginModel.getTimeout());// 写入 [token-last-activity] setLastActivityToNow(tokenValue); // $$ 发布事件:账号xxx 登录成功 SaTokenEventCenter.doLogin(loginType, id, tokenValue, loginModel);// 检查此账号会话数量是否超出最大值 if(config.getMaxLoginCount() != -1) {logoutByMaxLoginCount(id, session, null, config.getMaxLoginCount());}// 返回Token return tokenValue;}

 上面第2步生成token后,第3步底层在缓存中添加token和session对象的映射。

 上面第4步底层又存入了token和用户凭证的映射关系

 第三方框架snowy在登录时又在缓存中存了用户权限角色基本信息,方便单点登录时取权限角色信息(存在下面session的dataMap中)

 底层先在缓存创建一个新的session在缓存中,在更新了一次数据。调用了2次缓存操作

三.过滤器。

过滤器配置源码

package com.wisdomcity.laian.river.config;import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaResponse;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.interceptor.SaAnnotationInterceptor;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.strategy.SaStrategy;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.zy.genius.geomatics.basic.ResultCode;
import com.wisdomcity.laian.river.utils.GlobalExceptionUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import vip.xiaonuo.auth.core.enums.SaClientTypeEnum;
import vip.xiaonuo.auth.core.util.StpLoginUserUtil;
import vip.xiaonuo.common.pojo.CommonResult;import java.util.List;@Slf4j
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {/*** 注册Sa-Token的注解拦截器,打开注解式鉴权功能* <p>* 注解的方式有以下几中,注解既可以加在接口方法上,也可加在Controller类上:* 1.@SaCheckLogin: 登录认证 —— 只有登录之后才能进入该方法(常用)* 2.@SaCheckRole("admin"): 角色认证 —— 必须具有指定角色标识才能进入该方法(常用)* 3.@SaCheckPermission("user:add"): 权限认证 —— 必须具有指定权限才能进入该方法(常用)* 4.@SaCheckSafe: 二级认证校验 —— 必须二级认证之后才能进入该方法* 5.@SaCheckBasic: HttpBasic认证 —— 只有通过 Basic 认证后才能进入该方法* <p>* 在Controller中创建一个接口,默认不需要登录也不需要任何权限都可以访问的,只有加了上述注解才会校验**/@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册注解拦截器,并排除不需要注解鉴权的接口地址 (与登录拦截器无关,只是说明哪些接口不需要被拦截器拦截,此处都拦截)registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");}@Bean("stpLogic")public StpLogic getStpLogic() {// 重写Sa-Token的StpLogic,默认客户端类型为Breturn new StpLogic(SaClientTypeEnum.B.getValue());}@Bean("stpClientLogic")public StpLogic getStpClientLogic() {// 重写Sa-Token的StpLogic,默认客户端类型为Creturn new StpLogic(SaClientTypeEnum.C.getValue());}@Beanpublic void rewriteSaStrategy() {// 重写Sa-Token的注解处理器,增加注解合并功能SaStrategy.me.getAnnotation = AnnotatedElementUtils::getMergedAnnotation;}/*** 权限认证接口实现类,集成权限认证功能**/@Componentpublic static class StpInterfaceImpl implements StpInterface {/*** 返回一个账号所拥有的权限码集合*/@Overridepublic List<String> getPermissionList(Object loginId, String loginType) {return StpLoginUserUtil.getLoginUser().getPermissionCodeList();}/*** 返回一个账号所拥有的角色标识集合*/@Overridepublic List<String> getRoleList(Object loginId, String loginType) {return StpLoginUserUtil.getLoginUser().getRoleCodeList();}}/*** 无需登录的接口地址集合*/private static final String[] NO_LOGIN_PATH_ARR = {/* 主入口 */"/",/* 静态资源 */"/static/fonts/**","/static/icons/**","/favicon.ico","/doc.html","/webjars/**","/swagger-resources/**","/swagger-ui/**","/v2/api-docs","/v2/api-docs-ext","/v3/api-docs","/v3/api-docs-ext","/configuration/ui","/configuration/security","/ureport/**","/druid/**",/* 文件预览 */"/file/online/view",};/*** 仅超管使用的接口地址集合*/private static final String[] SUPER_PERMISSION_PATH_ARR = {"/auth/session/**","/auth/third/page","/client/user/**",};/*** 注册 [Sa-Token 全局过滤器]*/@Beanpublic SaServletFilter getSaServletFilter() {return new SaServletFilter()// 指定拦截路由.addInclude("/**")// 设置鉴权的接口.setAuth(r -> {SaRouter.match("/**").notMatch(CollectionUtil.newArrayList(NO_LOGIN_PATH_ARR)).check(r1 -> StpUtil.checkLogin());SaRouter.match(CollectionUtil.newArrayList(SUPER_PERMISSION_PATH_ARR)).notMatch(CollectionUtil.newArrayList(NO_LOGIN_PATH_ARR)).check(r1 -> StpUtil.checkRole("superAdmin"));})// 前置函数:在每次认证函数之前执行.setBeforeAuth(obj -> {// ---------- 设置跨域响应头 ----------SaHolder.getResponse()// 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以// .setHeader("X-Frame-Options", "SAMEORIGIN")// 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面.setHeader("X-XSS-Protection", "1; mode=block")// 禁用浏览器内容嗅探.setHeader("X-Content-Type-Options", "nosniff")// 允许指定域访问跨域资源.setHeader("Access-Control-Allow-Origin", "*")// 允许所有请求方式.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")// 有效时间.setHeader("Access-Control-Max-Age", "3600")// 允许的header参数.setHeader("Access-Control-Allow-Headers", "*");// 如果是预检请求,则立即返回到前端SaRouter.match(SaHttpMethod.OPTIONS)// OPTIONS预检请求,不做处理.free(r -> {}).back();})// 异常处理.setError(e -> {// 由于过滤器中抛出的异常不进入全局异常处理,所以必须提供[异常处理函数]来处理[认证函数]里抛出的异常// 在[异常处理函数]里的返回值,将作为字符串输出到前端,此处统一转为JSON输出前端SaResponse saResponse = SaHolder.getResponse();saResponse.setHeader(Header.CONTENT_TYPE.getValue(), ContentType.JSON + ";charset=" + CharsetUtil.UTF_8);CommonResult<String> commonResult = GlobalExceptionUtil.getCommonResult((Exception) e);saResponse.setStatus(commonResult.getCode() < ResultCode.HttpStatusCodeMax? commonResult.getCode(): HttpStatus.FORBIDDEN.value());return commonResult;});}
}

cn.dev33.satoken.filter.SaServletFilter#doFilter

在过滤中调用上面beforeAuth,Auth两个策略

@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {try {// 执行全局过滤器 SaRouter.match(includeList).notMatch(excludeList).check(r -> {beforeAuth.run(null);auth.run(null);});} catch (StopMatchException e) {} catch (Throwable e) {// 1. 获取异常处理策略结果 String result = (e instanceof BackResultException) ? e.getMessage() : String.valueOf(error.run(e));// 2. 写入输出流 if(response.getContentType() == null) {response.setContentType("text/plain; charset=utf-8"); }response.getWriter().print(result);return;}// 执行 chain.doFilter(request, response);}

在auth策略中调用StpUtil.checkLogin()检验账号是否登录,底层就是拿前端的token去缓存查询登录凭证。

四.获取权限

1.获取权限在第三方框架snowy中比较简单,因为缓存中已经存有token和session的映射(session中存有用户信息),直接通过token就能在缓存中取到了。

vip.xiaonuo.auth.core.util.StpLoginUserUtil#getLoginUser中调用StpUtil.getTokenSession()会在缓存中取session信息


2.开启权限注解后(配置了注解拦截器),调用com.wisdomcity.laian.river.config.SaTokenConfig.StpInterfaceImpl#getPermissionList的时机会在方法或类上添加SaCheckPermission.class注解后调用。

拦截器cn.dev33.satoken.interceptor.SaAnnotationInterceptor#preHandle 

cn.dev33.satoken.strategy.SaStrategy#checkElementAnnotation

 

调用链