SaToken登录鉴权示例的源码跟踪

源码跟踪

这里使用官方的springboot-demo来测试跟踪

源码跟踪 - 登录

登录分为存有session时的登录和没有session时的登录

  1. 没有session时的登录:

StpLogic:

/**
	 * 会话登录,并指定所有登录参数Model 
	 * @param id 登录id,建议的类型:(long | int | String)
	 * @param loginModel 此次登录的参数Model 
	 */
	public void login(Object id, SaLoginModel loginModel) {
		// 1、创建会话 
		String token = createLoginSession(id, loginModel);

		// 2、在当前客户端注入Token 
		setTokenValue(token, loginModel.getCookieTimeout());
	}

首先会去创建会话session

/**
	 * 创建指定账号id的登录会话
	 * @param id 登录id,建议的类型:(long | int | String)
	 * @param loginModel 此次登录的参数Model 
	 * @return 返回会话令牌 
	 */
	public String createLoginSession(Object id, SaLoginModel loginModel) {
		
		SaTokenException.throwByNull(id, "账号id不能为空");
		
		// ------ 0、前置检查:如果此账号已被封禁. 
		if(isDisable(id)) {
			throw new DisableLoginException(loginType, id, getDisableTime(id));
		}
		
		// ------ 1、初始化 loginModel 
		SaTokenConfig config = getConfig();
		loginModel.build(config);
		
		// ------ 2、生成一个token  
		String tokenValue = null;
		// --- 如果允许并发登录 
		if(config.getIsConcurrent()) {
			// 如果配置为共享token, 则尝试从Session签名记录里取出token 
			if(getConfigOfIsShare()) {
				tokenValue = getTokenValueByLoginId(id, loginModel.getDeviceOrDefault()); 这里
			}
		} else {
			// --- 如果不允许并发登录,则将这个账号的历史登录标记为:被顶下线 
			replaced(id, loginModel.getDevice());
		}
		。。。。。。
	}

下面先省略,先看进入到这个方法

public String getTokenValueByLoginId(Object loginId, String device) {
		List<String> tokenValueList = getTokenValueListByLoginId(loginId, device); 这里
		return tokenValueList.size() == 0 ? null : tokenValueList.get(tokenValueList.size() - 1);
	}
public List<String> getTokenValueListByLoginId(Object loginId, String device) {
		// 如果session为null的话直接返回空集合  
		SaSession session = getSessionByLoginId(loginId, false);
		if(session == null) {
			return Collections.emptyList();
		}
		// 遍历解析 
		List<TokenSign> tokenSignList = session.getTokenSignList();
		List<String> tokenValueList = new ArrayList<>();
		for (TokenSign tokenSign : tokenSignList) {
			if(device == null || tokenSign.getDevice().equals(device)) {
				tokenValueList.add(tokenSign.getValue());
			}
		}
		return tokenValueList;
	}
public SaSession getSessionByLoginId(Object loginId, boolean isCreate) {
		return getSessionBySessionId(splicingKeySession(loginId), isCreate);
	}
/** 
	 * 获取指定账号id指定设备端的tokenValue 集合 
	 * @param loginId 账号id 
	 * @param device 设备标识,填null代表不限设备 
	 * @return 此loginId的所有相关token 
 	 */
	public List<String> getTokenValueListByLoginId(Object loginId, String device) {
		// 如果session为null的话直接返回空集合  
		SaSession session = getSessionByLoginId(loginId, false);
// 这时候发现这个session确实是空的
		if(session == null) {
			return Collections.emptyList();
		}
		// 遍历解析 
		List<TokenSign> tokenSignList = session.getTokenSignList();
		List<String> tokenValueList = new ArrayList<>();
		for (TokenSign tokenSign : tokenSignList) {
			if(device == null || tokenSign.getDevice().equals(device)) {
				tokenValueList.add(tokenSign.getValue());
			}
		}
		return tokenValueList;
	}

直接返回到最开始那个省略了代码的方法:

