Token design scheme under microservice

You have to go to2022-08-06 08:24:20

背景介绍

        项目初期(The project is a microservice)Used for rapid developmentjwt生成tokenstateless development(Not stored)and is generatedtokenSpecify an expiration time of the next day04:30,So just take what is generated todaytoken就都可以用,This is not only detrimental to the safety of the project itself,And the following functions cannot be implemented either.

        需求一:Whether to support concurrent login

        需求二:Timeout no-operation expiration setting

        需求三:记录在线人数

为解决上述问题,Microservices need to manage statefultoken,And data format management,实现上述需求.

1.token数据存储位置

token数据存储位置,首选当然是redis

2.微服务下的token一致性问题

         一般情况下,生成并存储tokenThe steps are placed in the login link,I thought so too at first,later in the design process,发现一个问题,Under a set of microservice clusters,Some microservices use different onesredis,This is for management consistencytoken有点困难.The latter solution is,in the interception component(In fact, it is an interceptor that all microservices havetokenInterceptor to verify validity)上进行token管理,and record alltoken(In order to determine whether it is generated for the first timetoken).

3.tokenData format and storage format

一、Consider logins from different devices

Due to store nowtokenThe location is on the interceptor,There is no way to know which device the incoming request came from,所以需要用到jwt的Claim,为了后期扩展,将Claimobject with onemap存储.代码如下

 /**
* 生成签名
*
* @param userId 用户Id
* @param secret 用户密码
* @return 加密的token
*/
public static String sign(Long userId, String userName, String secret, Map<String, String> map) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTCreator.Builder builder = JWT.create()
.withClaim(CLAIM_USER_ID, userId)
.withClaim(CLAIM_USER_NAME, userName)
.withClaim(CLAIM_LOGIN_TIME, System.currentTimeMillis());
for (Map.Entry<String, String> entry : map.entrySet()) {
builder.withClaim(entry.getKey(), entry.getValue());
}
return builder.withExpiresAt(getExpiresTime())
.sign(algorithm);
} catch (UnsupportedEncodingException e) {
return null;
}
}

这样tokenThe information we need is there,and can be obtained dynamically,Here we have a device information,放在map中,Later if you want to add others,就可以通过map来扩展. 

二、Consider whether to support concurrent logins

这部分比较简单,i.e. when concurrent logins are not allowed,Create a map object,用户id->tokenThe data format of the collection,When Do Not Allow Concurrent Logins is turned on,获取id对应的token集合,And take the initiative to correspondtokenThe status becomes top and bottom line,以下是部分代码,提供思路.

if (!configurationFile.isConcurrent()) {
String deviceEnums = getMapKey(tokenValue, JwtClaimKey.DEVICEENUMS);
// --- If concurrent logins are not allowed,Then mark the login history of this account as :Was knocked off the line
replaced(userId, deviceEnums);
// ------ 开启session记录-获取 User-Session , 续期
TokenSession session = getSessionByUserId(userId, true);
// 在 User-Session 上记录token签名
session.addTokenSign(tokenValue, deviceEnums);
getSaasTokenDao().updateSession(session);
}
/**
* 顶人下线,根据账号id 和 设备类型
* <p> 当对方再次访问系统时,会抛出NotLoginException异常,场景值=-4 </p>
*
* @param userId 账号id
* @param deviceType 设备类型 (填nullRepresents to replace all device types)
*/
private void replaced(Long userId, String deviceType) {
TokenSession session = getSessionByUserId(userId, false);
if (session != null) {
for (TokenSign tokenSign : session.tokenSignListCopyByDevice(deviceType)) {
// 清理: token签名、token最后活跃时间
String tokenValue = tokenSign.getValue();
if (session.removeTokenSign(tokenValue)) {
getSaasTokenDao().updateSession(session);
}
// 将此 token Mark as superseded
updateTokenToOffline(tokenValue, Authorize.BE_REPLACED);
}
}
}

三、Data format structure under massive login

Due to store nowtokenThe location is on the interceptor,So there is no waytoken进行redis的ttl(设置过期时间,自动消失),So there needs to be a unified management key.使用redis的hash格式

 将过期的token放到 键为tokenOffline中进行统一管理.上面有说过,我们生成的tokenThe set expiration time is the next day04:30,这个是不变的,So the data we need to manage is only within a day.That's why our keys are followed by time.

四、Timeout no-operation expiration setting

判断出该tokenOn the premise of the first login,进行 ( token - > Except for the set expiration time2 )    的映射生成.

并且设置ttlis the set expiration time.

