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定期刷新+注解鉴权

SaToken使用springboot+redis+satoken权限认证

SaToken使用springboot+redis+satoken权限认证