mirror of
https://gitee.com/dromara/sa-token.git
synced 2025-09-23 04:23:36 +08:00
新增 maxLoginCount 配置,指定同一账号可同时在线的最大数量
This commit is contained in:
@@ -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
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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();
|
||||||
|
@@ -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 |
|
||||||
|
@@ -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 {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user