Shiro集成redis实现session共享

耿志义
2023-12-01

一. 搭建redis基本环境

①加入redis相关依赖

		<!-- 支持redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- Spring Session 分布式事务 ,在登录时使用 -->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <!-- spring-session -->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-core</artifactId>
        </dependency>

②写入redis的application.yml配置文件

#配置连接redis的参数
spring:
  redis:
    cluster:
    #配置redis集群节点
      nodes: 92.168.10.140:8001,192.168.10.140:8002,192.168.10.140:8003,192.168.10.140:8004,192.168.10.140:8005,192.168.10.140:8006
  session:
    store-type: redis #存放session的存储方式
    redis:
      flush-mode: on_save #配置session刷新方式
    timeout: 30m #设置session超时时间

这样redis的基本环境就搭建完成了。

二. shiro认证授权实现

①导入shiro相关依赖

		<dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>1.4.0</version>
        </dependency>

②配置shiro

Shiro 配置类-》ShiroConfig.java
–负责配置shiro的资源配置、安全管理器对象、和管理自定义的realm对象

@Configuration
public class ShiroConfig {

    //创建shiroFilter(shiro拦截器对象--负责拦截所有请求)
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
        //给Filter设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
       
        //配置系统受限资源
        //配置系统公共资源
        Map<String,String> map=new HashMap<String,String>();
        
        /*资源配置
        map.put("/index","authc");//authc受限资源需要认证才能访问(“/**”)表示所有资源
		map.put("/","anon");//anon设置公共资源
		map.put("/**","authc");//存在‘/**’时未受限的资源必须写在上面,否则将被覆盖
        //默认认证界面路径(认证失败会自动重定向返回到该路径界面)
        */
        
        shiroFilterFactoryBean.setLoginUrl("/login");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        return shiroFilterFactoryBean;
    }

    //创建安全管理对象
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //给安全管理器设置Realm
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }
    //创建自定义Realm
    @Bean
    public Realm getRealm(){
        CustomerRealm customerRealm = new CustomerRealm();
        //修改凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //设置加密算法为Md5
        credentialsMatcher.setHashAlgorithmName("MD5");
        //设置散列次数
        credentialsMatcher.setHashIterations(1024);

        customerRealm.setCredentialsMatcher(credentialsMatcher);

        //设置缓存管理器
        customerRealm.setCacheManager(new RedisCacheManager());
        //开启缓存
        customerRealm.setCachingEnabled(true);
        //开启认证缓存并指定缓存名称
        customerRealm.setAuthenticationCachingEnabled(true);
        customerRealm.setAuthenticationCacheName("AuthenticationCache");
        //开启授权缓存并指定缓存名称
        customerRealm.setAuthorizationCachingEnabled(true);
        customerRealm.setAuthorizationCacheName("AuthorizationCache");
        return customerRealm;
    }
}

自定义Realm-》 CustomerRealm.java
用来处理用户的认证与授权


public class CustomerRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {//授权
        //获取身份信息
//        String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
//        //根据身份信息查取数据库获取 权限 和 角色 信息
//        if(primaryPrincipal.equals("xwm")){  //(模拟操作)
//            SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();
//            simpleAuthorizationInfo.addRole("admin");//为用户添加角色
//            simpleAuthorizationInfo.addStringPermission("admin:*:*");//admin角色下的所有资源的所有权限
//            return simpleAuthorizationInfo; //返回对象
//        }
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {//认证
        String principal = (String)authenticationToken.getPrincipal();//获取用户名
        UserServiceImpl userService=(UserServiceImpl) ApplicationContextUtils.getContext("UserService");//获取容器里的Bean
        Patient patient = userService.loginUser(principal);//通过姓名获取用户信息
        if(patient!=null){//用户名,用户已加密的密码,盐,当前对象
            System.out.println("-将patient对象存入session-");//会同时将将patient对象存入session中
            return new SimpleAuthenticationInfo(patient,patient.getPatientpwd(),//密码
                    new CustomerByteSource(patient.getPsalt()),//盐
                    this.getName());//当前realm
        }
        return null;
    }
}

用来为用户加密产生加密盐值的工具类
可以产生随机的字符串,可以加强加密的安全性。

public class SaltUtils {
    public static String getSalt(int n){//快捷键:Ctrl+Shift+U==>可将选中的字母大写转小写
        char[] chars = "ABCDEFJHIJKLMNOPQRSTUVWXYZabcdefjhijklmnopqrstuvwxyz0987654321!@#$%^&*():?><|~/}{][`".toCharArray();
        StringBuilder str=new StringBuilder();
        for (int i = 0; i < n; i++) {
            char aChar = chars[new Random().nextInt(chars.length)];
            str.append(aChar);
        }
        return str.toString();
    }

    public static void main(String[] args) {//盐值生成测试
        String str=getSalt(8);
        System.out.println(str);
        Md5Hash md5Hash=new Md5Hash("123",str,1024);
        System.out.println(md5Hash.toHex());
    }
}

自定义盐值实现类(由于原本的盐实现是没有序列化的,在redis缓存过程中会出现序列化错误,所以需要自定义一个salt实现,实现序列化接口。)

