SaToken使用SpringBoot整合SaTokentoken自动续期+token定期刷新+注解鉴权
Posted 符华-
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SaToken使用SpringBoot整合SaTokentoken自动续期+token定期刷新+注解鉴权相关的知识,希望对你有一定的参考价值。
目录
🥤 一、需求
1、token自动续期
当用户一直在操作页面请求服务器时,token应该是需要一直有效的,不能那个页面点着点着就告诉用户需要重新登录吧,除非是用户长时间没有请求服务器了,才需要重新登录。
实现超过指定时间没有请求服务器,重新登录,可以直接在配置文件中设置 activity-timeout 值就行。
但是token自动续期,用satoken自带的话,它需要调用 StpUtil 类里面的一些方法才会续期,但是我要的效果是不管调用哪个接口,token都会续期,所以这里需要自定义一个拦截器来实现。
2、token定期刷新
如果token长时间续期或者token的有效期很长,token值一直不变的话可能不太安全,所以需要加个定期刷新token的功能。同样在拦截器中实现,获取token的创建时间,当创建时间距离当前时间超过了两个小时,就生成一个新的token,并设置到响应头中。
3、注解鉴权
通过注解来实现角色权限认证或者是菜单权限认证等。比如某个接口只有管理员角色才能访问,或者必须具有指定权限才能进入该方法。
注意:要使用注解鉴权,只有注册satoken自带的拦截器才能用,只用自定义的拦截器是没有效果的。
🏺 二、项目搭建
本次使用的SaToken是基于1.31.0版本的
1、引入依赖
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.5.6</spring-boot.version>
<sa-token-version>1.31.0</sa-token-version>
</properties>
<dependencies>
<!-- ......省略其他依赖...... -->
<!-- sa-token权限认证框架 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>$sa-token-version</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>$sa-token-version</version>
</dependency>
</dependencies>
2、配置文件
server:
port: 7070
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/base_project?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
username: root
password: root
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: "127.0.0.1"
port: 6379
timeout: 10s
password: 123456
database: 0
lettuce:
pool:
max-active: -1
max-wait: -1
max-idle: 16
min-idle: 8
main:
allow-bean-definition-overriding: true
servlet:
multipart:
max-file-size: -1
max-request-size: -1
aop:
auto: true
# Sa-Token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: base-project
# token有效期,单位s 默认30天, -1代表永不过期
timeout: 3600
# token风格
token-style: random-32
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: false
# 是否开启token自动续签
auto-renew: true
# 临时有效期,单位s,例如将其配置为 1800 (30分钟),代表用户如果30分钟无操作,则此Token会立即过期
activity-timeout: 1800
mybatis-plus:
mapper-locations: classpath:mapper/*/*.xml
type-aliases-package: com.entity.sys,;com.common.base,;com.entity.biz
global-config:
db-config:
id-type: auto
field-strategy: NOT_EMPTY
db-type: MYSQL
configuration:
map-underscore-to-camel-case: true
call-setters-on-nulls: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
filePath: upload/
3、全局配置
import cn.dev33.satoken.interceptor.SaInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.*;
/**
* 全局配置
*/
@Configuration
@EnableWebMvc
public class GlobalCorsConfig implements WebMvcConfigurer
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry)
String path = System.getProperty("user.dir") + System.getProperty("file.separator")+ "upload" + System.getProperty("file.separator");
registry.addResourceHandler("/upload/**").addResourceLocations("file:" + path);
/**
* 注册拦截器
* 关于 Sa-Token的拦截器 和 自定义的拦截器,其实也可以只选其中一个的。
* 我之所以两个都用了,是因为假如只用自带的拦截器的话,token续期只有调用 StpUtil 类里面的一些方法才会续期,但是我想要的是不管调用哪个接口,都自动续期。
* 假如只用自定义的拦截器的话,它不能用注解鉴权,有试过用 extends SaInterceptor 也还是不行,所以只能两个拦截器一起用。
* 可以根据自己实际情况选择其中一种或者两种都用。
*/
@Override
public void addInterceptors(InterceptorRegistry registry)
// 注册Sa-Token的路由拦截器
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
// 注册自定义拦截器,这个拦截器用于手动刷新token过期时间
registry.addInterceptor(new CustomInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/sys/login","/sys/getCode","/sys/getKey","/api/favicon.ico","/upload/**");
/**
* 允许跨域调用的过滤器
*/
@Bean
public CorsFilter corsFilter()
CorsConfiguration config = new CorsConfiguration();
//允许所有域名进行跨域调用
config.addAllowedOriginPattern("*");
//允许跨越发送cookie
config.setAllowCredentials(true);
//放行全部原始头信息
config.addAllowedHeader("*");
//允许所有请求方法跨域调用
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
@Override
public void addCorsMappings(CorsRegistry registry)
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600)
.exposedHeaders();
4、全局异常处理
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import com.common.base.BaseConstant;
import com.common.util.ResultUtil;
import com.common.vo.ExceptionVo;
import com.common.vo.ResultVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
/**
* 全局异常处理
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionConfig
/**
* 自定义异常
*/
@ExceptionHandler(value = ExceptionVo.class)
public ResultVo processException(ExceptionVo e)
log.error("位置: -> 错误信息:", e.getMethod() ,e.getMessage());
return ResultUtil.error(e.getCode(),e.getMessage());
/**
* 拦截表单参数校验
*/
@ResponseBody
@ExceptionHandler(BindException.class)
public ResultVo bindExceptionHandler(BindException ex)
StringBuffer sb = new StringBuffer();
BindingResult bindingResult = ex.getBindingResult();
if (bindingResult.hasErrors())
for (int i = 0; i < bindingResult.getAllErrors().size(); i++)
ObjectError error = bindingResult.getAllErrors().get(i);
sb.append((i == 0 ? "" : "\\n") + error.getDefaultMessage());
return ResultUtil.error(sb.toString());
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public ResultVo handler(ConstraintViolationException ex)
StringBuffer sb = new StringBuffer();
int i = 0;
for (ConstraintViolation violation : ex.getConstraintViolations())
sb.append((++i == 1 ? "" : "\\n") + violation.getMessage());
return ResultUtil.error(sb.toString());
/**
* 请求方式不支持
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResultVo httpReqMethodNotSupported(HttpRequestMethodNotSupportedException e)
log.error("错误信息:", e.getLocalizedMessage());
return ResultUtil.error("请求方式不支持");
/**
* 未登录异常
*/
@ExceptionHandler(NotLoginException.class)
public ResultVo notLoginException(NotLoginException e)
return ResultUtil.error(1003,"用户未登录");
/**
* 通用异常
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(Exception.class)
public ResultVo exception(Exception e)
if (e instanceof NotPermissionException)
return ResultUtil.error("没有操作权限");
e.printStackTrace();
return ResultUtil.error(BaseConstant.UNKNOWN_EXCEPTION);
5、自定义拦截器(token续期 和 定期刷新)
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.strategy.SaStrategy;
import cn.dev33.satoken.util.SaFoxUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.common.base.BaseConstant;
import com.common.util.RedisUtil;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 自定义拦截器(token续期 和 token定期刷新)
*/
public class CustomInterceptor implements HandlerInterceptor
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
response.setHeader( "Set-Cookie" , "cookiename=httponlyTest;Path=/;Domain=domainvalue;Max-Age=seconds;HTTPOnly");
response.setHeader( "Content-Security-Policy" , "default-src 'self'; script-src 'self'; frame-ancestors 'self'");
response.setHeader("Access-Control-Allow-Origin", (request).getHeader("Origin"));
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Referrer-Policy","no-referrer");
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
// 获取当前token(这个token获取的是请求头的token,也可以用 request 获取)
String tokenValue = StpUtil.getTokenValue();
// 根据token获取用户id(这里如果找不到id直接返回null,不会报错)
String loginId = (String) StpUtil.getLoginIdByToken(tokenValue);
//判断token的创建时间是否大于2小时,如果是的话则需要生成新的token
long time = System.currentTimeMillis() - StpUtil.getSession().getCreateTime();
long hour = time/1000/(60 * 60);
if (hour>2)
/**
* TODO: 生成新的token有两种方式:
* 方式一:先退出,然后再重新登录:退出之前得先把session中的用户信息拿出来,登录之后重新设置到session中。
* 方式二:重新登录,并且重写token生成方式:重新token后,redis中以token值为key的旧token还存在于redis中,得手动删除
*/
// TODO 方式一:获取session中存储的用户信息,重新登录后,将这个用户信息重新设置到session中。
/*SysUser user = (SysUser) StpUtil.getSession().get("user");
StpUtil.logout(loginId); // 这里要生成新的token的话,要先退出再重新登录
StpUtil.login(loginId); // 然后再重新登录,生成新的token
String newToken = StpUtil.getTokenValue();
StpUtil.getSession().set("user",user);*/
// TODO 方式二:重新登录,并且重写token生成方式,并且把redis中旧token手动删除
StpUtil.login(loginUserId);
SaStrategy.me.createToken = (loginId, loginType) ->
return SaFoxUtil.getRandomString(32); // 生成新的token,随机32位长度字符串
;
String newToken = StpUtil.getTokenValue();
RedisUtil redisUtil = SpringUtil.getBean(RedisUtil.class);
redisUtil.del(BaseConstant.tokenCachePrefix+tokenValue);// 删除旧to以上是关于SaToken使用SpringBoot整合SaTokentoken自动续期+token定期刷新+注解鉴权的主要内容,如果未能解决你的问题,请参考以下文章
SaToken使用SpringBoot整合SaToken关于数据权限
SaToken使用SpringBoot整合SaTokentoken自动续期+token定期刷新+注解鉴权
SaToken使用SpringBoot整合SaTokentoken自动续期+token定期刷新+注解鉴权
SaToken使用SpringBoot整合SaTokentoken自动续期+token定期刷新+注解鉴权