新增 maxLoginCount 配置,指定同一账号可同时在线的最大数量

This commit is contained in:
click33
2022-04-24 19:19:20 +08:00
parent cfc11d0ba8
commit 969deb9470
5 changed files with 122 additions and 56 deletions

View File

@@ -3,7 +3,7 @@ package cn.dev33.satoken.config;
import java.io.Serializable; import java.io.Serializable;
/** /**
* Sa-Token 配置类 Model * Sa-Token 配置类 Model
* <p> * <p>
* 你可以通过yml、properties、java代码等形式配置本类参数具体请查阅官方文档: http://sa-token.dev33.cn/ * 你可以通过yml、properties、java代码等形式配置本类参数具体请查阅官方文档: http://sa-token.dev33.cn/
* *
@@ -32,6 +32,11 @@ public class SaTokenConfig implements Serializable {
/** 在多人登录同一账号时是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) */ /** 在多人登录同一账号时是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) */
private Boolean isShare = true; private Boolean isShare = true;
/**
* 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置才有效)
*/
private int maxLoginCount = 10;
/** 是否尝试从请求体里读取token */ /** 是否尝试从请求体里读取token */
private Boolean isReadBody = true; private Boolean isReadBody = true;
@@ -176,6 +181,22 @@ public class SaTokenConfig implements Serializable {
return this; return this;
} }
/**
* @return 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置才有效)
*/
public int getMaxLoginCount() {
return maxLoginCount;
}
/**
* @param maxLoginCount 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置才有效)
* @return 对象自身
*/
public SaTokenConfig setMaxLoginCount(int maxLoginCount) {
this.maxLoginCount = maxLoginCount;
return this;
}
/** /**
* @return 是否尝试从请求体里读取token * @return 是否尝试从请求体里读取token
*/ */
@@ -458,6 +479,7 @@ public class SaTokenConfig implements Serializable {
+ ", activityTimeout=" + activityTimeout + ", activityTimeout=" + activityTimeout
+ ", isConcurrent=" + isConcurrent + ", isConcurrent=" + isConcurrent
+ ", isShare=" + isShare + ", isShare=" + isShare
+ ", maxLoginCount=" + maxLoginCount
+ ", isReadBody=" + isReadBody + ", isReadBody=" + isReadBody
+ ", isReadHead=" + isReadHead + ", isReadHead=" + isReadHead
+ ", isReadCookie=" + isReadCookie + ", isReadCookie=" + isReadCookie

View File

@@ -1,6 +1,7 @@
package cn.dev33.satoken.session; package cn.dev33.satoken.session;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -110,12 +111,37 @@ public class SaSession implements Serializable {
private final List<TokenSign> tokenSignList = new Vector<>(); private final List<TokenSign> tokenSignList = new Vector<>();
/** /**
* 返回token签名列表的拷贝副本 * 此Session绑定的token签名列表
* *
* @return token签名列表 * @return token签名列表
*/ */
public List<TokenSign> getTokenSignList() { public List<TokenSign> getTokenSignList() {
return new Vector<>(tokenSignList); return tokenSignList;
}
/**
* 返回token签名列表的拷贝副本
*
* @return token签名列表
*/
public List<TokenSign> tokenSignListCopy() {
return new ArrayList<>(tokenSignList);
}
/**
* 返回token签名列表的拷贝副本根据 device 筛选
*
* @param device 设备类型,填 null 代表不限设备类型
* @return token签名列表
*/
public List<TokenSign> tokenSignListCopyByDevice(String device) {
List<TokenSign> list = new ArrayList<>();
for (TokenSign tokenSign : tokenSignListCopy()) {
if(device == null || tokenSign.getDevice().equals(device)) {
list.add(tokenSign);
}
}
return list;
} }
/** /**
@@ -125,7 +151,7 @@ public class SaSession implements Serializable {
* @return 查找到的tokenSign * @return 查找到的tokenSign
*/ */
public TokenSign getTokenSign(String tokenValue) { public TokenSign getTokenSign(String tokenValue) {
for (TokenSign tokenSign : getTokenSignList()) { for (TokenSign tokenSign : tokenSignListCopy()) {
if (tokenSign.getValue().equals(tokenValue)) { if (tokenSign.getValue().equals(tokenValue)) {
return tokenSign; return tokenSign;
} }
@@ -140,7 +166,7 @@ public class SaSession implements Serializable {
*/ */
public void addTokenSign(TokenSign tokenSign) { public void addTokenSign(TokenSign tokenSign) {
// 如果已经存在于列表中,则无需再次添加 // 如果已经存在于列表中,则无需再次添加
for (TokenSign tokenSign2 : getTokenSignList()) { for (TokenSign tokenSign2 : tokenSignListCopy()) {
if (tokenSign2.getValue().equals(tokenSign.getValue())) { if (tokenSign2.getValue().equals(tokenSign.getValue())) {
return; return;
} }

View File

@@ -1,7 +1,10 @@
package cn.dev33.satoken.stp; package cn.dev33.satoken.stp;
import java.util.*; import java.util.ArrayList;
import java.util.function.Consumer; import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import cn.dev33.satoken.SaManager; import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckLogin;
@@ -316,6 +319,11 @@ public class StpLogic {
// 如果配置为共享token, 则尝试从Session签名记录里取出token // 如果配置为共享token, 则尝试从Session签名记录里取出token
if(getConfigOfIsShare()) { if(getConfigOfIsShare()) {
tokenValue = getTokenValueByLoginId(id, loginModel.getDeviceOrDefault()); tokenValue = getTokenValueByLoginId(id, loginModel.getDeviceOrDefault());
} else {
// 如果配置为不共享token需要检查会话是否超出 max-login-count
if(config.getMaxLoginCount() != -1) {
logoutByRetainCount(id, null, config.getMaxLoginCount() - 1);
}
} }
} else { } else {
// --- 如果不允许并发登录,则将这个账号的历史登录标记为:被顶下线 // --- 如果不允许并发登录,则将这个账号的历史登录标记为:被顶下线
@@ -383,7 +391,7 @@ public class StpLogic {
public void logout(Object loginId) { public void logout(Object loginId) {
logout(loginId, null); logout(loginId, null);
} }
/** /**
* 会话注销根据账号id 和 设备类型 * 会话注销根据账号id 和 设备类型
* *
@@ -391,12 +399,38 @@ public class StpLogic {
* @param device 设备类型 (填null代表注销所有设备类型) * @param device 设备类型 (填null代表注销所有设备类型)
*/ */
public void logout(Object loginId, String device) { public void logout(Object loginId, String device) {
clearTokenCommonMethod(loginId, device, tokenValue -> { logoutByRetainCount(loginId, device, 0);
// 删除Token-Id映射 & 清除Token-Session }
deleteTokenToIdMapping(tokenValue);
deleteTokenSession(tokenValue); /**
SaManager.getSaTokenListener().doLogout(loginType, loginId, tokenValue); * 会话注销根据账号id 和 设备类型 和 保留数量
}, true); *
* @param loginId 账号id
* @param device 设备类型 (填null代表注销所有设备类型)
* @param retainCount 保留最近的n次登录
*/
public void logoutByRetainCount(Object loginId, String device, int retainCount) {
SaSession session = getSessionByLoginId(loginId, false);
if(session != null) {
List<TokenSign> list = session.tokenSignListCopyByDevice(device);
// 遍历操作
for (int i = 0; i < list.size(); i++) {
// 只操作前n条
if(i >= list.size() - retainCount) {
continue;
}
// 清理: token签名、token最后活跃时间
String tokenValue = list.get(i).getValue();
session.removeTokenSign(tokenValue);
clearLastActivity(tokenValue);
// 删除Token-Id映射 & 清除Token-Session
deleteTokenToIdMapping(tokenValue);
deleteTokenSession(tokenValue);
SaManager.getSaTokenListener().doLogout(loginType, loginId, tokenValue);
}
// 注销 Session
session.logoutByTokenSignCountToZero();
}
} }
/** /**
@@ -451,11 +485,20 @@ public class StpLogic {
* @param device 设备类型 (填null代表踢出所有设备类型) * @param device 设备类型 (填null代表踢出所有设备类型)
*/ */
public void kickout(Object loginId, String device) { public void kickout(Object loginId, String device) {
clearTokenCommonMethod(loginId, device, tokenValue -> { SaSession session = getSessionByLoginId(loginId, false);
// 将此 token 标记为已被踢下线 if(session != null) {
updateTokenToIdMapping(tokenValue, NotLoginException.KICK_OUT); for (TokenSign tokenSign: session.tokenSignListCopyByDevice(device)) {
SaManager.getSaTokenListener().doKickout(loginType, loginId, tokenValue); // 清理: token签名、token最后活跃时间
}, true); String tokenValue = tokenSign.getValue();
session.removeTokenSign(tokenValue);
clearLastActivity(tokenValue);
// 将此 token 标记为已被踢下线
updateTokenToIdMapping(tokenValue, NotLoginException.KICK_OUT);
SaManager.getSaTokenListener().doKickout(loginType, loginId, tokenValue);
}
// 注销 Session
session.logoutByTokenSignCountToZero();
}
} }
/** /**
@@ -498,44 +541,18 @@ public class StpLogic {
* @param device 设备类型 (填null代表顶替所有设备类型) * @param device 设备类型 (填null代表顶替所有设备类型)
*/ */
public void replaced(Object loginId, String device) { public void replaced(Object loginId, String device) {
clearTokenCommonMethod(loginId, device, tokenValue -> {
// 将此 token 标记为已被顶替
updateTokenToIdMapping(tokenValue, NotLoginException.BE_REPLACED);
SaManager.getSaTokenListener().doReplaced(loginType, loginId, tokenValue);
}, false);
}
/**
* 封装 注销、踢人、顶人 三个动作的相同代码无API含义方法
* @param loginId 账号id
* @param device 设备类型
* @param appendFun 追加操作
* @param isLogoutSession 是否注销 User-Session
*/
protected void clearTokenCommonMethod(Object loginId, String device, Consumer<String> appendFun, boolean isLogoutSession) {
// 1. 如果此账号尚未登录,则不执行任何操作
SaSession session = getSessionByLoginId(loginId, false); SaSession session = getSessionByLoginId(loginId, false);
if(session == null) { if(session != null) {
return; for (TokenSign tokenSign: session.tokenSignListCopyByDevice(device)) {
} // 清理: token签名、token最后活跃时间
// 2. 循环token签名列表开始删除相关信息
for (TokenSign tokenSign : session.getTokenSignList()) {
if(device == null || tokenSign.getDevice().equals(device)) {
// -------- 共有操作
// s1. 获取token
String tokenValue = tokenSign.getValue(); String tokenValue = tokenSign.getValue();
// s2. 清理掉[token-last-activity] session.removeTokenSign(tokenValue);
clearLastActivity(tokenValue); clearLastActivity(tokenValue);
// s3. 从token签名列表移除 // 将此 token 标记为已被顶替
session.removeTokenSign(tokenValue); updateTokenToIdMapping(tokenValue, NotLoginException.BE_REPLACED);
// -------- 追加操作 SaManager.getSaTokenListener().doReplaced(loginType, loginId, tokenValue);
appendFun.accept(tokenValue);
} }
} }
// 3. 尝试注销session
if(isLogoutSession) {
session.logoutByTokenSignCountToZero();
}
} }
// ---- 会话查询 // ---- 会话查询
@@ -1381,7 +1398,7 @@ public class StpLogic {
return Collections.emptyList(); return Collections.emptyList();
} }
// 遍历解析 // 遍历解析
List<TokenSign> tokenSignList = session.getTokenSignList(); List<TokenSign> tokenSignList = session.tokenSignListCopy();
List<String> tokenValueList = new ArrayList<>(); List<String> tokenValueList = new ArrayList<>();
for (TokenSign tokenSign : tokenSignList) { for (TokenSign tokenSign : tokenSignList) {
if(device == null || tokenSign.getDevice().equals(device)) { if(device == null || tokenSign.getDevice().equals(device)) {
@@ -1411,7 +1428,7 @@ public class StpLogic {
return null; return null;
} }
// 遍历解析 // 遍历解析
List<TokenSign> tokenSignList = session.getTokenSignList(); List<TokenSign> tokenSignList = session.tokenSignListCopy();
for (TokenSign tokenSign : tokenSignList) { for (TokenSign tokenSign : tokenSignList) {
if(tokenSign.getValue().equals(tokenValue)) { if(tokenSign.getValue().equals(tokenValue)) {
return tokenSign.getDevice(); return tokenSign.getDevice();

View File

@@ -79,6 +79,7 @@ PS两者的区别在于**`模式1会覆盖yml中的配置模式2会与y
| activityTimeout | long | -1 | token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒, 默认-1 代表不限制 (例如可以设置为1800代表30分钟内无操作就过期) [参考token有效期详解](/fun/token-timeout) | | activityTimeout | long | -1 | token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒, 默认-1 代表不限制 (例如可以设置为1800代表30分钟内无操作就过期) [参考token有效期详解](/fun/token-timeout) |
| isConcurrent | Boolean | true | 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) | | isConcurrent | Boolean | true | 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) |
| isShare | Boolean | true | 在多人登录同一账号时是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) | | isShare | Boolean | true | 在多人登录同一账号时是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) |
| maxLoginCount | int | 10 | 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置才有效) |
| isReadBody | Boolean | true | 是否尝试从 请求体 里读取 Token | | isReadBody | Boolean | true | 是否尝试从 请求体 里读取 Token |
| isReadHead | Boolean | true | 是否尝试从 header 里读取 Token | | isReadHead | Boolean | true | 是否尝试从 header 里读取 Token |
| isReadCookie | Boolean | true | 是否尝试从 cookie 里读取 Token | | isReadCookie | Boolean | true | 是否尝试从 cookie 里读取 Token |

View File

@@ -5,12 +5,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.session.SaSession;
/** /**
* Jackson定制版SaSession忽略 timeout 属性的序列化 * Jackson定制版SaSession忽略 timeout 属性的序列化
* *
* @author kong * @author kong
* *
*/ */
@JsonIgnoreProperties("timeout") @JsonIgnoreProperties({"timeout"})
public class SaSessionForJacksonCustomized extends SaSession { public class SaSessionForJacksonCustomized extends SaSession {
/** /**