1.现有情况
我们有客户端PC端和App端。
-
1.App端要实现单设备登录,App端账号在一台设备上登录后,其他App端该账号token自动失效。
-
2.PC端,一个客户端只能登录一个账号。
-
3.根据不同的业务和登录方式获取不同的token失效时间
2.redis设计
2.1 Redis Key|Value设计
RedisKey.REDIS_JOIN :定义的特殊链接字符常量。
Key | Value | 备注 |
---|---|---|
RedisKey.CLIENT_USER_TOKEN, clientId + RedisKey.REDIS_JOIN + accountId | token | 根据clientId和accountId存放token |
RedisKey.CLIENT_TOKEN+ RedisKey.REDIS_JOIN +clientId | token | 方便清除在同一个客户端登录的其他账号token |
RedisKey.TOKEN_ACCOUNT_ID+ RedisKey.REDIS_JOIN +token | accountId | 根据token存放accountId |
RedisKey.ACCOUNT_LOGIN_CLIENTS+ RedisKey.REDIS_JOIN +accountId | clientId集合 | 存放accountId登陆的客户端id(App端传来的client保存时增加后缀clientIdSuffix) |
2.2 流程思路
/**
* 生成token, 种缓存并清除统一账号下已登录的token
*
* @param signInType
* @param accountId
* @param clientId
* @param clientIdSuffix 用于区分系统(APP或PC,PC默认为null)
* @return
*/
方法:generateToken(int signInType, String accountId, String clientId, String business, String clientIdSuffix)
//清除在同一个客户端登录的其他账号token
RedisKey.CLIENT_TOKEN, clientId tokenInSameClientKey
//根据其他账号token清除存放的accountId
RedisKey.TOKEN_ACCOUNT_ID, token
//根据其他账号accountId和clientId清除存放的token
RedisKey.CLIENT_USER_TOKEN, clientId + RedisKey.REDIS_JOIN + accountId redisExecutorKey
clientIdSuffix !=null 说明是App端传来的clientId,需要通过清除accountId清除App其他设备的token
RedisKey.ACCOUNT_LOGIN_CLIENTS, accountId 拿到account登录的设备id集合:clientIds
遍历clientIds——>item,判断item是否包含clientIdSuffix前缀(item.contains(clientIdSuffix))
包含的话,根据clientId和acountId删除旧的token
// set new token
redis.setString(redisExecutorKey, token, timeout);
redis.setString(tokenInSameClientKey, token, timeout);
redis.setString(redis.generateKey(RedisKey.TOKEN_ACCOUNT_ID, token), accountId, timeout);
//添加clientId
recordLoginClients(accountId, clientId, timeout, clientIdSuffix);
3代码实现
import org.apache.commons.lang3.StringUtils;
import java.util.UUID;
public class ModelUtil {
//用于生成token
public static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}
}
@Service
public class TokenDomainService {
@Resource
private RedisClientService redis;
@Autowired
MerchantConfiguration merchantConfiguration;
/**
* 生成token, 种缓存并清除统一账号下已登录的token
*
* @param signInType
* @param accountId
* @param clientId
* @param clientIdSuffix 用于区分系统(APP或PC,PC默认为null)
* @return
*/
public String generateToken(int signInType, String accountId, String clientId, String business, String clientIdSuffix) {
//生成token
String token = ModelUtil.uuid();
//根据不同的业务和登录方式获取失效时间
int timeout = getTokenTimeout(signInType, business);
// 清除在同一个客户端登录的其他账号token
Key tokenInSameClientKey = redis.generateKey(RedisKey.CLIENT_TOKEN, clientId);
String tokenInSameClient = redis.get(tokenInSameClientKey);
if (!StringUtils.isEmpty(tokenInSameClient)) {
redis.delete(tokenInSameClientKey);
Key redisExecutorForOtherTokenKey = redis.generateKey(RedisKey.TOKEN_ACCOUNT_ID, tokenInSameClient);
String prevAccountId = redis.get(redisExecutorForOtherTokenKey);
if (!StringUtils.isEmpty(prevAccountId)) {
redis.delete(redisExecutorForOtherTokenKey);
redis.delete(redis.generateKey(RedisKey.CLIENT_USER_TOKEN, clientId + RedisKey.REDIS_JOIN + prevAccountId));
}
}
// remove old token for same clientId&accountId
Key redisExecutorKey = redis.generateKey(RedisKey.CLIENT_USER_TOKEN, clientId + RedisKey.REDIS_JOIN + accountId);
String oldToken = redis.get(redisExecutorKey);
if (!StringUtils.isEmpty(oldToken)) {
redis.delete(redis.generateKey(RedisKey.TOKEN_ACCOUNT_ID, oldToken));
LogUtils.INFO.info("单态登录清除token:{}", oldToken);
}
//remove same clientType&accountId
if (StringUtils.equals(clientIdSuffix, ClientTypeEnum.MERCHANT_APP.getValue())) {
clearAllToken(accountId, clientIdSuffix);
}
// set new token
redis.setString(redisExecutorKey, token, timeout);
redis.setString(tokenInSameClientKey, token, timeout);
redis.setString(redis.generateKey(RedisKey.TOKEN_ACCOUNT_ID, token), accountId, timeout);
recordLoginClients(accountId, clientId, timeout, clientIdSuffix);
return token;
}
/**
* 根据业务线及登陆类型区分token有效期
*
* @param signInType
* @param business
* @return
*/
public int getTokenTimeout(int signInType, String business) {
AuthTokenConfig authTokenConfig = merchantConfiguration.getAuthTokenConfig();
int timeout;
if (SignInTypeEnum.PASSWORD.getValue().equals(signInType)) {
timeout = authTokenConfig.getPwdTokenTimeout();
if (!StringUtils.isEmpty(business) && MapUtils.isNotEmpty(authTokenConfig.getBusinessPwdTokenTimeout())) {
if (authTokenConfig.getBusinessPwdTokenTimeout().get(business) != null) {
timeout = authTokenConfig.getBusinessPwdTokenTimeout().get(business);
}
}
} else {
timeout = authTokenConfig.getSmsTokenTimeout();
if (!StringUtils.isEmpty(business) && MapUtils.isNotEmpty(authTokenConfig.getBusinessSmsTokenTimeout())) {
if (authTokenConfig.getBusinessSmsTokenTimeout().get(business) != null) {
timeout = authTokenConfig.getBusinessSmsTokenTimeout().get(business);
}
}
}
return timeout;
}
/**
* 记录账号当前登录的 clientId, 方便修改登录信息时候 清除所有登录端的token
*
* @param accountId
* @param clientId
* @param timeout
* @param clientIdSuffix (不同端添加不同后缀,PC端默认不添加)
*/
private void recordLoginClients(String accountId, String clientId, int timeout, String clientIdSuffix) {
if (StringUtils.isNotEmpty(clientIdSuffix)) {
clientId = clientId + MerchantConsts.splitStr.underLine + clientIdSuffix;
}
Key key = redis.generateKey(RedisKey.ACCOUNT_LOGIN_CLIENTS, accountId);
redis.sadd(key, clientId);
redis.expire(redis.generateKey(RedisKey.ACCOUNT_LOGIN_CLIENTS, accountId), timeout);
}
/**
* 从token中解析账号id
*
* @param token
* @return
*/
public String extractAccountId(String token) {
Key key = redis.generateKey(RedisKey.TOKEN_ACCOUNT_ID, token);
return redis.get(key);
}
/**
* 清除账号在所有端登录的token
*
* @param accountId
*/
public void clearAllToken(String accountId, String clientIdSuffix) {
Key key = redis.generateKey(RedisKey.ACCOUNT_LOGIN_CLIENTS, accountId);
Set<String> clientIds = redis.smembers(key);
if (CollectionUtils.isEmpty(clientIds)) {
return;
}
//根据端类型删token
if (StringUtils.isNotEmpty(clientIdSuffix)) {
clientIds.forEach(clientIdWithSuffix -> {
if (StringUtils.isNotEmpty(clientIdWithSuffix) && clientIdWithSuffix.contains(clientIdSuffix)) {
String[] clientIdArray = clientIdWithSuffix.split(MerchantConsts.splitStr.underLine);
if (clientIdArray.length == 2) {
String clientId = clientIdArray[0];
Key oldTokenKey = redis.generateKey(RedisKey.CLIENT_USER_TOKEN, clientId + RedisKey.REDIS_JOIN + accountId);
String oldToken = redis.get(oldTokenKey);
if (StringUtils.isNotEmpty(oldToken)) {
redis.delete(redis.generateKey(RedisKey.TOKEN_ACCOUNT_ID, oldToken));
}
redis.delete(oldTokenKey);
}
}
});
} else {
//不区分端类型删token
clientIds.forEach(clientId -> {
if (clientId.contains(ClientTypeEnum.MERCHANT_APP.getValue())) {
String[] clientIdArray = clientId.split(MerchantConsts.splitStr.underLine);
if (clientIdArray.length == 2) {
String realClientId = clientIdArray[0];
Key oldTokenKey = redis.generateKey(RedisKey.CLIENT_USER_TOKEN, realClientId + RedisKey.REDIS_JOIN + accountId);
String oldToken = redis.get(oldTokenKey);
if (StringUtils.isNotEmpty(oldToken)) {
redis.delete(redis.generateKey(RedisKey.TOKEN_ACCOUNT_ID, oldToken));
}
redis.delete(oldTokenKey);
}
} else {
Key oldTokenKey = redis.generateKey(RedisKey.CLIENT_USER_TOKEN, clientId + RedisKey.REDIS_JOIN + accountId);
String oldToken = redis.get(oldTokenKey);
if (StringUtils.isNotEmpty(oldToken)) {
redis.delete(redis.generateKey(RedisKey.TOKEN_ACCOUNT_ID, oldToken));
}
redis.delete(oldTokenKey);
}
});
}
}
/**
* 清除token,账号登出调用
*
* @param token
* @param clientId
* @param accountId
*/
public void clearToken(String token, String clientId, String accountId) {
redis.delete(redis.generateKey(RedisKey.TOKEN_ACCOUNT_ID, token));
redis.delete(redis.generateKey(RedisKey.CLIENT_USER_TOKEN, clientId + RedisKey.REDIS_JOIN + accountId));
}
}