public String createLoginSession(Object id, SaLoginModel loginModel) {
		
		SaTokenException.throwByNull(id, "账号id不能为空");
		
		// ------ 0、前置检查:如果此账号已被封禁. 
		if(isDisable(id)) {
			throw new DisableLoginException(loginType, id, getDisableTime(id));
		}
		
		// ------ 1、初始化 loginModel 
		SaTokenConfig config = getConfig();
		loginModel.build(config);
		
		// ------ 2、生成一个token  
		String tokenValue = null;
		// --- 如果允许并发登录 
		if(config.getIsConcurrent()) {
			// 如果配置为共享token, 则尝试从Session签名记录里取出token 
			if(getConfigOfIsShare()) {
				tokenValue = getTokenValueByLoginId(id, loginModel.getDeviceOrDefault());
			}
		} else {
			// --- 如果不允许并发登录,则将这个账号的历史登录标记为:被顶下线 
			replaced(id, loginModel.getDevice());
		}
		// 如果至此,仍未成功创建tokenValue, 则开始生成一个 
    // 这个时候确实未生成token
		if(tokenValue == null) {
			if(SaFoxUtil.isEmpty(loginModel.getToken())) {
				tokenValue = createTokenValue(id, loginModel.getDeviceOrDefault(), loginModel.getTimeout(), loginModel.getExtraData());
			} else {
				tokenValue = loginModel.getToken();
			}
		}
		
		// ------ 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 登录成功 
		SaManager.getSaTokenListener().doLogin(loginType, id, loginModel);
		
		// 返回Token 
		return tokenValue;
	}

createTokenValue 会去根据策略创建出一个token返回

接着就是一系列设置,其中也包括session设置!

然后返回,回到最开始的方法:

/**
	 * 会话登录,并指定所有登录参数Model 
	 * @param id 登录id,建议的类型:(long | int | String)
	 * @param loginModel 此次登录的参数Model 
	 */
	public void login(Object id, SaLoginModel loginModel) {
		// 1、创建会话 
		String token = createLoginSession(id, loginModel);

		// 2、在当前客户端注入Token 
		setTokenValue(token, loginModel.getCookieTimeout());
	}

第二步是在Cookie里设置token了

/**
 	 * 在当前会话写入当前TokenValue 
 	 * @param tokenValue token值 
 	 * @param cookieTimeout Cookie存活时间(秒)
 	 */
	public void setTokenValue(String tokenValue, int cookieTimeout){
		
		if(SaFoxUtil.isEmpty(tokenValue)) {
			return;
		}
		
		// 1. 将token保存到[存储器]里  
		setTokenValueToStorage(tokenValue);
		
		// 2. 将 Token 保存到 [Cookie] 里 
		if (getConfig().getIsReadCookie()) {
			setTokenValueToCookie(tokenValue, cookieTimeout);
		}
	}

还有一步是讲tokenvalue设置到这个storage里,后面这个也有用的


  1. 然后现在是带有token了,我们再次去登录的时候,他就会从原有session里拿出原来那个token再次返回,并不需要重新去生成token

也就是在public String createLoginSession(Object id, SaLoginModel loginModel) 这个方法中的

tokenValue = getTokenValueByLoginId(id, loginModel.getDeviceOrDefault()); 就会获取到token了。

源码跟踪 - 验证登录

StpUtil.*isLogin*()

其实就是验证有没有token啦还有有效性之类的

/** 
	 * 获取当前会话账号id, 如果未登录,则返回null 
	 * @return 账号id 
	 */
	public Object getLoginIdDefaultNull() {
		// 如果正在[临时身份切换]
		if(isSwitch()) {
			return getSwitchLoginId();
		}
		// 如果连token都是空的,则直接返回 
		String tokenValue = getTokenValue();
 		if(tokenValue == null) {
 			return null;
 		}
 		// loginId为null或者在异常项里面,均视为未登录, 返回null 
 		Object loginId = getLoginIdNotHandle(tokenValue);
 		if(isValidLoginId(loginId) == false) {
 			return null;
 		}
 		// 如果已经[临时过期] 
 		if(getTokenActivityTimeoutByToken(tokenValue) == SaTokenDao.NOT_VALUE_EXPIRE) {
 			return null;
 		}
 		// 执行到此,证明loginId已经是个正常的账号id了 
 		return loginId;
 	}

我们先看获取token的

