redisson集成spring-session和shiro实现分布式session

8 篇文章 1 订阅
6 篇文章 0 订阅

一、pom

       <!-- Shiro权限验证 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.6.0</version>
        </dependency>
        <!-- 使用Shiro<Tag>-->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>
         <!--redisson-->
        <!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.16.3</version>
        </dependency>
        <!--session-redis-->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

二、使用redisson的方式实现shiro的cache和cachemanager

  • RedissonShiroCacheManager
@Component
public class RedissonShiroCacheManager implements CacheManager, Initializable {
    private boolean allowNullValues = true;
    private Codec codec = new JsonJacksonCodec();
    private RedissonClient redisson;
    private String configLocation;
    private Map<String, CacheConfig> configMap = new ConcurrentHashMap<>();
    private ConcurrentMap<String, Cache> instanceMap = new ConcurrentHashMap<>();
    public RedissonShiroCacheManager(){}
    public RedissonShiroCacheManager(RedissonClient redisson){
        this(redisson, (String)null, null);
    }
    public RedissonShiroCacheManager(RedissonClient redisson, Map<String, ? extends CacheConfig> config) {
        this(redisson, config, null);
    }
    public RedissonShiroCacheManager(RedissonClient redisson, Map<String, ? extends CacheConfig> config, Codec codec) {
        this.redisson = redisson;
        this.configMap = (Map<String, CacheConfig>) config;
        if (codec != null) {
            this.codec = codec;
        }
    }
    public RedissonShiroCacheManager(RedissonClient redisson, String configLocation) {
        this(redisson, configLocation, null);
    }
    public RedissonShiroCacheManager(RedissonClient redisson, String configLocation, Codec codec) {
        this.redisson = redisson;
        this.configLocation = configLocation;
        if (codec != null) {
            this.codec = codec;
        }
    }
    protected CacheConfig createDefaultConfig() {
        return new CacheConfig();
    }

    @Override
    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        Cache<K, V> cache = this.instanceMap.get(name);
        if (cache != null) {
            return cache;
        }
        CacheConfig config = this.configMap.get(name);
        if (config == null) {
            config = createDefaultConfig();
            configMap.put(name, config);
        }
        if (config.getMaxIdleTime() == 0 && config.getTTL() == 0 && config.getMaxSize() == 0) {
            return createMap(name, config);
        }
        return createMapCache(name, config);
    }
    private <K, V> Cache<K, V> createMap(String name, CacheConfig config) {
        RMap<K, Object> map = getMap(name, config);
        Cache<K, V> cache = new RedissonShiroCache<>(map, this.allowNullValues);
        Cache<K, V> oldCache = this.instanceMap.putIfAbsent(name, cache);
        if (oldCache != null) {
            cache = oldCache;
        }
        return cache;
    }
    protected <K> RMap<K, Object> getMap(String name, CacheConfig config) {
        if (this.codec != null) {
            return  this.redisson.getMap(name, this.codec);
        }
        return this.redisson.getMap(name);
    }
    private <K, V> Cache<K, V> createMapCache(String name, CacheConfig config) {
        RMapCache<K, Object> map = getMapCache(name, config);
        Cache<K, V> cache = new RedissonShiroCache<>(map, config, this.allowNullValues);
        Cache<K, V> oldCache = this.instanceMap.putIfAbsent(name, cache);
        if (oldCache != null) {
            cache = oldCache;
        } else {
            map.setMaxSize(config.getMaxSize());
        }
        return cache;
    }
    protected <K> RMapCache<K, Object> getMapCache(String name, CacheConfig config) {
        if (this.codec != null) {
            return this.redisson.getMapCache(name, this.codec);
        }
        return redisson.getMapCache(name);
    }
    @Override
    public void init() {
        if (this.configLocation == null) {
            return;
        }
        try {
            this.configMap = (Map<String, CacheConfig>) CacheConfig.fromJSON(ResourceUtils.getInputStreamForPath(this.configLocation));
        } catch (IOException e) {
            // try to read yaml
            try {
                this.configMap = (Map<String, CacheConfig>) CacheConfig.fromYAML(ResourceUtils.getInputStreamForPath(this.configLocation));
            } catch (IOException e1) {
                throw new IllegalArgumentException(
                        "Could not parse cache configuration at [" + configLocation + "]", e1);
            }
        }
    }
    public void setConfig(Map<String, ? extends CacheConfig> config) {
        this.configMap = (Map<String, CacheConfig>) config;
    }

    public RedissonClient getRedisson() {
        return redisson;
    }

    public void setRedisson(RedissonClient redisson) {
        this.redisson = redisson;
    }

    public Codec getCodec() {
        return codec;
    }

    public void setCodec(Codec codec) {
        this.codec = codec;
    }

    public String getConfigLocation() {
        return configLocation;
    }

    public void setConfigLocation(String configLocation) {
        this.configLocation = configLocation;
    }

    public boolean isAllowNullValues() {
        return allowNullValues;
    }

    public void setAllowNullValues(boolean allowNullValues) {
        this.allowNullValues = allowNullValues;
    }
}
  • RedissonShiroCache
