SaToken系列--指定token存储的数据源

一、背景

        由于特殊需求,需要在一个项目里面使用两个redis数据源,一个是本项目A的使用的,另一个数据源是用于satoken使用的。相当于需要使用其他项目B的登录校验,因此需要接入B的satoken,而B的satoken使用的token存储的数据源和本项目不是同一个。

 二、解决方案

一、satoken配置需要和B项目配置一致

# Sa-Token配置
sa-token:
  # token名称 (同时也是cookie名称)
  token-name: 
  # token有效期,单位s 默认30天, -1代表永不过期
  timeout: 
  # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
  active-timeout: 
  # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
  is-concurrent: 
  # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
  is-share: 
  # 是否在登录后将 Token 写入到响应头
  isWriteHeader: tue
  # token风格
  token-style: 
  # 是否输出操作日志
  is-log: 

 二、satoken采用的数据源需要一致

当在springboot项目中加入下面依赖时,satoken将会自动使用redis作为数据源使用。

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-redis-jackson</artifactId>
</dependency>

如果项目配置文件中还配置了以下配置,satoken将会默认使用。

redis:
  database: 
  host: 
  port: 
  password: 
  timeout: 
  lettuce:
    pool:
      max-active: 
      max-wait: 
      max-idle: 
      min-idle: 
    shutdown-timeout: 

注意:

一、所以想要在B项目登录拿到的token能够在A项目中使用,就需要给在A项目中的tsaoken使用和B项目一样的redis配置。如此在B项目中拿到token在A项目中使用,才能校验通过。

二、在A项目中配置两个redis数据源,且使用lettuce作为连接池,就需要手动配置两个数据源。一个用于A项目的业务操作,另外一个用于B项目的satoken使用。

 一、A项目的redis数据源配置

1、配置好RedisTemplate,以及lettuce链接池,当使用两个redis数据源的时候springboot底层对redis的自动装配会失效,需要手动处理。

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
    public RedisAutoConfiguration() {
    }

    // 这里会由于导入一个RedisTemplate而不再自动导入
    @Bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}
@Bean(name = "redisTemplate")
public RedisTemplate<String, Serializable> redisTemplate(@Qualifier("connectionFactory") RedisConnectionFactory connectionFactory) {
    RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    // 值采用JSON序列化
    GenericFastJsonRedisSerializer genericFastJsonRedisSerializer = new GenericFastJsonRedisSerializer();
    redisTemplate.setValueSerializer(genericFastJsonRedisSerializer);
    redisTemplate.setHashValueSerializer(genericFastJsonRedisSerializer);
    redisTemplate.setDefaultSerializer(genericFastJsonRedisSerializer);
    redisTemplate.setConnectionFactory(connectionFactory);
    return redisTemplate;
}
@Bean("connectionFactory") // 这里使用这个名字主要是SaTokenDaoRedisJackson底层需要
@Qualifier("connectionFactory")
public RedisConnectionFactory firstRedisConnectionFactory() {

    RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
    redisStandaloneConfiguration.setHostName(host);
    redisStandaloneConfiguration.setPort(port);
    redisStandaloneConfiguration.setDatabase(Integer.parseInt(database));
    redisStandaloneConfiguration.setPassword(password);

    GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
    poolConfig.setMaxIdle(maxIdle);
    poolConfig.setMinIdle(minIdle);
    poolConfig.setMaxTotal(maxActive);
    poolConfig.setMaxWaitMillis(Long.parseLong(maxWait));

    LettucePoolingClientConfiguration lettucePoolingClientConfiguration = LettucePoolingClientConfiguration
            .builder()
            .commandTimeout(Duration.ofMillis(Long.parseLong(timeout)))
            .poolConfig(poolConfig)
            .build();
    return new LettuceConnectionFactory(redisStandaloneConfiguration, lettucePoolingClientConfiguration);
}

二、B项目中的redis 数据源配置

