Shiro安全框架基于Redis的分布式集群方案

  • Shiro安全框架基于Redis的分布式集群方案已关闭评论
  • 115,052 views
  • A+
所属分类:系统架构

前段时间做了一个市场推广相关的项目,安全框架使用的是Shiro,缓存框架使用的是spring-data-redis。为了使用户7x24小时访问,决定把项目由单机升级为分布式部署架构。但是安全框架shiro只有单机存储的SessionDao,尽管Shrio有基于Ehcache-rmi的组播/广播实现,然而集群的分布往往是跨网段的,甚至是跨地域的,所以寻求新的方案。

运行环境

Nginx + Tomcat7(3台) + JDK1.7

项目架构图

Shiro安全框架基于Redis的分布式集群方案

Shiro安全框架基于Redis的分布式集群方案

项目实现

pom.xml引入配置(版本自行更换):

  1. <dependency>
  2. <groupId>org.springframework.data</groupId>
  3. <artifactId>spring-data-redis</artifactId>
  4. <version>1.7.10.RELEASE</version>
  5. </dependency>

redis.properties配置:

  1. #============================#
  2. #===== redis sttings ====#
  3. #============================#
  4. redis.host=127.0.0.1
  5. redis.port=6379
  6. redis.password=123456
  7. #单位秒
  8. redis.expire=1800
  9. redis.timeout=2000
  10. redis.usepool=true
  11. redis.database=1

spring-context-redis.xml配置:

  1. <!-- redis 配置 -->
  2. <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig" />
  3. <bean id="jedisConnectionFactory"
  4. class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
  5. <property name="hostName" value="${redis.host}" />
  6. <property name="port" value="${redis.port}" />
  7. <property name="password" value="${redis.password}" />
  8. <property name="timeout" value="${redis.timeout}" />
  9. <property name="poolConfig" ref="jedisPoolConfig" />
  10. <property name="usePool" value="true" />
  11. </bean>
  12. <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
  13. <property name="connectionFactory" ref="jedisConnectionFactory" />
  14. </bean>

RedisSessionDAO配置(重写 AbstractSessionDAO):

  1. import java.io.Serializable;
  2. import java.util.Collection;
  3. import java.util.HashSet;
  4. import java.util.Set;
  5. import java.util.concurrent.TimeUnit;
  6. import org.apache.shiro.session.Session;
  7. import org.apache.shiro.session.UnknownSessionException;
  8. import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
  9. import org.slf4j.Logger;
  10. import org.slf4j.LoggerFactory;
  11. import org.springframework.data.redis.core.RedisTemplate;
  12. /**
  13. * 重写 AbstractSessionDAO
  14. * 使用Redis缓存
  15. * 创建者 张志朋
  16. * 创建时间 2018年1月10日
  17. */
  18. public class RedisSessionDAO extends AbstractSessionDAO {
  19. private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class);
  20. /**
  21. * shiro-redis的session对象前缀
  22. */
  23. private RedisTemplate<String, Object> redisTemplate;
  24. // 0 - never expire
  25. private int expire = 3600000;
  26. /**
  27. * The Redis key prefix for the sessions
  28. */
  29. private String keyPrefix = "shiro_market_redis_session:";
  30. @Override
  31. public void update(Session session) throws UnknownSessionException {
  32. this.saveSession(session);
  33. }
  34. /**
  35. * save session
  36. * @param session
  37. * @throws UnknownSessionException
  38. */
  39. private void saveSession(Session session) throws UnknownSessionException{
  40. if(session == null || session.getId() == null){
  41. logger.error("session or session id is null");
  42. return;
  43. }
  44. String key = session.getId().toString();
  45. session.setTimeout(expire);
  46. redisTemplate.opsForValue().set(keyPrefix+key, session, expire, TimeUnit.MILLISECONDS);
  47. }
  48. @Override
  49. public void delete(Session session) {
  50. if(session == null || session.getId() == null){
  51. logger.error("session or session id is null");
  52. return;
  53. }
  54. redisTemplate.delete(keyPrefix+session.getId().toString());
  55. }
  56. @Override
  57. public Collection<Session> getActiveSessions() {
  58. Set<Session> sessions = new HashSet<Session>();
  59. Set<String> keys = redisTemplate.keys(this.keyPrefix + "*");
  60. if(keys != null && keys.size()>0){
  61. for(String key:keys){
  62. Session s = (Session)redisTemplate.opsForValue().get(key);
  63. sessions.add(s);
  64. }
  65. }
  66. return sessions;
  67. }
  68. @Override
  69. protected Serializable doCreate(Session session) {
  70. Serializable sessionId = this.generateSessionId(session);
  71. this.assignSessionId(session, sessionId);
  72. this.saveSession(session);
  73. return sessionId;
  74. }
  75. @Override
  76. protected Session doReadSession(Serializable sessionId) {
  77. if(sessionId == null){
  78. logger.error("session id is null");
  79. return null;
  80. }
  81. Session s = (Session)redisTemplate.opsForValue().get(keyPrefix+sessionId);
  82. return s;
  83. }
  84. /**
  85. * Returns the Redis session keys
  86. * prefix.
  87. * @return The prefix
  88. */
  89. public String getKeyPrefix() {
  90. return keyPrefix;
  91. }
  92. /**
  93. * Sets the Redis sessions key
  94. * prefix.
  95. * @param keyPrefix The prefix
  96. */
  97. public void setKeyPrefix(String keyPrefix) {
  98. this.keyPrefix = keyPrefix;
  99. }
  100. public RedisTemplate<String, Object> getRedisTemplate() {
  101. return redisTemplate;
  102. }
  103. public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
  104. this.redisTemplate = redisTemplate;
  105. }
  106. }

spring-shiro.xml配置:

  1. <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
  2. <!-- 会话超时时间,单位:毫秒 20m=1200000ms, 30m=1800000ms, 60m=3600000ms-->
  3. <!-- 设置session过期时间为1小时(单位:毫秒),默认为30分钟 -->
  4. <!-- 如果设置 Redis缓存 此处不生效将 -->
  5. <property name="globalSessionTimeout" value="3600000"></property>
  6. <property name="sessionValidationSchedulerEnabled" value="true"></property>
  7. <property name="sessionIdUrlRewritingEnabled" value="false"></property>
  8. <!-- 注入 redisSessionDAO -->
  9. <property name="sessionDAO" ref="sessionDAO"/>
  10. </bean>
  11. <!-- redisSessionDAO -->
  12. <bean id="sessionDAO" class="com.acts.market.common.session.RedisSessionDAO">
  13. <property name="redisTemplate" ref="redisTemplate" />
  14. </bean>

乱码问题

2018年1月11日,新增了一个在线用户查询的功能,使用API查询所有用户:

  1. Collection<Session> sessions = redisSessionDAO.getActiveSessions();

结果sessions的size居然为空,继续跟踪底层代码:

  1. private String keyPrefix = "shiro_market_redis_session:";
  2. @Override
  3. public Collection<Session> getActiveSessions() {
  4. Set<Session> sessions = new HashSet<Session>();
  5. Set<Serializable> keys = redisTemplate.keys(this.keyPrefix + "*");
  6. if(keys != null && keys.size()>0){
  7. for(Serializable key:keys){
  8. Session s = (Session)redisTemplate.opsForValue().get(key);
  9. sessions.add(s);
  10. }
  11. }
  12. return sessions;
  13. }

感觉API没啥问题,后台登录redis查询下:

  1. ./redis-cli -h 192.168.1.180
  2. # 输入 auth password (没有设置密码的略过)

查看所有Keys:

  1. keys *

keys中居然出现了乱码

Shiro安全框架基于Redis的分布式集群方案

由于之前是精确匹配,虽然也有乱码的问题,但是可以查询出来,这次模糊匹配就出问题了。

由于我们使用的是spring-data-redis 中的核心操作类是 RedisTemplate<k, v="">, key 和 value 都是泛型的,这就涉及到将类型进行序列化的问题了。</k,>

RedisTemplate源码中存在以下序列环工具类:

  1. private RedisSerializer<?> defaultSerializer;
  2. private ClassLoader classLoader;
  3. private RedisSerializer keySerializer = null;
  4. private RedisSerializer valueSerializer = null;
  5. private RedisSerializer hashKeySerializer = null;
  6. private RedisSerializer hashValueSerializer = null;
  7. private RedisSerializer<String> stringSerializer = new StringRedisSerializer();

默认使用的是:

  1. if (defaultSerializer == null) {
  2. defaultSerializer = new JdkSerializationRedisSerializer(
  3. classLoader != null ? classLoader : this.getClass().getClassLoader());
  4. }

继续跟踪JdkSerializationRedisSerializer中的序列化方法:

  1. public byte[] serialize(Object object) {
  2. if (object == null) {
  3. return SerializationUtils.EMPTY_ARRAY;
  4. }
  5. try {
  6. return serializer.convert(object);
  7. } catch (Exception ex) {
  8. throw new SerializationException("Cannot serialize", ex);
  9. }
  10. }

SerializingConverter 类中的转化方法:

  1. /**
  2. * Serializes the source object and returns the byte array result.
  3. */
  4. @Override
  5. public byte[] convert(Object source) {
  6. ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024);
  7. try {
  8. this.serializer.serialize(source, byteStream);
  9. return byteStream.toByteArray();
  10. }
  11. catch (Throwable ex) {
  12. throw new SerializationFailedException("Failed to serialize object using " +
  13. this.serializer.getClass().getSimpleName(), ex);
  14. }
  15. }

由于项目中使用String作为缓存的key,变更了序列化类就可以了。

解决办法:

  1. <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
  2. p:connection-factory-ref="jedisConnectionFactory">
  3. <property name="keySerializer">
  4. <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
  5. </property>
  6. <property name="hashKeySerializer">
  7. <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
  8. </property>
  9. </bean>
  • 安卓客户端下载
  • 微信扫一扫
  • weinxin
  • 微信公众号
  • 微信公众号扫一扫
  • weinxin
avatar