/**
	 * 获取当前TokenValue
	 * @return 当前tokenValue
	 */
	public String getTokenValue(){
		// 1. 获取
		String tokenValue = getTokenValueNotCut();
		
		// 2. 如果打开了前缀模式,则裁剪掉 
		String tokenPrefix = getConfig().getTokenPrefix();
		if(SaFoxUtil.isEmpty(tokenPrefix) == false) {
			// 如果token并没有按照指定的前缀开头,则视为未提供token 
			if(SaFoxUtil.isEmpty(tokenValue) || tokenValue.startsWith(tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT) == false) {
				tokenValue = null;
			} else {
				// 则裁剪掉前缀 
				tokenValue = tokenValue.substring(tokenPrefix.length() + SaTokenConsts.TOKEN_CONNECTOR_CHAT.length());
			}
		}
		
		// 3. 返回 
		return tokenValue;
	}
/**
	 * 获取当前TokenValue (不裁剪前缀)
	 * @return / 
	 */
	public String getTokenValueNotCut(){
		// 0. 获取相应对象 
		SaStorage storage = SaHolder.getStorage();
		SaRequest request = SaHolder.getRequest();
		SaTokenConfig config = getConfig();
		String keyTokenName = getTokenName();
		String tokenValue = null;
		
		// 1. 尝试从Storage里读取 
		if(storage.get(splicingKeyJustCreatedSave()) != null) {
			tokenValue = String.valueOf(storage.get(splicingKeyJustCreatedSave()));
		}
		// 2. 尝试从请求体里面读取 
		if(tokenValue == null && config.getIsReadBody()){
			tokenValue = request.getParam(keyTokenName);
		}
		// 3. 尝试从header里读取 
		if(tokenValue == null && config.getIsReadHead()){
			tokenValue = request.getHeader(keyTokenName);
		}
		// 4. 尝试从cookie里读取 
		if(tokenValue == null && config.getIsReadCookie()){
			tokenValue = request.getCookieValue(keyTokenName);
		}
		
		// 5. 返回 
		return tokenValue;
	}

这里有多个if条件语句,

第一个是针对于你一个请求里面同时登录了(会把token保存在一个justCreateSave的地方,通过splicingKeyJustCreatedSave可以获取到)

第二个是请求体,第三个是header,第四个是Cookie,都是看你有没有satoken这几个读取配置是否打开并且有没有值去拿的。

返回tokenvalue为空则直接就是false了,没有登录;

如果返回不为空则判断有没有过期之类的。

源码跟踪 - 验证权限(注解式鉴权)

还是基于springboot,其实框架是通过注入一个拦截器达到鉴权的效果(大多框架都是这样)

package cn.dev33.satoken.interceptor;

import java.lang.reflect.Method;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import cn.dev33.satoken.strategy.SaStrategy;

/**
 * 注解式鉴权 - 拦截器
 * 
 * @author kong
 */
public class SaAnnotationInterceptor implements HandlerInterceptor {

	/**
	 * 构建: 注解式鉴权 - 拦截器
	 */
	public SaAnnotationInterceptor() {
	}

	/**
	 * 每次请求之前触发的方法
	 */
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		// 获取处理method
		if (handler instanceof HandlerMethod == false) {
			return true;
		}
		Method method = ((HandlerMethod) handler).getMethod();

		// 进行验证
		SaStrategy.me.checkMethodAnnotation.accept(method);
		
		// 通过验证
		return true;
	}

}

基本思路:获取到改controller方法,获取其父类和自己的相关鉴权注解,分别进行验证。

/**
	 * 对一个 [Method] 对象进行注解校验 (注解鉴权内部实现) 
	 * <p> 参数 [Method句柄] 
	 */
	public Consumer<Method> checkMethodAnnotation = (method) -> {

		// 先校验 Method 所属 Class 上的注解 
		me.checkElementAnnotation.accept(method.getDeclaringClass());

		// 再校验 Method 上的注解  
		me.checkElementAnnotation.accept(method);
	};
/**
	 * 对一个 [元素] 对象进行注解校验 (注解鉴权内部实现) 
	 * <p> 参数 [element元素] 
	 */
	public Consumer<AnnotatedElement> checkElementAnnotation = (element) -> {
		// 为了兼容旧版本 
		SaManager.getSaTokenAction().validateAnnotation(element);
	};
