🎏:你只管努力,剩下的交给时间
🏠 :小破站
springboot+satoken实现刷新token
- satoken是什么?支持什么?
- 为什么需要?
- token一直不变存在的问题
- 1. 安全风险增加
- 2. 难以撤销 Token
- 3. 权限滥用和过期信息的风险
- 4. 缺乏会话管理
- 5. 影响用户隐私
- 6. 无法确保设备和网络变化
- 7. 用户体验不佳
- 逻辑+代码实现
- 代码实现
- 拦截器知识补充
- 1. 注册顺序决定执行顺序
- 2. 拦截器方法的执行顺序
- 3. 优先级控制
- 总结
satoken是什么?支持什么?
satoken官网
借用官网的一句话, Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。
这里我们只说明刷新token,也就是前后端分离的场景中常见的一种方案。
为什么需要?
在satoken中存在两个token,一个是真正的token有效期,还有一个是活跃token,也就是说当活跃token过期的时候是不能访问服务的,需要调用相关方法解除。而当正真的token过期的时候就需要登录重新获取凭证。
所以其实根据上面的表述也是可以实现刷新token的
前端后端约定,当后端因为活跃token过期返回给前端响应的状态值,前端拦截并重新调用相关方法,这样也是可以实现刷新token的。
token一直不变存在的问题
Token 长期不变或过期时间过长会带来一系列安全和用户体验方面的问题。以下是一些主要的风险和潜在问题:
1. 安全风险增加
- Token 被截获的风险:如果攻击者通过某种方式截获了用户的 Token,那么在 Token 长期不变或过期时间过长的情况下,攻击者可以持续使用该 Token 访问用户的账号,而用户不会意识到。这极大增加了账号被恶意使用的风险。
- 无法控制 Token 失效:在 Token 长期有效的情况下,即使用户想要主动注销或更改密码,攻击者手中的旧 Token 仍然有效,导致安全威胁无法解除。
- 无法防御会话劫持:当用户登录后 Token 长期不变,一旦发生会话劫持,攻击者可以一直利用该 Token 冒充用户进行操作,直到 Token 失效。
2. 难以撤销 Token
- Token 黑名单机制的缺乏:当 Token 的有效期非常长时,后端很难立即撤销某个 Token。即使用户账号被禁用或者注销,旧的 Token 可能依然可以继续访问系统,直到 Token 到期。
- 缺乏灵活性:Token 如果是长期有效的,即便用户被强制下线,无法简单地让旧 Token 失效,除非重新设计 Token 管理系统。
3. 权限滥用和过期信息的风险
- 权限变化后无法及时更新:当 Token 长时间有效,而用户的权限发生了变化(例如角色升级、降级或权限被撤销),旧 Token 可能依然具有不该有的权限。这可能导致用户获得不适当的访问权限。
- 过期数据风险:如果用户 Token 长期不更新,系统可能无法捕捉到最新的用户状态变更,比如权限、角色、信息等,导致系统提供了不正确的访问权限或内容。
4. 缺乏会话管理
- 无法追踪用户的活跃度:长期有效的 Token 会让系统无法准确跟踪用户的登录会话。系统无法判断用户是否活跃,用户的最后访问时间也无法准确追踪。
- 无法强制用户重新登录:如果 Token 过期时间过长,用户可能在极长时间内不需要重新登录,丧失了会话管理的能力。对于需要更高安全性的场景,强制用户定期登录是必要的。
5. 影响用户隐私
- 隐私泄露风险增加:Token 长期有效可能使得用户的个人信息在长时间内暴露于潜在的攻击面,增加了隐私泄露的风险。如果用户长期未使用系统,Token 应该过期以保护用户隐私。
- 设备共享中的风险:如果用户在共享设备上登录,而 Token 长期不失效,其他人可以轻松访问用户账户,特别是在用户忘记登出或清理浏览器时。
6. 无法确保设备和网络变化
- IP地址、设备等环境因素没有变化:一些 Token 通常会包含用户设备、IP 地址等信息来防止 Token 被滥用。如果 Token 长期不变,那么即使用户的网络环境发生了变化,系统也无法感知。这将导致 Token 在不可信的环境中继续使用,增加了安全风险。
7. 用户体验不佳
- 无法提供个性化内容更新:长期不变的 Token 可能导致系统无法捕捉用户状态的实时变化,从而影响个性化内容推荐或提示用户更新信息的能力。
- 会话管理不灵活:如果用户希望在不同设备上管理会话(如在一个设备上登出时使另一个设备上的 Token 失效),长期不变的 Token 可能无法支持这种场景。
逻辑+代码实现
可以采用双拦截器实现,第一个拦截器是自定义的,而这个拦截器总是返回true,第二个拦截器使用satoken的拦截器做一些登录或者权限的认证。
代码实现
package fun.acowbo.config;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author <a href="https://acowbo.fun">acowbo</a>
* @since 2024/8/27
*/
@Slf4j
@Component
public class CustomInterceptor implements HandlerInterceptor{
@Value("${sa-token.token-name}")
private String tokenName;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
return true;
}
long tokenActivityTimeout = StpUtil.getTokenActivityTimeout();
log.info("tokenActivityTimeout:{}", tokenActivityTimeout);
long tokenTimeout = StpUtil.getTokenTimeout();
if (tokenActivityTimeout > 0){
response.setHeader(tokenName, StpUtil.getTokenValue());
response.setHeader("Access-Control-Expose-Headers", tokenName);
}
if (tokenActivityTimeout < 0 && tokenTimeout > 0){
// 首先要让token活跃
StpUtil.updateLastActivityToNow();
String loginId = (String) StpUtil.getLoginId();
// 先退出,否则之前的token还能用
StpUtil.logout(loginId);
// 重新设置token,这里仅仅是为了安全,否则始终token是一个值
StpUtil.login(loginId,new SaLoginModel().setToken(IdUtil.randomUUID()));
// 请求头修改token的值,否则在第二个拦截器会报错,因为老的token已经失效了
request.setAttribute(tokenName, StpUtil.getTokenValue());
// 响应头设置值
response.setHeader(tokenName, StpUtil.getTokenValue());
response.setHeader("Access-Control-Expose-Headers", tokenName);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
}
}
package fun.acowbo.config;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
import java.util.Arrays;
/**
* description: sa-token权限配置类
*
* @author <a href="https://acowbo.fun">acowbo</a>
* @since 2024/6/7 13:59
*/
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
@Value("${excludePath}")
private String excludePath;
@Resource
private CustomInterceptor customInterceptor;
@Override
public void addCorsMappings(CorsRegistry registry) {
// 允许所有路径
registry.addMapping("/**")
// 允许所有来源
.allowedOrigins("*")
// 允许的 HTTP 方法
.allowedMethods("GET", "POST", "PUT", "DELETE")
// 允许的请求头
.allowedHeaders("*")
// 允许发送 Cookie
.allowCredentials(false)
// 预检请求的缓存时间
.maxAge(3600);
}
// 注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
registry.addInterceptor(customInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(Arrays.asList(excludePath.split(",")));
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
.addPathPatterns("/**")
.excludePathPatterns(Arrays.asList(excludePath.split(",")));
}
}
拦截器知识补充
在 Spring 框架中,如果定义了多个拦截器,它们的执行顺序是根据它们的注册顺序决定的。具体的执行顺序可以通过以下规则来理解:
1. 注册顺序决定执行顺序
在 Spring 中,拦截器是通过 WebMvcConfigurer
接口中的 addInterceptors
方法进行注册的。多个拦截器会按照它们注册的先后顺序进行调用。
- 拦截器的执行顺序(进入请求时):按照注册顺序依次调用,先注册的拦截器会先执行。
- 拦截器的执行顺序(响应时):响应返回时,拦截器的执行顺序与请求时相反,最后注册的拦截器会最先执行。
示例:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new FirstInterceptor()).addPathPatterns("/**");
registry.addInterceptor(new SecondInterceptor()).addPathPatterns("/**");
}
}
在这个例子中:
- 进入请求时:
FirstInterceptor
->SecondInterceptor
- 返回响应时:
SecondInterceptor
->FirstInterceptor
2. 拦截器方法的执行顺序
拦截器的核心方法有以下三个,它们的调用顺序也需要注意:
preHandle
: 在请求处理之前执行。postHandle
: 在请求处理完后(但在视图渲染之前)执行。afterCompletion
: 在视图渲染完成后执行。
当有多个拦截器时:
preHandle
方法按照拦截器的注册顺序执行。postHandle
和afterCompletion
方法则按照相反顺序执行。
3. 优先级控制
如果需要更精确地控制拦截器的顺序,除了按注册顺序,还可以借助其他方法:
- 排序接口:通过实现
Ordered
接口或使用@Order
注解,可以为拦截器明确指定顺序。 - 配置拦截器链的顺序:通过配置文件明确指定拦截器的顺序,也可以使用
InterceptorRegistry
的 API 动态调整顺序。
总结
- 拦截器的执行顺序取决于它们的注册顺序,先注册的拦截器先处理请求,后注册的拦截器先处理响应。
- 可以使用
@Order
注解或实现Ordered
接口来精确控制多个拦截器的执行顺序。