//自定义salt实现  实现序列化接口
public class CustomerByteSource implements ByteSource, Serializable {

    private byte[] bytes;
    private String cachedHex;
    private String cachedBase64;

    public CustomerByteSource() {

    }

    public CustomerByteSource(byte[] bytes) {
        this.bytes = bytes;
    }

    public CustomerByteSource(char[] chars) {
        this.bytes = CodecSupport.toBytes(chars);
    }

    public CustomerByteSource(String string) {
        this.bytes = CodecSupport.toBytes(string);
    }

    public CustomerByteSource(ByteSource source) {
        this.bytes = source.getBytes();
    }

    public CustomerByteSource(File file) {
        this.bytes = (new CustomerByteSource.BytesHelper()).getBytes(file);
    }

    public CustomerByteSource(InputStream stream) {
        this.bytes = (new CustomerByteSource.BytesHelper()).getBytes(stream);
    }

    public static boolean isCompatible(Object o) {
        return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
    }

    @Override
    public byte[] getBytes() {
        return this.bytes;
    }

    @Override
    public boolean isEmpty() {
        return this.bytes == null || this.bytes.length == 0;
    }

    @Override
    public String toHex() {
        if (this.cachedHex == null) {
            this.cachedHex = Hex.encodeToString(this.getBytes());
        }

        return this.cachedHex;
    }

    @Override
    public String toBase64() {
        if (this.cachedBase64 == null) {
            this.cachedBase64 = Base64.encodeToString(this.getBytes());
        }

        return this.cachedBase64;
    }

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

    @Override
    public int hashCode() {
        return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (o instanceof ByteSource) {
            ByteSource bs = (ByteSource) o;
            return Arrays.equals(this.getBytes(), bs.getBytes());
        } else {
            return false;
        }
    }

    private static final class BytesHelper extends CodecSupport {
        private BytesHelper() {
        }

        public byte[] getBytes(File file) {
            return this.toBytes(file);
        }

        public byte[] getBytes(InputStream stream) {
            return this.toBytes(stream);
        }
    }
}

③redis缓存管理器配置

通过实现CacheManager接口为shiro配置redis缓存管理器。

public class RedisCacheManager implements CacheManager {
    @Override
    public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
        System.out.println("缓存名称: "+cacheName);
        return new RedisCache<K,V>(cacheName);
    }
}

自定义redis缓存实现RedisCache类,内部都是操作redis数据库的方法。

//存储键值对不应该使用注解方式而应该从容器获取对象,因为这种方式只能存储值为字符串的键值对。

public class RedisCache<K,V> implements Cache<K,V> {
    private String cacheName;

    public RedisCache() {
    }

    public RedisCache(String cacheName) {
        this.cacheName = cacheName;
    }

    @Override
    public V get(K k) throws CacheException {
        System.out.println("获取缓存:"+ k);
        return (V) getRedisTemplate().opsForHash().get(this.cacheName,k.toString());
    }

    @Override
    public V put(K k, V v) throws CacheException {
        System.out.println("设置缓存key: "+k+" value:"+v);
        getRedisTemplate().opsForHash().put(this.cacheName,k.toString(),v);
        return null;
    }

    @Override
    public V remove(K k) throws CacheException {
        System.out.println("移除缓存key: "+k);
        return (V) getRedisTemplate().opsForHash().delete(this.cacheName,k.toString());
    }

    @Override
    public void clear() throws CacheException {
        getRedisTemplate().delete(this.cacheName);
    }

    @Override
    public int size() {
        return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
    }

    @Override
    public Set<K> keys() {
        return getRedisTemplate().opsForHash().keys(this.cacheName);
    }

    @Override
    public Collection<V> values() {
        return getRedisTemplate().opsForHash().values(this.cacheName);
    }

    private RedisTemplate getRedisTemplate(){//获取redis操作对象 RedisTemplate 
    	//通过工具类ApplicationContextUtils指定Bean名来获取容器中指定的Bean对象
        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getContext("redisTemplate");
        return redisTemplate;
    }
}

用来从容器里获取Bean对象的工具类ApplicationContextUtils

@Component
public class ApplicationContextUtils implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override//系统会自动调用该类经工厂传如该函数(由程序自动调用)
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context=applicationContext;
    }

    //根据bean名字获取工厂里指定的bean对象
    public static Object getContext(String name){
        Object bean = context.getBean(name);
        return bean;
    }
}

④redis缓存序列化方式配置

redis配置类 用来配置redis个类型的序列化方式(如果不配置该项,程序会出现反序列化失败的错误)
注:在分布式项目里,如果想要这种方式来实现session共享则必须两端即双方的redis缓存序列化方式都必须一样否则程序会出现序列化的异常错误。从而使程序无法正常运行。

@Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 使用Jackson2JsonRedisSerialize 替换默认序列化
        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);

        // 设置value的序列化规则和 key的序列化规则
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

三. 测试

在分布式项目中,A模块内通过session.setAttribute("xxx",xxx)将数据存入redis中,再通过ModelAndView view = new ModelAndView(new RedirectView("请求地址"))来重定向到B模块,通过测试B模块的session.getAttribute("xxx")来判断共享是否成功,如果有值则代表session共享成功。

 类似资料: