文档章节

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

qwzh110
 qwzh110
发布于 2018/02/08 17:51
字数 1607
阅读 5055
收藏 45

应用如果做负载均衡,集群间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<String, Object> redisTemplate() {
        RedisTemplate<String, Object> 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<String, Cache> caches = new ConcurrentHashMap<String, Cache>();
    // 注入Spring的缓存管理器
    @Autowired
    private org.springframework.cache.CacheManager cacheManager;

    @Override
    public <K, V> Cache<K, V> 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<K, V> implements Cache<K, V> {

    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<K> keys() {
        throw new RuntimeException("");
    }

    @Override
    public Collection<V> 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<Object, Object> 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缓存。

© 著作权归作者所有

qwzh110

qwzh110

粉丝 11
博文 3
码字总数 3126
作品 0
深圳
程序员
私信 提问
加载中

评论(17)

qwzh110
qwzh110 博主

引用来自“又在写BUG了”的评论

我想看下您com.bkcell.security.common.kit.PropKit个类的代码,
可是https://github.com/qwzhang01/bkcell_security您这个网站挂掉了,
能把com.bkcell.security.common.kit.PropKit的代码贴出来吗,谢谢了
package com.bkcell.security.common.kit;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringContextKit implements ApplicationContextAware {
public static ApplicationContext applicationContext;

public static Object getBean(String name) {
return applicationContext.getBean(name);
}

public static T getBean(String name, Class requiredType) {
return applicationContext.getBean(name, requiredType);
}

public static T getBean(Class requiredType) throws BeansException {
T result = applicationContext.getBean(requiredType);
return result;
}

public static boolean containsBean(String name) {
return applicationContext.containsBean(name);
}

public static boolean isSingleton(String name) {
return applicationC
qwzh110
qwzh110 博主

引用来自“又在写BUG了”的评论

还有,SpringContextKit又是什么东东
熊蝶,没有挂掉哦,是github,应该可以访问的
又在写BUG了
还有,SpringContextKit又是什么东东
又在写BUG了
我想看下您com.bkcell.security.common.kit.PropKit个类的代码,
可是https://github.com/qwzhang01/bkcell_security您这个网站挂掉了,
能把com.bkcell.security.common.kit.PropKit的代码贴出来吗,谢谢了
qwzh110
qwzh110 博主

引用来自“繁华-落幕”的评论

<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.0</version>
</dependency>
博主适用过SpringBoot2.X,然后pom引入的时上方的shiro配置,来整合过嘛
最近用了2.0,不过想集成springcloud全家桶,尤其是oauth2.0,所以用的spring security
淡若悠然
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.0</version>
</dependency>
博主适用过SpringBoot2.X,然后pom引入的时上方的shiro配置,来整合过嘛
qwzh110
qwzh110 博主

引用来自“二十岁以后”的评论

边看博客 边看源码 一边做 ,码云上的代码比本节讲的多好多配置,懵逼的状态啊
是的,源码实现的是具体的功能。很多配置好多人都写文章介绍过,所以没有做具体介绍。有时间我把别人介绍的文章链接整理一下,附在这里。😄
二十岁以后
二十岁以后
边看博客 边看源码 一边做 ,码云上的代码比本节讲的多好多配置,懵逼的状态啊
二十岁以后
二十岁以后
找到了
二十岁以后
二十岁以后
com.bkcell.security.common.kit.PropKit; 这个是哪个依赖的包呢?找半天没找到
Shiro系列(3) - What is shiro?

什么是shiro? Shiro是apache的一个开源权限管理的框架,它实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架 使用shiro来实现权限管理,可以非常有效的提高...

风间影月
2017/10/25
0
0
SpringBoot集成Shiro三个渐进式项目以及Shiro功能介绍

版权声明:本文为谙忆原创文章,转载请附上本文链接,谢谢。 https://blog.csdn.net/qq_26525215/article/details/82499114 首先,本篇博客的目的的重点是这里介绍的三个SpringBoot集成Shiro...

谙忆
2018/09/07
0
0
从权限控制到shiro框架的应用

说明:本文很多观点和内容来自互联网以及各种资料,如果侵犯了您的权益,请及时联系我,我会删除相关内容。 权限管理 基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范...

神秘的寇先森
2018/01/01
0
0
SpringBoot 优雅的整合 Shiro

Apache Shiro是一个功能强大且易于使用的Java安全框架,可执行身份验证,授权,加密和会话管理。借助Shiro易于理解的API,您可以快速轻松地保护任何应用程序 - 从最小的移动应用程序到最大的...

木云凌
03/19
387
0
SpringMvc + Shiro[数据库存权限] 配置 ;[附git.oschina的项目地址]

一 shiro简介 apache shiro 是功能强大并且容易集成的开源权限框架,它能够完成认证、授权、加密、会话管理等功能。认证和授权为权限控制的核心,简单来说,“认证”就是证明“你是谁?” We...

王庭
2015/10/28
11K
2

没有更多内容

加载失败,请刷新页面

加载更多

查看线上日志常用命令

cat 命令(文本输出命令) 通常查找出错误日志 cat error.log | grep 'nick' , 这时候我们要输出当前这个日志的前后几行: 显示file文件里匹配nick那行以及上下5行 cat error.log | grep -C ...

xiaolyuh
36分钟前
5
0
六、Java设计模式之工厂方法

工厂方法定义: 定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类,工厂方法让类的实例化推迟到子类中进行 类型:创建型 工厂方法-使用场景: 创建对象需要大量重复的代码 ...

东风破2019
43分钟前
5
0
win服务器管理遇到的一系列问题记录

有些小伙伴在使用iis7远程桌面管理工具的时候总是会遇到一系列的问题,下面就是为大家介绍一下服务器日常管理过程中出现的问题及我的解决办法和心得。希望能帮到大家。   拒绝服务器重新启...

1717197346
50分钟前
6
0
flutter 剪切板 复制粘贴

复制粘贴功能 import 'package:flutter/services.dart'; Clipboard.setData(ClipboardData(text:_text));Clipboard.getData;...

zdglf
53分钟前
4
0
如何保证消息的可靠性传输?或者说,如何处理消息丢失的问题?

面试题 如何保证消息的可靠性传输?或者说,如何处理消息丢失的问题? 面试官心理分析 这个是肯定的,用 MQ 有个基本原则,就是数据不能多一条,也不能少一条,不能多,就是前面说的重复消费...

米兜
53分钟前
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部