一、 SpringBoot中Token登录授权、续期和终止的方案Redis+Token
SpringBoot项目写登录注册之类的方案
- 使用Cookie或Session的话,它是有状态的,不符合分布式技术架构
- 使用Security或者Shiro框架实现起来比较复杂,一般项目无需用那么复杂
- 使用JWT它虽然是无状态的,也可以载荷用户数据,但还是有很多缺点:
- 缺点1:设置过期时间后,无法强制让它过期,在有效期内它始终可用
- 缺点2:一次性的,如果用户数据有变,只能重新生成新的JWT
下面我们看下常用的两种方案redis+token方案和JWT方案:
1、Redis+Token方案的授权
1.1 基本原理
登录后使用UUID生成token,前端每次请求都会带上这个token作为授权凭证。这种方案是能自动续签,也能做到主动终止。所以很多项目用的都是Redis+Token方案,简单方便问题少。缺点就是需要依赖Redis和数据库。
1.2 基本流程:
- 设置一个拦截器,不校验登录接口,拦截其他接口
- 登录接口接收前端传来的用户名密码,去数据库查询该用户名是否存在,该密码是否正确
- 如果正确则表示登录成功,调用生成Token方法,返回Token给前端
- Token使用用户id或账号+时间戳+UUID用MD5加密的一串字符串(不建议用其他数据,因为id或账号极少变更的,变更会增加复杂性)
- 生成后存储到Redis,把Token当作键,用户数据当作值,并设置过期时间
- 生成Token的方法中,还得防止重复调登录接口,不停生成不同的Token,所以先判断数据库中是否存在键,所以保存token键到redis的同时要在redis中再增加一条用户ID为键Token为值的数据,可以验证该用户是否已经生成过token。
针对上述重复生成token问题,使用lua优化以下就可以了
demo代码:
private static final String TOKEN_FLAG = "redis_key";
// token 过期时间18000秒 = 5个小时
private static final String EXPIRE_TIME = "18000";
//提前20秒去延期
private static final String CRITICAL = "20";
@Resource
private StringRedisTemplate stringRedisTemplate;
public String sign (String userId){
String key = TOKEN_FLAG + userId;
DefaultRedisScript<String> defaultRedisScript = new DefaultRedisScript<>(
"local token = redis.call('get', KEYS[1]) );" +
"local tokenttl = redis.call('ttl', KEYS[1] );"+
"local critical = tonumber(ARGV4]);"+
"if (token and tokenttl > critical) then return token;" +
"elseif(token and tokenttl <= critical) then" +
"local uid = redis.call('get', token );"+
"redis.call('expire',token,ARGV[3]);"+
"redis.call('expire,uid,ARGV[3]');"+
"return token;"+
"else"+
"redis.call('SET',KEYS[1],ARGV[1]);"+
"redis.call('SET',KEYS[2],ARGV[2]);"+
"redis.call('expire',KEY[1],ARGC[3]);"+
"redis.call('expire,KEYS[2],ARGV[3]');"+
"return ARGC[1];"+
"end;", String.class);
String token = DigestUtils.md5DigestAsHex((getuid32()).getBytes());
return stringRedisTemplate.execute(defaultRedisScript, Arrays.asList(key,token),token,key,EXPIRE_TIME,CRITICAL);
}
校验token时也用lua做验证和续期:
public void checkToken(String token){
DefaultRedisScript<String> defaultRedisScript = new DefaultRedisScript<>(
"local uid = redis.call('get',KEYS[1]);"+
"local uidttl = redis.call('ttl',KEYS[1]);" +
"local critical = tonumber(ARGV[3]);"+
"if (uid and uidttl > critical) then return uid;" +
"elseif(uid and uidttl <= critical) then "+
"redis.call('expire',KEYS[1,ARGV[2]);"+
"redis.call('expire',uid,ARGV[2]);" +
"return uid; else end;",String.class
);
String value = stringRedisTemplate.execute(defaultRedisScript, Collections.singletonList(token),token,EXPIRE_TIME,CRITICAL);
if(StringUtils.hasText(value)){
String userId = value.replace(TOKEN_FLAG,"");
User user = tokenUserMapper.selectById(userId);
if(user == null){
throw new BizException(ComRespnse.UNAUTHORIZED,"用户不存在数据库中,请重新输入");
}
ThreadLocalUtil.setUser(user);
}else{
throw new BizException(ComRespnse.UNAUTHORIZED,"token无效,请重新登录");
}
}
二 、SpringBoot中基于JWT的单token授权和续期方案
在前后端分离架构中,用户登录成功后,后端颁发JWT (Json Web Token)token至前端,该token被安全存储于LocalStorage。随后,每次请求均自动携带此token于请求头中,以验证用户身份。后端设有过滤器,拦截并校验token有效性,一旦发现过期则引导用户重新登录。
非jwt方案参考: SpringBoot中Token登录授权、续期和主动终止的方案(Redis+Token)
简单总结token实现身份认证的步骤:
- 用户登录成功服务端返回token
- 之后每次用户请求都携带token,在Authorization Header中。
- 后端服务取出token进行decode,判断有效期及失效策略。
- 返回对应的成功失败
鉴于JWT包含用户信息且需保障安全,其过期时间通常设置较短。然而,这易导致用户频繁登录,尤其是在处理复杂表单时(比如在线考试),因耗时过长而遇token过期,引发不必要的登录中断和数据丢失,严重影响用户体验。如何在用户无感知状态下实现token自动续期的策略,减少频繁登录需求,确保表单数据不丢失呢?
解决token过期续期问题可以有很多种不同的方案,这里举一些比较有代表性的例子,一种是单token续期,一种是双token续期。
1、 单token续期
-
用户认证与Token生成:用户成功登录后,服务端生成一个包含必要信息的JWT并返回给客户端。此Token作为后续请求的身份验证依据。
-
请求携带Token:在后续的每一次需授权API请求中,客户端都需在HTTP请求的Authorization头部字段中携带此JWT,以便服务端验证用户的身份和权限。
-
Token管理策略:服务端设定了Token的失效时间(或失效次数)以及一个重新登录的期限阈值。每当用户登录时,服务端会记录当前的登录时间,以便后续验证使用。
-
Token验证与响应:
- 当用户携带Token发起请求时,服务端首先根据Token的失效时间和重新登录期限进行验证。
- 若Token有效,则正常处理请求并返回所需资源。
- 若Token已失效但仍在重新登录期限内,服务端返回特定的错误代码提示Token已过期,同时提示客户端进行Token刷新。
5.Token刷新机制:
- 客户端接收到Token过期错误代码后,自动调用Refresh Token接口,向服务端请求刷新Token。
- 服务端验证请求的有效性(如检查是否仍在重新登录期限内等),通过后生成新的有效Token并返回给客户端。
-
使用刷新后的Token:客户端在收到新的Token后,自动替换掉旧的Token,并在后续的请求中携带此新Token继续访问服务。
-
强制重新登录:
- 若服务端判断当前Token的使用时长已超过了设定的重新登录期限,则不再允许通过Refresh Token接口刷新Token。
- 此时,服务端会返回强制重新登录的错误代码给客户端,客户端接收到此代码后,应引导用户跳转至登录页面进行重新登录。
比如:
- 将 token 过期时间设置为15分钟;
- 前端发起请求,后端验证 token 是否过期;如果过期,前端发起刷新token请求,后端为前端返回一个新的token;
- 前端用新的token发起请求,请求成功;
- 如果要实现每隔72小时,必须重新登录,后端需要记录每次用户的登录时间;用户每次请求时,检查用户最后一次登录日期,如超过72小时,则拒绝刷新token的请求,请求失败,跳转到登录页面。
- 后端还可以记录刷新token的次数,比如最多刷新50次,如果达到50次,则不再允许刷新,需要用户重新授权。
2、 双token续期
双token需求方案
三、 SpringBoot中基于JWT的双token授权和续期方案
SpringBoot项目写登录注册之类的方案
- 使用Cookie或Session的话,它是有状态的,不符合分布式技术架构
- 使用Security或者Shiro框架实现起来比较复杂,一般项目无需用那么复杂
- 使用JWT它虽然是无状态的,也可以载荷用户数据,但还是有很多缺点:
-
- 缺点1:设置过期时间后,无法强制让它过期,在有效期内它始终可用
-
- 缺点2:一次性的,如果用户数据有变,只能重新生成新的JWT
下面我们看下常用的两种方案redis+token方案和JWT方案:
1、Redis+Token方案的授权
1.1 基本原理
登录后使用UUID生成token,前端每次请求都会带上这个token作为授权凭证。这种方案是能自动续签,也能做到主动终止。所以很多项目用的都是Redis+Token方案,简单方便问题少。缺点就是需要依赖Redis和数据库。
1.2 基本流程:
- 设置一个拦截器,不校验登录接口,拦截其他接口
- 登录接口接收前端传来的用户名密码,去数据库查询该用户名是否存在,该密码是否正确
- 如果正确则表示登录成功,调用生成Token方法,返回Token给前端
- Token使用用户id或账号+时间戳+UUID用MD5加密的一串字符串(不建议用其他数据,因为id或账号极少变更的,变更会增加复杂性)
- 生成后存储到Redis,把Token当作键,用户数据当作值,并设置过期时间
- 生成Token的方法中,还得防止重复调登录接口,不停生成不同的Token,所以先判断数据库中是否存在键,所以保存token键到redis的同时要在redis中再增加一条用户ID为键Token为值的数据,可以验证该用户是否已经生成过token。
针对上述重复生成token问题,使用lua优化以下就可以了