Sa-Token源码简单阅读

文章详细介绍了Sa-Token框架在权限登录模块中的实现,包括登录、过滤器配置以及权限获取的流程。登录过程涉及用户凭证校验、token生成和存储。过滤器通过客户端token验证访问权限,同时配置了注解拦截器以支持权限注解。权限获取则从缓存中直接读取。整个框架提供了灵活的角色和权限管理机制。
摘要由CSDN通过智能技术生成

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

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中创建一个接口,默认不需要登录也不需要任何权限都可以访问的,只有加了上述注解才会校验
     **/
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册注解拦截器,并排除不需要注解鉴权的接口地址 (与登录拦截器无关,只是说明哪些接口不需要被拦截器拦截,此处都拦截)
        registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");
    }

    @Bean("stpLogic")
    public StpLogic getStpLogic() {
        // 重写Sa-Token的StpLogic,默认客户端类型为B
        return new StpLogic(SaClientTypeEnum.B.getValue());
    }

    @Bean("stpClientLogic")
    public StpLogic getStpClientLogic() {
        // 重写Sa-Token的StpLogic,默认客户端类型为C
        return new StpLogic(SaClientTypeEnum.C.getValue());
    }

    @Bean
    public void rewriteSaStrategy() {
        // 重写Sa-Token的注解处理器,增加注解合并功能
        SaStrategy.me.getAnnotation = AnnotatedElementUtils::getMergedAnnotation;
    }

    /**
     * 权限认证接口实现类,集成权限认证功能
     **/
    @Component
    public static class StpInterfaceImpl implements StpInterface {

        /**
         * 返回一个账号所拥有的权限码集合
         */
        @Override
        public List<String> getPermissionList(Object loginId, String loginType) {
            return StpLoginUserUtil.getLoginUser().getPermissionCodeList();
        }

        /**
         * 返回一个账号所拥有的角色标识集合
         */
        @Override
        public 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 全局过滤器]
     */
    @Bean
    public 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两个策略

@Override
	public 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

 

调用链

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
sa-token可以通过以下配置来校验token: 1. 配置token名称:在yml配置文件中,可以通过设置`sa-token.token-name`来指定token的名称,例如`sa-token.token-name: X-Token`\[1\]。 2. 配置token有效期:可以通过设置`sa-token.timeout`来指定token的有效期,单位为秒,默认为30天,可以设置为-1代表永不过期,例如`sa-token.timeout: 2592000`\[1\]。 3. 配置token临时有效期:可以通过设置`sa-token.activity-timeout`来指定token的临时有效期,即在指定时间内无操作就视为token过期,单位为秒,默认为-1,表示不设置临时有效期,例如`sa-token.activity-timeout: -1`\[1\]。 4. 配置是否允许同一账号并发登录:可以通过设置`sa-token.is-concurrent`来指定是否允许同一账号并发登录,为true时允许一起登录,为false时新登录会挤掉旧登录,例如`sa-token.is-concurrent: true`\[1\]。 5. 配置是否共用一个token:可以通过设置`sa-token.is-share`来指定在多人登录同一账号时,是否共用一个token,为true时所有登录共用一个token,为false时每次登录会新建一个token,例如`sa-token.is-share: true`\[1\]。 6. 配置是否输出操作日志:可以通过设置`sa-token.is-log`来指定是否输出操作日志,为true时输出操作日志,为false时不输出操作日志,例如`sa-token.is-log: false`\[1\]。 7. 配置是否使用cookie保存token:可以通过设置`sa-token.is-read-cookie`来指定是否使用cookie保存token,为true时使用cookie保存token,为false时不使用cookie保存token,例如`sa-token.is-read-cookie: false`\[1\]。 8. 配置是否使用head保存token:可以通过设置`sa-token.is-read-head`来指定是否使用head保存token,为true时使用head保存token,为false时不使用head保存token,例如`sa-token.is-read-head: true`\[1\]。 通过以上配置,sa-token可以根据配置的规则来校验token的有效性。 #### 引用[.reference_title] - *1* [最简单的权限验证实现——使用Sa-Token进行权限验证](https://blog.csdn.net/lp840312696/article/details/127072424)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [探索 Sa-Token (一) SpringBoot 集成 Sa-Token](https://blog.csdn.net/weixin_38982591/article/details/126764928)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值