@Bean("secondRedisConnectionFactory")
@Qualifier("secondRedisConnectionFactory")
public RedisConnectionFactory secondRedisConnectionFactory() {
    RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
    redisStandaloneConfiguration.setHostName(secondHost);
    redisStandaloneConfiguration.setPort(secondPort);
    redisStandaloneConfiguration.setDatabase(Integer.parseInt(secondDatabase));
    redisStandaloneConfiguration.setPassword(secondPassword);

    GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
    poolConfig.setMaxIdle(maxIdle);
    poolConfig.setMinIdle(minIdle);
    poolConfig.setMaxTotal(maxActive);
    poolConfig.setMaxWaitMillis(Long.parseLong(maxWait));

    LettucePoolingClientConfiguration lettucePoolingClientConfiguration = LettucePoolingClientConfiguration
            .builder()
            .commandTimeout(Duration.ofMillis(Long.parseLong(timeout)))
            .poolConfig(poolConfig)
            .build();
    return new LettuceConnectionFactory(redisStandaloneConfiguration, lettucePoolingClientConfiguration);
}


@Bean(name = "secondStringRedisTemplate")
@Qualifier("secondStringRedisTemplate")
public StringRedisTemplate secondStringRedisTemplate(@Qualifier("secondRedisConnectionFactory") RedisConnectionFactory lettuceConnectionFactory) {
    StringRedisTemplate stringTemplate = new StringRedisTemplate();
    stringTemplate.setConnectionFactory(lettuceConnectionFactory);
    stringTemplate.afterPropertiesSet();
    return stringTemplate;
}

@Bean(name = "secondRedisTemplate")
@Qualifier("secondRedisTemplate")
public RedisTemplate<String, Object> secondRedisTemplate(@Qualifier("secondRedisConnectionFactory") RedisConnectionFactory lettuceConnectionFactory) {
    StringRedisSerializer keySerializer = new StringRedisSerializer();
    GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(lettuceConnectionFactory);
    template.setKeySerializer(keySerializer);
    template.setHashKeySerializer(keySerializer);
    template.setValueSerializer(valueSerializer);
    template.setHashValueSerializer(valueSerializer);
    template.afterPropertiesSet();
    return template;
}

三、替换SaTokenDaoRedisJackson使用的redis配置

1、SaTokenDaoRedisJackson底层源码

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package cn.dev33.satoken.dao;

import cn.dev33.satoken.strategy.SaStrategy;
import cn.dev33.satoken.util.SaFoxUtil;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.lang.reflect.Field;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;

@Component
public class SaTokenDaoRedisJackson implements SaTokenDao {
    public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
    public static final String DATE_PATTERN = "yyyy-MM-dd";
    public static final String TIME_PATTERN = "HH:mm:ss";
    public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
    public ObjectMapper objectMapper;
    public StringRedisTemplate stringRedisTemplate;
    public RedisTemplate<String, Object> objectRedisTemplate;
    public boolean isInit;

    public SaTokenDaoRedisJackson() {
    }

