sprintboot使用spring-security包,缓存内存与redis共存

2023年7月17日10:06:59

项目修改需求描述

项目需要使用分布式缓存机制,但是使用@Cacheable原始仅配置了内存版的,故此次需要改成redis用以支持多应用模式的。
项目中如果直接改成redis的,存在一个问题。如果内存对象同一类,比如都是String的list对象,存的key值又都是"code",会把缓存给冲掉,所以需要对redis的做后缀处理。又因为只有一个redis服务器,所以需要对缓存做项目的前缀处理。
还希望能支持历史项目,做到内存与redis共存,所以顺便就做了CacheManager处理。

代码

使用到的依赖包

		<!-- 缓存、权限 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-cache</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
		<!-- json -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.47</version>
		</dependency>
		<!-- redis -->
		<dependency>
			<groupId>org.springframework.data</groupId>
			<artifactId>spring-data-redis</artifactId>
		</dependency>
		<dependency>
			<groupId>io.lettuce</groupId>
			<artifactId>lettuce-core</artifactId>
			<version>5.1.0.M1</version>
		</dependency>

代码处理

首先如果希望程序支持缓存需要注解

@EnableCaching

然后在application.yml里增加redis的配置

spring:
  application:
    name:app
  redis:
    database: 0
    host: XX.XX.XX.XX
    port: 6379
    password:
    pool:
      max-total: 100
      block-when-exhausted: false
    timeout: 5000  

增加一个redis处理类RedisConfig,此处做了redis前缀的处理

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import com.fasterxml.jackson.annotation.JsonAutoDetect;

import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;

@Configuration
public class RedisConfig {
	
	@Autowired
	private PrefixRedisSerializer stringRedisSerializer;
 
    @Autowired  
    private Environment env;  

//    @Bean
//    //这块如果不使用redisTemplate.opsForHash(),可以不用使用
//    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
//        // 为了开发方便,一般使用<String, Object>
//        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
//        redisTemplate.setConnectionFactory(redisConnectionFactory);
//
//        // Json序列化配置
//        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//        ObjectMapper objectMapper = new ObjectMapper();
//        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
//        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
//        
//        //StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//
//        // 设置key的序列化方式
//        redisTemplate.setKeySerializer(stringRedisSerializer);
//
//        // 设置hashkey的序列化方式
//        redisTemplate.setHashKeySerializer(stringRedisSerializer);
//
//        // 设置value的序列化方式
        template.setValueSerializer(stringRedisSerializer);
//        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
//
//        // 设置hashvalue的序列化方式
        template.setHashValueSerializer(stringRedisSerializer);
//        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
//        
        template.setBeanClassLoader(this.getClass().getClassLoader());
//
        redisTemplate.afterPropertiesSet();
//
//        return redisTemplate;
//    }

    @Bean("redisCacheManager")
    //@Primary
    public RedisCacheManager myRedisCacheManager(RedisConnectionFactory redisConnectionFactory){
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        
        
        //String prefixStr = env.getProperty("spring.application.name") + ":";
        //if(!'prod'.equals(env.getActiveProfiles()[0])) {
        //	prefixStr += env.getActiveProfiles()[0]  + ":";
        //}
        
        // 配置序列化(解决乱码的问题)
//        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(this.getClass().getClassLoader())
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
				.computePrefixWith(cacheName -> {
					String prefixStr = env.getProperty("spring.application.name") + ":";
					if (!"prod".equals(env.getActiveProfiles()[0])) {
						prefixStr += env.getActiveProfiles()[0] + ":";
					}
					prefixStr += cacheName + ":";
					return prefixStr;
				})
        		//.prefixKeysWith(prefixStr)
				
				//这块如果不使用computePrefixWith,也可以把redisSerializer改成stringRedisSerializer
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();

        RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config).build();

        return cacheManager;
    }

    @Bean("defaultCacheManager")
    @Primary
    public ConcurrentMapCacheManager cacheManager() {
    	ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
		return cacheManager;
	}
	
}

注:RedisCacheManager 和 RedisTemplate 里的json处理要注意一致性。
这边配置了2个cacheManager:(ConcurrentMapCacheManager 默认缓存、RedisCacheManager redis缓存)
必须要默认一个cacheManager,哪个是默认的就在哪个bean上加注解@Primary
如果不写会报下列错误:

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
19:42:21.462 [restartedMain] ERROR o.s.b.SpringApplication:858 - Application run failed
java.lang.IllegalStateException: No CacheResolver specified, and no unique bean of type CacheManager found. Mark one as primary or declare a specific CacheManager to use.
	at org.springframework.cache.interceptor.CacheAspectSupport.afterSingletonsInstantiated(CacheAspectSupport.java:223)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:866)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:877)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:142)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:316)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49)