Let's talk about why( token - > Except for the set expiration time2+当前时间戳 )this one mapping,(Except for the set expiration time2+当前时间戳 = faultToleranceTime)is a fault-tolerant time,每次访问时,Get this map,and with the current timestamp(current)进行判断,如果faultToleranceTime<=current则进行,更新( token - > Except for the set expiration time2+当前时间戳 )往后推.And re-update this key-value pairttlis the set expiration time.

五、记录在线人数

判断出该tokenOn the premise of the first login,将新的token放到 键为tokenOnline中进行统一管理.

上面有说过,我们生成的tokenThe set expiration time is the next day04:30,这个是不变的,So the data we need to manage is only within a day.That's why our keys are followed by time.这个键值对,Holds a collection of all online.

问题:There will be a problem with the data design,也就是token过期了,But it didn't put intokenOffline的情况,这个时候就可以用上tokenOnline键值的value,Judging with the current time if it exceeds the time,That is, it is considered to be offline.

4.Consider frequent visitsredis问题

tokenThere is a problem with validation There are several requests for a page,一起发过来,这样redisGet it a few times,为了降低redisacquisition frequency,We can use the project's memory,And it can expire automatically.We are using Google hereguava

设置了30秒过期,这样在30Requests accessed within seconds will not be connectedredis.

5.Consider using separate onesredis连接

Because there may be a large amount of data,And the problem of a large number of visits,So as an extension,We also do it speciallytoken管理的redis连接,This can also divide the pressure to a certain extent.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.StringUtils;
/**
* @author kaixin
* @version 1.0
* @date 2022/7/27
*/
@Configuration
@EnableConfigurationProperties({TokenRedisProperties.class, RedisProperties.class})
public class TokenRedisTemplateManager {
private static final Logger LOGGER = LoggerFactory.getLogger(TokenRedisTemplateManager.class);
private final TokenRedisProperties tokenRedisProperties;
private final RedisProperties redisProperties;
private RedisTemplate<String, String> tokenRedisTemplate;
public TokenRedisTemplateManager(TokenRedisProperties tokenRedisProperties, RedisProperties redisProperties) {
this.tokenRedisProperties = tokenRedisProperties;
this.redisProperties = redisProperties;
buildRedisTemplate();
}
private void buildRedisTemplate() {
//单机模式
RedisStandaloneConfiguration rsc = new RedisStandaloneConfiguration();
if (!StringUtils.isEmpty(tokenRedisProperties.getHost())) {
rsc.setPort(tokenRedisProperties.getPort());
rsc.setPassword(tokenRedisProperties.getPassword());
rsc.setHostName(tokenRedisProperties.getHost());
rsc.setDatabase(tokenRedisProperties.getDatabase());
LOGGER.info("==============token管理-启动token自带redis配置=============");
} else {
rsc.setPort(redisProperties.getPort());
rsc.setPassword(redisProperties.getPassword());
rsc.setHostName(redisProperties.getHost());
rsc.setDatabase(redisProperties.getDatabase());
LOGGER.info("==============token管理-启动spring默认redis配置=============");
}
RedisTemplate<String, String> template = new RedisTemplate<>();
//单机模式
JedisConnectionFactory fac = new JedisConnectionFactory(rsc);
fac.afterPropertiesSet();
template.setDefaultSerializer(new StringRedisSerializer());
template.setConnectionFactory(fac);
template.afterPropertiesSet();
tokenRedisTemplate = template;
}
public RedisTemplate<String, String> get() {
return tokenRedisTemplate;
}
}

this wayRedisTemplateas the internal implementation of the class,It doesn't affect the whole worldredis使用(I have been looking for this method for a long time)

使用的时候,Just follow the code below.

 @Resource
private TokenRedisTemplateManager redisTemplateConcetion;
/**
* Get onlinetoken集合
*
* @return
*/
public boolean checkOnlineByToken(String token) {
return redisTemplateConcetion.get().opsForHash().hasKey(splicingLineTokenValue(ONLINE), token);
}

Share and record as one of your own,If you have any comments or suggestions, you can leave a message and tell me.谢谢大家!!!


 博主新推出的gitee免费开源项目(商城+APP+小程序+H5),有兴趣的小伙伴可以了解一下.

生鲜商城kxmall-小程序 + App + 公众号H5: kxmall-生鲜商城+APP+小程序+H5.同时支持微信小程序、H5、安卓App、苹果App.支持集群部署,单机部署.可用于B2C商城,O2O外卖,社区超市,生鲜【带配套骑手端配送系统】.kxmall使用uniapp编码.使用Java开发,SpringBoot 2.1.x框架,MyBatis-plus持久层框架、Redis作为缓存、MySql作为数据库.前端vuejs作为开发语言.https://gitee.com/zhengkaixing/kxmall


thank
Similar articles