    @Autowired // 优先从容器中获取RedisConnectionFactory这个类型的bean,这个时候会有两个,然后这个注解会按照connectionFactory这个名字取获取就能获取到A项目的
    public void init(RedisConnectionFactory connectionFactory) {
        if (!this.isInit) {
            StringRedisSerializer keySerializer = new StringRedisSerializer();
            GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();

            try {
                Field field = GenericJackson2JsonRedisSerializer.class.getDeclaredField("mapper");
                field.setAccessible(true);
                this.objectMapper = (ObjectMapper)field.get(valueSerializer);
                this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
                JavaTimeModule timeModule = new JavaTimeModule();
                timeModule.addSerializer(new LocalDateTimeSerializer(DATE_TIME_FORMATTER));
                timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER));
                timeModule.addSerializer(new LocalDateSerializer(DATE_FORMATTER));
                timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DATE_FORMATTER));
                timeModule.addSerializer(new LocalTimeSerializer(TIME_FORMATTER));
                timeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(TIME_FORMATTER));
                this.objectMapper.registerModule(timeModule);
                SaStrategy.instance.createSession = (sessionId) -> {
                    return new SaSessionForJacksonCustomized(sessionId);
                };
            } catch (Exception var6) {
                System.err.println(var6.getMessage());
            }

            StringRedisTemplate stringTemplate = new StringRedisTemplate();
            stringTemplate.setConnectionFactory(connectionFactory);
            stringTemplate.afterPropertiesSet();
            RedisTemplate<String, Object> template = new RedisTemplate();
            template.setConnectionFactory(connectionFactory);
            template.setKeySerializer(keySerializer);
            template.setHashKeySerializer(keySerializer);
            template.setValueSerializer(valueSerializer);
            template.setHashValueSerializer(valueSerializer);
            template.afterPropertiesSet();
            this.stringRedisTemplate = stringTemplate;
            this.objectRedisTemplate = template;
            this.isInit = true;
        }
    }

    public String get(String key) {
        return (String)this.stringRedisTemplate.opsForValue().get(key);
    }

    public void set(String key, String value, long timeout) {
        if (timeout != 0L && timeout > -2L) {
            if (timeout == -1L) {
                this.stringRedisTemplate.opsForValue().set(key, value);
            } else {
                this.stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
            }

        }
    }

    public void update(String key, String value) {
        long expire = this.getTimeout(key);
        if (expire != -2L) {
            this.set(key, value, expire);
        }
    }

    public void delete(String key) {
        this.stringRedisTemplate.delete(key);
    }

    public long getTimeout(String key) {
        return this.stringRedisTemplate.getExpire(key);
    }

    public void updateTimeout(String key, long timeout) {
        if (timeout == -1L) {
            long expire = this.getTimeout(key);
            if (expire != -1L) {
                this.set(key, this.get(key), timeout);
            }

        } else {
            this.stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
        }
    }

    public Object getObject(String key) {
        return this.objectRedisTemplate.opsForValue().get(key);
    }

    public void setObject(String key, Object object, long timeout) {
        if (timeout != 0L && timeout > -2L) {
            if (timeout == -1L) {
                this.objectRedisTemplate.opsForValue().set(key, object);
            } else {
                this.objectRedisTemplate.opsForValue().set(key, object, timeout, TimeUnit.SECONDS);
            }

        }
    }

    public void updateObject(String key, Object object) {
        long expire = this.getObjectTimeout(key);
        if (expire != -2L) {
            this.setObject(key, object, expire);
        }
    }

    public void deleteObject(String key) {
        this.objectRedisTemplate.delete(key);
    }

    public long getObjectTimeout(String key) {
        return this.objectRedisTemplate.getExpire(key);
    }

    public void updateObjectTimeout(String key, long timeout) {
        if (timeout == -1L) {
            long expire = this.getObjectTimeout(key);
            if (expire != -1L) {
                this.setObject(key, this.getObject(key), timeout);
            }

        } else {
            this.objectRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
        }
    }

    public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {
        Set<String> keys = this.stringRedisTemplate.keys(prefix + "*" + keyword + "*");
        List<String> list = new ArrayList(keys);
        return SaFoxUtil.searchList(list, start, size, sortType);
    }
}
 

 2、替换SaTokenDaoRedisJackson中的redis配置

也就是替换他的这两个成员变量,换成B的即可

    public StringRedisTemplate stringRedisTemplate;
    public RedisTemplate<String, Object> objectRedisTemplate;