下面这个类是针对加redis-key前缀用的,不用可以不考虑

import java.nio.charset.StandardCharsets;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import com.google.common.base.Strings;

@Configuration
public class PrefixRedisSerializer extends StringRedisSerializer {

	@Autowired
	private Environment env;
	
//	public PrefixRedisSerializer() {
//		super(StandardCharsets.UTF_8);
//	}
	
	private String cachePrefix;
	
	private String getPrefix() {
		if(!Strings.isNullOrEmpty(cachePrefix)) {
			return cachePrefix;
		}
		//这块是去application配置文件获取项目名称和环境配置,可以按照自己需要加
		String prefixStr = env.getProperty("spring.application.name") + ":";
		if (!"prod".equals(env.getActiveProfiles()[0])) {
			prefixStr += env.getActiveProfiles()[0] + ":";
		}
		cachePrefix = prefixStr;
		return cachePrefix;
	}

    @Override
    public byte[] serialize(String string) {
    	return (string == null ? null : (getPrefix() + string).getBytes(StandardCharsets.UTF_8));
    }
    
    @Override
    public String deserialize(byte[] bytes) {
		return (bytes == null ? null : new String(bytes, StandardCharsets.UTF_8).replaceFirst(getPrefix(), ""));
    }
    
}

增加MyKeyGenerator,做redis后缀处理

CachingConfigurerSupport 这个接口默认就有@Primary功能,无需额外配置

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.lang.reflect.Method;

@Configuration
public class MyKeyGeneratorConfig extends CachingConfigurerSupport {

	private final Logger logger = LoggerFactory.getLogger(this.getClass());
	
    @Bean
//    @Primary
//    @Override
    public KeyGenerator myKeyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
//                String re= String.format("%s::%s(%s)",target.getClass().getName(),method.getName(),
//                        CollectionUtils.arrayToList(params));//Arrays.asList(params)
            	
        		StringBuilder sb = new StringBuilder();
        		String valueStr = null;
        		if (method.isAnnotationPresent(Cacheable.class)) {
        			Cacheable ca = method.getAnnotation(Cacheable.class);
        			if (ca.value() != null) {
        				valueStr = ca.value()[0];
        			}
        		} else if(method.isAnnotationPresent(CacheEvict.class)) {
        			CacheEvict ce = method.getAnnotation(CacheEvict.class);
        			if (ce.value() != null) {
        				valueStr = ce.value()[0];
        			}
        		}
        		
        		if (valueStr != null) {
        			sb.append(valueStr + ":");
        		} else {
        			sb.append(target.getClass().getName() + ":");
        			sb.append(method.getName() + ":");
        		}
        		for (Object obj : params) {
        			sb.append(obj.toString() + ":");
        		}
//        		return sb.toString();
        		String re = sb.toString();
            	
            	
                logger.debug("缓存生成的key:{}。",re);
                return re;
            }
        };
    }
	
}

缓存类处理:

	@Cacheable(value = "XXX", keyGenerator = "myKeyGenerator", cacheManager = "redisCacheManager")
	public List<String> getXXXX(String userCode) {
		XXXXXXXXXX
	}

对应的@CacheEvict 记得也把该属性加上,如果不是默认的话: keyGenerator = “myKeyGenerator”

权限类处理:

针对权限类controller增加注解判断,没权限直接报错
@PreAuthorize(“hasAuthority(‘privileges’)”)

权限缓存加载类:

	@Cacheable("privileges")
	private List<GrantedAuthority> makeAuth(String userCode) {
		List<MyGrantedAuthority> privileges = XXX;
		return privileges;
	}

权限类:


import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.util.Assert;

public class MyGrantedAuthority  implements GrantedAuthority {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private String authority;
	
    public MyGrantedAuthority() {
    	super();
    }

	public MyGrantedAuthority(String authority) {
		Assert.hasText(authority, "A granted authority textual representation is required");
		this.authority = authority;
	}

	@Override
	public String getAuthority() {
		return authority;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}

		if (obj instanceof MyGrantedAuthority) {
			return authority.equals(((MyGrantedAuthority) obj).authority);
		}

		return false;
	}

	@Override
	public int hashCode() {
		return this.authority.hashCode();
	}

	@Override
	public String toString() {
		return this.authority;
	}
}

异常问题解决

如果项目中存在LocalDateTime会报错:

Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (byte[])
[truncated 981 bytes]; line: 1, column: 360] (through reference chain: ["createdTime"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1452)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1028)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1297)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
at com.fasterxml.jackson.databind.deser.impl

  • 作者:miheal_near
  • 原文链接:https://blog.csdn.net/weixin_47136046/article/details/113133595
    更新时间:2023年7月17日10:06:59 ,共 12163 字。