使用Redis缓存Shiro授权认证信息,搭建集群权限系统

阅读更多

应用如果做负载均衡,集群间session需要共享,如果session没有共享,用户登录系统以后session保存在登录的应用里面,其他应用里面没有session,没有登陆状态,访问会失败。下面介绍一个SpringBoot下面基于Shiro的session共享方案。

方案的全部代码在码云上面。https://github.com/qwzhang01/bkcell_security

思路

  • 使用Shiro托管应用session
  • 使用Redis管理Shiro缓存

实现步骤

  1. 设置项目缓存为Redis,这样Spring项目的缓存就都会存在Redis
  2. 设置应用session由Shiro托管
  3. 实现Shiro的缓存管理器CacheManger接口,将Spring应用缓存管理器注入shiro缓存管理器,这样shiro的缓存都由Spring处理
  4. 实现Shiro的Cache接口,将Spring的缓存工具类注入,使Shiro对缓存信息的存取由Spring的缓存实现
  5. 实现Shiro的EnterpriseCacheSessionDAO类,重写Shiro对于session的CRUD,使用重写的Shiro的Cache接口,对session的CRUD在Redis中处理

具体实现

1. 配置Redis

在application.properties文件中添加如下内容,配置Redis的host 密码 端口号等

spring.redis.host=192.168.10.135
spring.redis.port=6379  
spring.redis.password=000000

添加Redis缓存配置类


import com.canyou.bkcell.common.kit.PropKit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    @Autowired
    private RedisConnectionFactory factory;

    @Override
    @Bean
    public KeyGenerator keyGenerator() {
        return (target, method, params) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getName());
            sb.append(method.getName());
            for (Object obj : params) {
                sb.append(obj.toString());
            }
            return sb.toString();
        };
    }

    @Bean
    public CacheManager cacheManager(RedisTemplate redisTemplate) {
        RedisCacheManager rcm = new RedisCacheManager(redisTemplate);
        rcm.setDefaultExpiration(PropKit.getInt("spring.redis.timeout") * 60);
        return rcm;
    }

    @Bean
    public RedisTemplate redisTemplate() {
        RedisTemplate redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer(this.getClass().getClassLoader()));
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }
}

通过以上两步,应用的缓存实现使用Redis。

2. 配置Shiro,由Shiro托管应用session

在Shiro的SecurityManager中注入SessionManager

    @Bean
    public DefaultWebSessionManager sessionManager() {
        ShiroSessionManager sessionManager = new ShiroSessionManager();
        sessionManager.setSessionDAO(sessionDao());
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        //设置session过期时间为1小时(单位:毫秒),默认为30分钟
        sessionManager.setGlobalSessionTimeout(PropKit.getInt("spring.redis.session.timeout") * 60 * 1000);
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setCacheManager(shiroRedisCacheManager());
        sessionManager.setSessionValidationSchedulerEnabled(false);
        Cookie sessionIdCookie = sessionManager.getSessionIdCookie();
        sessionIdCookie.setPath("/");
        sessionIdCookie.setName("csid");
        sessionManager.setSessionIdCookieEnabled(true);
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(authRealm());
        securityManager.setCacheManager(shiroRedisCacheManager());
        // 设置通过shiro管理应用session
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }

3. 实现Shiro的缓存管理器CacheManger接口,将Spring应用缓存管理器注入shiro缓存管理器,这样shiro的缓存都由Spring处理


import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

@Component
@Qualifier("shiroRedisCacheManager")
public class ShiroRedisCacheManager implements CacheManager {

    private final ConcurrentMap caches = new ConcurrentHashMap();
    // 注入Spring的缓存管理器
    @Autowired
    private org.springframework.cache.CacheManager cacheManager;

    @Override
    public  Cache getCache(String name) throws CacheException {
        Cache cache = caches.get(name);
        if (cache == null) {
            org.springframework.cache.Cache springCache = cacheManager.getCache(name);
            // 通过spring的缓存管理器,获取缓存,将缓存注入Redis的缓存中
            cache = new ShiroRedisCache(springCache);
            caches.put(name, cache);
        }
        return cache;
    }
}

4. Shiro的缓存类


import com.canyou.bkcell.common.kit.ByteKit;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;

import java.util.Collection;
import java.util.Set;

public class ShiroRedisCache implements Cache {

    private String keyPrefix = "shiro_redis_session:";
    private org.springframework.cache.Cache cache;

    public ShiroRedisCache(org.springframework.cache.Cache springCache) {
        this.cache = springCache;
    }

    public void setKeyPrefix(String keyPrefix) {
        this.keyPrefix = keyPrefix;
    }

    private String genKey(K key) {
        return (keyPrefix + new String(ByteKit.toByte(key)));
    }