@Configuration
public class SaTokenDaoRedisConfig {

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.database}")
    private String database;
    @Value("${spring.redis.second.host}")
    private String secondHost;
    @Value("${spring.redis.second.port}")
    private int secondPort;
    @Value("${spring.redis.second.password}")
    private String secondPassword;
    @Value("${spring.redis.second.database}")
    private String secondDatabase;
    @Value("${spring.redis.lettuce.pool.max-active}")
    private int maxActive;
    @Value("${spring.redis.lettuce.pool.max-wait}")
    private String maxWait;
    @Value("${spring.redis.lettuce.pool.max-idle}")
    private int maxIdle;
    @Value("${spring.redis.lettuce.pool.min-idle}")
    private int minIdle;
    @Value("${spring.redis.timeout}")
    private String  timeout;

    @Autowired
    @Qualifier("secondStringRedisTemplate")
    private StringRedisTemplate secondStringRedisTemplate;

    @Autowired
    @Qualifier("secondRedisTemplate")
    private RedisTemplate<String, Object> secondRedisTemplate;
    @Autowired
    private SaTokenDaoRedisJackson saTokenDaoRedisJackson;


    @PostConstruct // 项目启动初始化的时候,也就是bean以及创建完成,并赋值后在初始化的时候重新给这两个成员变量赋值
    public void init() {
        saTokenDaoRedisJackson.stringRedisTemplate = secondStringRedisTemplate;
        saTokenDaoRedisJackson.objectRedisTemplate = secondRedisTemplate;
    }

    @Bean("secondRedisConnectionFactory")
    @Qualifier("secondRedisConnectionFactory")
    public RedisConnectionFactory secondRedisConnectionFactory() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(secondHost);
        redisStandaloneConfiguration.setPort(secondPort);
        redisStandaloneConfiguration.setDatabase(Integer.parseInt(secondDatabase));
        redisStandaloneConfiguration.setPassword(secondPassword);

        GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
        poolConfig.setMaxIdle(maxIdle);
        poolConfig.setMinIdle(minIdle);
        poolConfig.setMaxTotal(maxActive);
        poolConfig.setMaxWaitMillis(Long.parseLong(maxWait));

        LettucePoolingClientConfiguration lettucePoolingClientConfiguration = LettucePoolingClientConfiguration
                .builder()
                .commandTimeout(Duration.ofMillis(Long.parseLong(timeout)))
                .poolConfig(poolConfig)
                .build();
        return new LettuceConnectionFactory(redisStandaloneConfiguration, lettucePoolingClientConfiguration);
    }


    @Bean(name = "secondStringRedisTemplate")
    @Qualifier("secondStringRedisTemplate")
    public StringRedisTemplate secondStringRedisTemplate(@Qualifier("secondRedisConnectionFactory") RedisConnectionFactory lettuceConnectionFactory) {
        StringRedisTemplate stringTemplate = new StringRedisTemplate();
        stringTemplate.setConnectionFactory(lettuceConnectionFactory);
        stringTemplate.afterPropertiesSet();
        return stringTemplate;
    }

    @Bean(name = "secondRedisTemplate")
    @Qualifier("secondRedisTemplate")
    public RedisTemplate<String, Object> secondRedisTemplate(@Qualifier("secondRedisConnectionFactory") RedisConnectionFactory lettuceConnectionFactory) {
        StringRedisSerializer keySerializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(lettuceConnectionFactory);
        template.setKeySerializer(keySerializer);
        template.setHashKeySerializer(keySerializer);
        template.setValueSerializer(valueSerializer);
        template.setHashValueSerializer(valueSerializer);
        template.afterPropertiesSet();
        return template;
    }

    @Bean("connectionFactory")
    @Qualifier("connectionFactory")
    public RedisConnectionFactory firstRedisConnectionFactory() {

        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPort(port);
        redisStandaloneConfiguration.setDatabase(Integer.parseInt(database));
        redisStandaloneConfiguration.setPassword(password);

        GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
        poolConfig.setMaxIdle(maxIdle);
        poolConfig.setMinIdle(minIdle);
        poolConfig.setMaxTotal(maxActive);
        poolConfig.setMaxWaitMillis(Long.parseLong(maxWait));

        LettucePoolingClientConfiguration lettucePoolingClientConfiguration = LettucePoolingClientConfiguration
                .builder()
                .commandTimeout(Duration.ofMillis(Long.parseLong(timeout)))
                .poolConfig(poolConfig)
                .build();
        return new LettuceConnectionFactory(redisStandaloneConfiguration, lettucePoolingClientConfiguration);
    }
}
 

以上设置完,就可以让A项目处理A项目的缓存数据,也可以让A项目通过B项目中的satoken的校验 

三、权限认证也需要重新配置,否则也无法通过,这是由于所有权限和菜单数据都放在B项目中导致的。

StpInterface

权限数据加载源接口
在使用权限校验 API 之前,你必须实现此接口,告诉框架哪些用户拥有哪些权限。 框架默认不对数据进行缓存,如果你的数据是从数据库中读取的,一般情况下你需要手动实现数据的缓存读写。

@Component
public class StpInterfaceImpl implements StpInterface {

    @Resource
    private SysMenuService sysMenuService;

    @Resource
    private SysUserRoleService sysUserRoleService;

    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        if (!loginType.equals(StpUtil.TYPE)) {
            return null;
        }
        long userId = SaFoxUtil.getValueByType(loginId, long.class);
        return sysMenuService.permissionList(userId);
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        if (!loginType.equals(StpUtil.TYPE)) {
            return null;
        }
        long userId = SaFoxUtil.getValueByType(loginId, long.class);
        return sysUserRoleService.roleList(userId);
    }
}
 

一、@SaCheckPermission(AuthKeyConstant.REPORT_LIST)

 每个接口都会使用这样一个注解,并且标注上对应的权限标识。SaCheckPermission源码如下

package cn.dev33.satoken.annotation;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
/**
 * 权限认证:必须具有指定权限才能进入该方法 
 * <p> 可标注在函数、类上(效果等同于标注在此类的所有方法上) 
 * @author kong
 *
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface SaCheckPermission {
 
	/**
	 * 需要校验的权限码
	 * @return 需要校验的权限码
	 */
	String [] value() default {};
 
	/**
	 * 验证模式:AND | OR,默认AND
	 * @return 验证模式
	 */
	SaMode mode() default SaMode.AND;
 
    /**
     * 多账号体系下所属的账号体系标识 
     * @return see note 
     */
	String type() default "";
 
	/**
	 * 在权限认证不通过时的次要选择,两者只要其一认证成功即可通过校验  
	 * 
	 * <p> 
	 * 	例1:@SaCheckPermission(value="user-add", orRole="admin"),
	 * 	代表本次请求只要具有 user-add权限 或 admin角色 其一即可通过校验 
	 * </p>
	 * 
	 * <p> 
	 * 	例2: orRole = {"admin", "manager", "staff"},具有三个角色其一即可 <br> 
	 * 	例3: orRole = {"admin, manager, staff"},必须三个角色同时具备 
	 * </p>
	 * 
	 * @return /
	 */
	String[] orRole() default {};
	
}

二、校验原理

每次请求对应接口时,只要有@SaCheckPermission这个注解,底层都会回调StpInterface实现类实现的方法,拿到这个用户的所有权限,然后与当前注解配置的权限码进行比较,如果这个用户有这个权限,比对就会通过,然后进入接口获取对应数据,否者会抛出对应异常。

注意:

也正是由于这个原因,获取权限的数据库需要配置为B项目的数据库,从B项目中获取用户的权限。

三、跨域问题解决方式

在和前段进行本地联调时出现跨域问题,导致拿不到请求头上面携带的token数据。

一、什么情况下会发生跨域 

简单理解,就是你在 A 域名下的页面,去调用 B 域名的接口,浏览器感觉你这次调用可能是不安全的请求行为,于是它需要用 cors 安全策略来确认一下这个请求是由用户真实的意愿发出的,而不是被 csrf 伪造请求攻击偷偷发送的。(这么说只是为了方便大家理解,不是特别严谨,实际上同域名下部分情形也会出现跨域问题)

请仔细理解上面这段话,因为它说明了两点:

  • 跨域不是后端接口对前端浏览器的限制,而是前端浏览器对用户的限制。
  • 跨域不是在保护后端接口免受攻击,而是浏览器在保护用户,避免用户发送自己不想发送的请求。

一般情况下,我们会碰到三种跨域场景:

1、本地页面调用测试服务器,只在项目开发阶段会有跨域问题。
2、使用 header 头提交 token,产生的跨域问题。
3、使用第三方 Cookie 提交 token,产生的跨域问题。

二、什么是OPTIONS请求?

浏览器在发送请求之前会先发送一个OPTIONS请求,来校验是否允许跨域访问,校验的结果存放在头信息的Access-Control-Allow-Origin,因此解决跨域也就是设置头部信息。当第一个请求通过了,才会真正的发送第二个请求,也即是请求接口获取数据。

三、解决方案

一、第一种方案

public class SaInterceptorImpl extends SaInterceptor {

    public SaInterceptorImpl() {
    }

    public SaInterceptorImpl(SaParamFunction<Object> auth) {
        super(auth);
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 如果这个请求是一个预检请求,或者它登录了则接口放行,然后会让浏览器进行第二次请求
        if ("OPTIONS".equals(request.getMethod())) {
            return true;
        }
        return super.preHandle(request, response, handler);
    }
}

    /**
     * 注册拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
        registry.addInterceptor(new SaInterceptorImpl(handle -> StpUtil.checkLogin())).addPathPatterns("/**").excludePathPatterns(excludePathList);
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("PUT", "DELETE", "GET", "POST")
                .allowedHeaders("*")
                .exposedHeaders("access-control-allow-headers",
                        "access-control-allow-methods",
                        "access-control-allow-origin",
                        "access-control-max-age",
                        "X-Frame-Options")
                .allowCredentials(false).maxAge(3600);
    }

 第二个方案

在SaTokenConfig里面直接再加入一个即可

@Configuration
public class SaTokenConfig implements WebMvcConfigurer {

    /**
     * 白名单
     */
    private final List<String> excludePathList = Arrays.asList(
          
    );

    /**
     * 注册拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
        registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin())).addPathPatterns("/**").excludePathPatterns(excludePathList);
    }

}


 @Bean
    public SaServletFilter getSaServletFilter() {
        return new SaServletFilter()
                
                // 指定 [拦截路由] 与 [放行路由]
                .addInclude("/**").addExclude("/favicon.ico")
                
                // 认证函数: 每次请求执行 
                .setAuth(obj -> {
                    SaManager.getLog().debug("----- 请求path={}  提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());
                    // ...
                })
                
                // 异常处理函数:每次认证函数发生异常时执行此函数 
                .setError(e -> {
                    return SaResult.error(e.getMessage());
                })
                
                // 前置函数:在每次认证函数之前执行
                .setBeforeAuth(obj -> {
                    SaHolder.getResponse()
 
                    // ---------- 设置跨域响应头 ----------
                    // 允许指定域访问跨域资源
                    .setHeader("Access-Control-Allow-Origin", "*")
                    // 允许所有请求方式
                    .setHeader("Access-Control-Allow-Methods", "*")
                    // 允许的header参数
                    .setHeader("Access-Control-Allow-Headers", "*")
                    // 有效时间
                    .setHeader("Access-Control-Max-Age", "3600")
                    ;
                    
                    // 如果是预检请求,则立即返回到前端 
                    SaRouter.match(SaHttpMethod.OPTIONS)
                        .free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
                        .back();
                })
                ;
    }
 

  • 21
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
sa-token可以通过以下配置来校验token: 1. 配置token名称:在yml配置文件中,可以通过设置`sa-token.token-name`来指定token的名称,例如`sa-token.token-name: X-Token`\[1\]。 2. 配置token有效期:可以通过设置`sa-token.timeout`来指定token的有效期,单位为秒,默认为30天,可以设置为-1代表永不过期,例如`sa-token.timeout: 2592000`\[1\]。 3. 配置token临时有效期:可以通过设置`sa-token.activity-timeout`来指定token的临时有效期,即在指定时间内无操作就视为token过期,单位为秒,默认为-1,表示不设置临时有效期,例如`sa-token.activity-timeout: -1`\[1\]。 4. 配置是否允许同一账号并发登录:可以通过设置`sa-token.is-concurrent`来指定是否允许同一账号并发登录,为true时允许一起登录,为false时新登录会挤掉旧登录,例如`sa-token.is-concurrent: true`\[1\]。 5. 配置是否共用一个token:可以通过设置`sa-token.is-share`来指定在多人登录同一账号时,是否共用一个token,为true时所有登录共用一个token,为false时每次登录会新建一个token,例如`sa-token.is-share: true`\[1\]。 6. 配置是否输出操作日志:可以通过设置`sa-token.is-log`来指定是否输出操作日志,为true时输出操作日志,为false时不输出操作日志,例如`sa-token.is-log: false`\[1\]。 7. 配置是否使用cookie保存token:可以通过设置`sa-token.is-read-cookie`来指定是否使用cookie保存token,为true时使用cookie保存token,为false时不使用cookie保存token,例如`sa-token.is-read-cookie: false`\[1\]。 8. 配置是否使用head保存token:可以通过设置`sa-token.is-read-head`来指定是否使用head保存token,为true时使用head保存token,为false时不使用head保存token,例如`sa-token.is-read-head: true`\[1\]。 通过以上配置,sa-token可以根据配置的规则来校验token的有效性。 #### 引用[.reference_title] - *1* [最简单的权限验证实现——使用Sa-Token进行权限验证](https://blog.csdn.net/lp840312696/article/details/127072424)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [探索 Sa-Token (一) SpringBoot 集成 Sa-Token](https://blog.csdn.net/weixin_38982591/article/details/126764928)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值