public class RedissonShiroCache<K, V> implements Cache<K, V> {

    private RMapCache<K, Object> mapCache;

    private final RMap<K, Object> map;

    private CacheConfig config;

    private final boolean allowNullValues;

    private final AtomicLong hits = new AtomicLong();

    private final AtomicLong misses = new AtomicLong();

    public RedissonShiroCache(RMapCache<K, Object> mapCache, CacheConfig config, boolean allowNullValues) {
        this.mapCache = mapCache;
        this.map = mapCache;
        this.config = config;
        this.allowNullValues = allowNullValues;
    }

    public RedissonShiroCache(RMap<K, Object> map, boolean allowNullValues) {
        this.map = map;
        this.allowNullValues = allowNullValues;
    }

    @Override
    public V get(K key) throws CacheException {
        Object value = this.map.get(key);
        if (value == null) {
            addCacheMiss();
        } else {
            addCacheHit();
        }
        return fromStoreValue(value);
    }

    @Override
    public V put(K key, V value) throws CacheException {
        Object previous;
        if (!allowNullValues && value == null) {
            if (mapCache != null) {
                previous = mapCache.remove(key);
            } else {
                previous = map.remove(key);
            }
            return fromStoreValue(previous);
        }

        Object val = toStoreValue(value);
        if (mapCache != null) {
            previous = mapCache.put(key, val, config.getTTL(), TimeUnit.MILLISECONDS,
                    config.getMaxIdleTime(), TimeUnit.MILLISECONDS);
        } else {
            previous = map.put(key, val);
        }
        return fromStoreValue(previous);
    }

    public void fastPut(K key, V value) throws CacheException {
        if (!allowNullValues && value == null) {
            if (mapCache != null) {
                mapCache.fastRemove(key);
            } else {
                map.fastRemove(key);
            }
            return;
        }

        Object val = toStoreValue(value);
        if (mapCache != null) {
            mapCache.fastPut(key, val, config.getTTL(), TimeUnit.MILLISECONDS,
                    config.getMaxIdleTime(), TimeUnit.MILLISECONDS);
        } else {
            map.fastPut(key, val);
        }
    }

    public V putIfAbsent(K key, V value) throws CacheException {
        Object previous;
        if (!allowNullValues && value == null) {
            if (mapCache != null) {
                previous = mapCache.get(key);
            } else {
                previous = map.get(key);
            }
            return fromStoreValue(previous);
        }

        Object val = toStoreValue(value);
        if (mapCache != null) {
            previous = mapCache.putIfAbsent(key, val, config.getTTL(), TimeUnit.MILLISECONDS,
                    config.getMaxIdleTime(), TimeUnit.MILLISECONDS);
        } else {
            previous = map.putIfAbsent(key, val);
        }
        return fromStoreValue(previous);
    }

    public boolean fastPutIfAbsent(K key, V value) throws CacheException {
        if (!allowNullValues && value == null) {
            return false;
        }

        Object val = toStoreValue(value);
        if (mapCache != null) {
            return mapCache.fastPutIfAbsent(key, val, config.getTTL(), TimeUnit.MILLISECONDS,
                    config.getMaxIdleTime(), TimeUnit.MILLISECONDS);
        } else {
            return map.fastPutIfAbsent(key, val);
        }
    }

    @Override
    public V remove(K key) throws CacheException {
        Object previous = this.map.remove(key);
        return fromStoreValue(previous);
    }

    public long fastRemove(K ... keys) {
        return this.map.fastRemove(keys);
    }

    @Override
    public void clear() throws CacheException {
        this.map.clear();
    }

    @Override
    public int size() {
        return this.map.size();
    }

    @Override
    public Set<K> keys() {
        return this.map.readAllKeySet();
    }

    @Override
    public Collection<V> values() {
        Collection<Object> innerValues = this.map.readAllValues();
        Collection<V> res = new ArrayList<>(innerValues.size());
        for (Object val : innerValues) {
            res.add(fromStoreValue(val));
        }
        return res;
    }

    protected V fromStoreValue(Object storeValue) {
        if (storeValue instanceof NullValue) {
            return null;
        }
        return (V) storeValue;
    }
    protected Object toStoreValue(V userValue) {
        if (userValue == null) {
            return NullValue.INSTANCE;
        }
        return userValue;
    }
    /** The number of get requests that were satisfied by the cache.
     * @return the number of hits
     */
    long getCacheHits(){
        return this.hits.get();
    }
    /** A miss is a get request that is not satisfied.
     * @return the number of misses
     */
    long getCacheMisses(){
        return this.misses.get();
    }
    private void addCacheHit(){
        this.hits.incrementAndGet();
    }
    private void addCacheMiss(){
        this.misses.incrementAndGet();
    }
}

三、开启springsession,接管shiro-session

@SpringBootApplication
//使用EnableRedisHttpSession注解开启spring分布式session,该类的作用是配置org.springframework.session.web.http.SessionRepositoryFilter进行请求拦截
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)
@EnableCaching
public class GeApplication {
    public static void main(String[] args) {
        SpringApplication.run(GeApplication.class, args);
    }
}
  • shiroconfig类的配置
public class ShiroConfig {
    @Autowired
    RedissonClient redissonClient;
    /**
     * ServletContainerSessionManager 类中有一个方法是isServletContainerSessions(),返回的是true.
     * DefaultWebSessionManager类中有一个方法是isServletContainerSessions(),返回是false。
     * 因为实现了Spring Session,代理了所有Servlet里的session,所以这里的session一定是Servlet能控制的,否则无法实现Spring session共享。
     */
    @Bean
    public SessionManager sessionManager(){
        return new ServletContainerSessionManager();
    }
    /**
     * 注入这个是是为了在thymeleaf中使用shiro的自定义tag。
     */
    @Bean(name = "shiroDialect")
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }

    /**
     * 地址过滤器
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 设置登录url
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 设置主页url
        shiroFilterFactoryBean.setSuccessUrl("/shiro");
        // 设置未授权的url
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        // 注销登录
        LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
        filtersMap.put("logout", shiroLogoutFilter());
        shiroFilterFactoryBean.setFilters(filtersMap);

        filterChainDefinitionMap.put("/loginOut", "logout");
        // 开放登录接口
        filterChainDefinitionMap.put("/doLogin", "anon");
        ........
        // 其余url全部拦截,必须放在最后
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }


    public ShiroLogoutFilter shiroLogoutFilter(){
        ShiroLogoutFilter shiroLogoutFilter = new ShiroLogoutFilter(redissonClient);
        //配置登出后重定向的地址,等出后配置跳转到登录接口
        shiroLogoutFilter.setRedirectUrl("/login");
        return shiroLogoutFilter;
    }

    @Bean("authenticator")
    public SessionsSecurityManager securityManager() {
        SessionsSecurityManager securityManager = new DefaultWebSecurityManager();
        //设置认证realm
        securityManager.setRealm(loginRealm());
        // 设置记住我功能
//        securityManager.setRememberMeManager(rememberMeManager());
        // 设置会话管理器
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }


    @Bean
    public RememberMeManager rememberMeManager() {
        CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
        //注入自定义cookie(主要是设置寿命, 默认的一年太长)
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        simpleCookie.setHttpOnly(true);
        //设置RememberMe的cookie有效期为7天
        simpleCookie.setMaxAge(604800);
        rememberMeManager.setCookie(simpleCookie);
        //手动设置对称加密秘钥,防止重启系统后系统生成新的随机秘钥,防止导致客户端cookie无效
        rememberMeManager.setCipherKey(Base64.decode("6ZmI6I2j3Y+R1aSn5BOlAA=="));
        return rememberMeManager;
    }

    @Bean
    public LoginRealm loginRealm() {
        LoginRealm loginRealm = new LoginRealm();
        //开启缓存处理
        loginRealm.setCacheManager(new RedissonShiroCacheManager(redissonClient));
        //开启全局缓存
        //loginRealm.setCachingEnabled(true);
//        //开启认证缓存
//        loginRelam.setAuthenticationCachingEnabled(true);
        //loginRealm.setAuthenticationCacheName("authenticafdfdfdtionCache");
        //认证授权缓存
//        loginRealm.setAuthorizationCachingEnabled(true);
//        loginRealm.setAuthorizationCacheName("AuthorizationCache");
        return loginRealm;
    }

    /**
     * 以下是为了能够使用@RequiresPermission()等标签
     * 这里命名为advisorAutoProxyCreatorShiro是因为advisorAutoProxyCreator会与druid冲突
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreatorShiro() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * LifecycleBeanPostProcessor将Initializable和Destroyable的实现类统一在其内部
     * 自动分别调用了Initializable.init()和Destroyable.destroy()方法,从而达到管理shiro bean生命周期的目的。
     */
    @Bean
    public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }
}

四、自定义shiro的登出行为,做一些springsession中与用户有关的缓存操作

  • 自定义shiro只需要实现LogoutFilter,重写prehandle方法即可
public class ShiroLogoutFilter extends LogoutFilter {
    private final RedissonClient redissonClient;
    /**
     * @param request
     * @param response
     * @throws Exception
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = getSubject(request,response);
        try {
            HttpServletRequest httpReq = (HttpServletRequest) request;
            HttpSession session = httpReq.getSession();
            //根据spring session的信息,删除用户的缓存
            String sessionKey="spring:session:sessions:" + session.getId();
            String expires="spring:session:sessions:expires:" + session.getId();
            redissonClient.getBucket(sessionKey).delete();
            redissonClient.getBucket(expires).delete();
        } catch (Throwable t) {
            t.printStackTrace();
        }
        //登出
        subject.logout();
        //获取登出后重定向到的地址
        String redirectUrl = getRedirectUrl(request,response,subject);
        //重定向
        issueRedirect(request,response,redirectUrl);
        //配置登出后重定向的地址,等出后配置跳转到登录接口
        return false;
    }
    public ShiroLogoutFilter(RedissonClient redissonClient){
        this.redissonClient=redissonClient;
    }
}

五、最后,redissonnclient用于操作redis

@Configuration
public class RedissonConfig {
    @Bean(destroyMethod="shutdown")
    public RedissonClient getRedissonClient() throws IOException {
        ResourceLoader loader = new DefaultResourceLoader();
        Resource resource = loader.getResource("redisson.yml");
        Config config = Config.fromYAML(resource.getInputStream());
        return Redisson.create(config);
    }
}
  • redisson.yml
singleServerConfig:
  idleConnectionTimeout: 10000  #连接空闲超时,单位:毫秒 默认:10000
  connectTimeout: 10000  #连接超时,单位:毫秒。默认:10000
  timeout: 3000 #命令等待超时,单位:毫秒 默认:3000
  retryAttempts: 3 #命令失败重试次数
  retryInterval: 1500 #命令重试发送时间间隔,单位:毫秒
 # lockWatchdogTimeout: 30000 #监控锁的看门狗超时,单位:毫秒  该参数只适用于分布式锁的加锁请求中未明确使用leaseTimeout参数的情况。如果该看门口未使用lockWatchdogTimeout去重新调整一个分布式锁的lockWatchdogTimeout超时,那么这个锁将变为失效状态。这个参数可以用来避免由Redisson客户端节点宕机或其他原因造成死锁的情况。
  password: xxxxxxxxx
  clientName: null #客户端名称
  subscriptionsPerConnection: 5 #单个连接最大订阅数量
  address: "redis://ip:port"
  subscriptionConnectionMinimumIdleSize: 1 #从节点发布和订阅连接的最小空闲连接数
  subscriptionConnectionPoolSize: 50 #从节点发布和订阅连接池大小
  # 集群模式下不支持该选项
  database: 10
  dnsMonitoringInterval: 5000 #DNS监控间隔,单位:毫秒 在启用该功能以后,Redisson将会监测DNS的变化情况
  sslEnableEndpointIdentification: true #启用SSL终端识别,默认为true
threads: 0 #线程池数量 默认值: 当前处理核数量 * 2
nettyThreads: 0 #Netty线程池数量 默认值: 当前处理核数量 * 2 ,这个线程池数量是在一个Redisson实例内,被其创建的所有分布式数据类型和服务,以及底层客户端所一同共享的线程池里保存的线程数量。
codec: #编码 默认值:org.redisson.codec.JsonJacksonCodec,Redisson的对象编码类是用于将对象进行序列化和反序列化,以实现对该对象在Redis里的读取和存储
  !<org.redisson.codec.JsonJacksonCodec> {}
"transportMode": #传输模式 默认:TransportMode.NIO linux系统下可以使用RPOLL,性能高
  "NIO"

在这里插入图片描述

六、总结

通过spring-session集成shiro,我们可以实现用户权限控制,认证缓存,将session的存储位置由tomcat等web容器剥离至redis或者mysql中进行持久化,这样即使微服务中某台机器宕机,重启,也不会丢失用户信息,进一步提高系统的健壮性。同时,本例子只在在单机redis的情况下使用,各位可以进一步将redis扩展到redis集群,在redisson的帮助下,使用起来也非常方便

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值