    @Override
    public V get(K key) throws CacheException {
        if (key == null) {
            return null;
        }
        org.springframework.cache.Cache.ValueWrapper valueWrapper = cache.get(genKey(key));
        if (valueWrapper == null) {
            return null;
        }
        V v = (V) valueWrapper.get();
        return v;
    }

    @Override
    public V put(K key, V value) throws CacheException {
        cache.put(genKey(key), value);
        return value;
    }

    @Override
    public V remove(K key) throws CacheException {
        V v = (V) cache.get(genKey(key)).get();
        cache.evict(genKey(key));
        return v;
    }

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

    @Override
    public int size() {
        throw new RuntimeException("");
    }

    @Override
    public Set keys() {
        throw new RuntimeException("");
    }

    @Override
    public Collection values() {
        throw new RuntimeException("");
    }
}

5. 重写SessionDAO,实现session的CRUD功能


import com.canyou.bkcell.common.kit.ServletKit;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;

import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;

public class RedisSessionDao extends EnterpriseCacheSessionDAO {

    private Cache cache() {
        Cache cache = getCacheManager().getCache(this.getClass().getName());
        return cache;
    }

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = super.doCreate(session);
        cache().put(sessionId.toString(), session);
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        Session session = null;
        HttpServletRequest request = ServletKit.getRequest();
        if (request != null){
            String uri = request.getServletPath();
            if (ServletKit.isStaticFile(uri)){
                return null;
            }
            session = (Session)request.getAttribute("session_"+sessionId);
        }
        if (session == null) {
            session = super.doReadSession(sessionId);
        }
        if (session == null) {
            session = (Session) cache().get(sessionId.toString());
        }
        return session;
    }

    @Override
    protected void doUpdate(Session session) {
        HttpServletRequest request = ServletKit.getRequest();
        if (request != null) {
            String uri = request.getServletPath();
            if (ServletKit.isStaticFile(uri)) {
                return;
            }
        }
        super.doUpdate(session);
        cache().put(session.getId().toString(), session);
    }

    @Override
    protected void doDelete(Session session) {
        super.doDelete(session);
        cache().remove(session.getId().toString());
    }
}

致此,完成使用Redis缓存Shiro授权认证信息,搭建集群权限系统。

6. 简单优化,减少session的redis读取次数

shiro的session存在redis里面后,一次Request对session有很多次读取操作,同时静态资源的访问等都会读取session,虽然redis的性能与内存一样,但是redis毕竟存在网络传输的过程。因此在sessionDAO里面优化的session读操作,减少不必要的在redis读取次数。

1) 优化思路

  • 过滤静态资源,请求静态资源的时候不读取session
  • 读取session先通过Request域获取,如果Request域中不存时,再通过Redis读取获取session。

2)实现步骤及具体实现

    1.添加Servlet工具类,实现在任意位置获取Request,添加判断请求uri是否是静态资源方法。


import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.resource.ResourceUrlProvider;

import javax.servlet.http.HttpServletRequest;

public class ServletKit {
    public static HttpServletRequest getRequest() {
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }

    public static boolean isStaticFile(String uri) {
        ResourceUrlProvider resourceUrlProvider = SpringContextKit.getBean(ResourceUrlProvider.class);
        String staticUri = resourceUrlProvider.getForLookupPath(uri);
        return staticUri != null;
    }
}

    2.在sessionDao的doReadSession操作中过滤静态资源代码,请求uri如果是静态资源,session返回null;读取session操作先获取Request域中的session,如果获取不到,再读取Redis缓存。

@Override
    protected Session doReadSession(Serializable sessionId) {
        Session session = null;
        // 获取本次Request
        HttpServletRequest request = ServletKit.getRequest();
        if (request != null){
            String uri = request.getServletPath();
            // 过滤静态资源请求
            if (ServletKit.isStaticFile(uri)){
                return null;
            }
            // 在Request域中获取session
            session = (Session)request.getAttribute("session_"+sessionId);
        }
        if (session == null) {
            session = super.doReadSession(sessionId);
        }
        if (session == null) {
            session = (Session) cache().get(sessionId.toString());
        }
        return session;
    }

7. Shiro缓存使用Spring缓存方案优缺点简单总结

  • 优点,缓存使用Spring自身的缓存,Redis读写以及序列化等全部由Spring实现,简化开发,方便Spring项目之间的整合。
  • 缺点,不同版本Spring或者不同技术栈之间的应用做session共享,可能会由于Redis读写方案不一致或序列化方案不一致等问题,无法兼容。
  • 二备方案,session共享如果要兼容老板本项目,可以重写shiro的缓存管理器(上面代码中的ShiroRedisCacheManager类)和缓存(ShiroRedisCache)两个类,使用兼容老版本Redis读写及序列化的方法代替Spring缓存。

转载于:https://my.oschina.net/qwzhang01/blog/1620339

你可能感兴趣的:(使用Redis缓存Shiro授权认证信息,搭建集群权限系统)