源码跟踪
这里使用官方的springboot-demo来测试跟踪
源码跟踪 - 登录
登录分为存有session时的登录和没有session时的登录
- 没有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里,后面这个也有用的
- 然后现在是带有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就完事了
这只是一个简单的针对一种情况的源码跟踪,其他分支代码以此类推