/**
	 * 从指定元素校验注解 
	 * @param target see note 
	 */
	public void validateAnnotation(AnnotatedElement target) {
		
		// 校验 @SaCheckLogin 注解 
		SaCheckLogin checkLogin = (SaCheckLogin) SaStrategy.me.getAnnotation.apply(target, SaCheckLogin.class);
		if(checkLogin != null) {
			SaManager.getStpLogic(checkLogin.type()).checkByAnnotation(checkLogin);
		}
		
		// 校验 @SaCheckRole 注解 
		SaCheckRole checkRole = (SaCheckRole) SaStrategy.me.getAnnotation.apply(target, SaCheckRole.class);
		if(checkRole != null) {
			SaManager.getStpLogic(checkRole.type()).checkByAnnotation(checkRole);
		}
		
		// 校验 @SaCheckPermission 注解
		SaCheckPermission checkPermission = (SaCheckPermission) SaStrategy.me.getAnnotation.apply(target, SaCheckPermission.class);
		if(checkPermission != null) {
			SaManager.getStpLogic(checkPermission.type()).checkByAnnotation(checkPermission);
		}

		// 校验 @SaCheckSafe 注解
		SaCheckSafe checkSafe = (SaCheckSafe) SaStrategy.me.getAnnotation.apply(target, SaCheckSafe.class);
		if(checkSafe != null) {
			SaManager.getStpLogic(checkSafe.type()).checkByAnnotation(checkSafe);
		}
		
		// 校验 @SaCheckBasic 注解
		SaCheckBasic checkBasic = (SaCheckBasic) SaStrategy.me.getAnnotation.apply(target, SaCheckBasic.class);
		if(checkBasic != null) {
			SaBasicUtil.check(checkBasic.realm(), checkBasic.account());
		}
		
	}

这里例子是设置permission鉴权的

/**
	 * 根据注解(@SaCheckPermission)鉴权
	 * @param at 注解对象 
	 */
	public void checkByAnnotation(SaCheckPermission at) {
		String[] permissionArray = at.value();
		try {
			if(at.mode() == SaMode.AND) {
				this.checkPermissionAnd(permissionArray);	
			} else {
				this.checkPermissionOr(permissionArray);	
			}
		} catch (NotPermissionException e) {
			// 权限认证未通过,再开始角色认证 
			if(at.orRole().length > 0) {
				for (String role : at.orRole()) {
					String[] rArr = SaFoxUtil.convertStringToArray(role);
					// 某一项role认证通过,则可以提前退出了,代表通过 
					if(hasRoleAnd(rArr)) {
						return;
					}
				}
			}
			throw e;
		}
	}
/** 
 	 * 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过] 
 	 * @param permissionArray 权限码数组
 	 */
 	public void checkPermissionAnd(String... permissionArray){
 		Object loginId = getLoginId();
 		List<String> permissionList = getPermissionList(loginId);
 		for (String permission : permissionArray) {
 			if(!hasElement(permissionList, permission)) {
 				throw new NotPermissionException(permission, this.loginType);	
 			}
 		}
 	}
/**
	 * 获取:指定账号的权限码集合 
	 * @param loginId 指定账号id
	 * @return / 
	 */
	public List<String> getPermissionList(Object loginId) {
		return SaManager.getStpInterface().getPermissionList(loginId, loginType);
	}

这里SaManager.*getStpInterface*()

是获取到容器里我们配置的继承StpInterface 的类,如果没有则返回默认的实现类

例如我们这里:

package com.pj.satoken;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Component;

import cn.dev33.satoken.stp.StpInterface;

/**
 * 自定义权限验证接口扩展 
 */
@Component	// 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 
public class StpInterfaceImpl implements StpInterface {

	/**
	 * 返回一个账号所拥有的权限码集合 
	 */
	@Override
	public List<String> getPermissionList(Object loginId, String loginType) {
		// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限
		List<String> list = new ArrayList<String>();	
		list.add("101");
		list.add("user-add");
		list.add("user-delete");
		list.add("user-update");
		list.add("user-get");
		list.add("article-get");
		return list;
	}

	/**
	 * 返回一个账号所拥有的角色标识集合 
	 */
	@Override
	public List<String> getRoleList(Object loginId, String loginType) {
		// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色
		List<String> list = new ArrayList<String>();	
		list.add("admin");
		list.add("super-admin");
		return list;
	}

}

获取到权限列表后就回到 checkPermissionAnd

后面就验证当前权限是否在权限列表中,不是则抛出异常,是则一步步通过返回,最终拦截器返回true就完事了

这只是一个简单的针对一种情况的源码跟踪,其他分支代码以此类推

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值