史上最全面的分布式微服务权限控制、会话管理的详细设计和实现

您所在的位置:网站首页 一加权限管理在哪里 史上最全面的分布式微服务权限控制、会话管理的详细设计和实现

史上最全面的分布式微服务权限控制、会话管理的详细设计和实现

2024-01-19 20:52| 来源: 网络整理| 查看: 265

一、微服务权限设计

先说下为什么写这篇文章,因为实际项目需要,需要对我们现在项目页面小到每个部件都要做权限控制,然后查了下网上常用的权限框架,一个是shrio,一个是spring security,看了下对比,都说shrio比较轻量,比较好用,然后我也就选择了shrio来做整个项目的权限框架,同时结合网上大佬做过的一些spring boot+shrio整合案例,只能说大家图都画的挺好的…,看着大家的功能流程图仔细想想是那么回事,然后自己再实践就走不动了,各种坑都有啊。。。,回归到具体实现真的是步步都是坑。在实践的过程中想了下面几种方案,有些要么是还没开始coding就已经想着走不通了,有些就是代码敲了一半了发现行不通了,在本项目中我也参考了RCBA权限设计模型(图源权限设计方案)。 在这里插入图片描述

1、将shrio和网关gateway放在同一个服务中,但是这就带来一个问题,众所周知,shrio的数据中心realm需要用到用户服务当中的数据(查询用户、角色、权限之间的关系及数据),因此这里shrio就需要使用服务发现组件(我这里用的dubbo)去发现用户服务,但是用户服务中的登录又需要用到shrio的认证,到这里可能有人要说了,可以在用户服务中再去远程调用shrio服务啊,如果这种方法可以的话大家就可以用这种方法就不用往下看了…所以这就造成两个服务耦合在一块儿去了,这种方法直接pass掉。 在这里插入图片描述 2、在每一个服务中都共享一个shrio配置模块,这种方式同样也有问题,和上面出现的问题类似,现在shrio是个单独的模块,需要用到用户服务,可以使用dubbo远程调用,而用户服务需要将shrio配置模块通过maven导入进来,现在启动用户服务,肯定会报错:在shrio配置模块中没有找到服务的提供者。因此这种方案也可以pass掉了。

相信上面两种方案肯定不止我一个人这么做过,只能说shrio还是适合单体架构啊…当然,也不是说shrio不能做微服务的权限控制,在经过我长达一周的钻研和尝试之后,终于还是发现微服务用shrio怎样做权限设计了,下面说一下我的方案。

二、设计方案

结合上面两种行不通的方法,我们取长补短,新的方案如下。

方案一

既然用户服务和shrio模块需要分开但是两者又是需要互相依赖,我们可以针对用户服务专门配置一个shrio模块,其他服务共享一个shrio模块。当然这两个shrio模块需要共享session会话

在这里插入图片描述

三、具体实现

示例项目使用springboot+mysql+mybatis-plus实现,服务发现和注册工具采用dubbo+zookeeper(这里我主要是想学习下这两个组件的用法,大家也可以使用eureka+feign)。

3.1 项目的结构如下:

在这里插入图片描述

common模块:整个项目的公共模块,common-core就包含了其他微服务需要的一些常量数据、返回值、异常,common-cache模块中包含了所有微服务需要的shrio缓存配置,除了用户服务其他服务需要的授权模块common-auth。 在这里插入图片描述 gateway-service服务:网关服务,所有其他服务的入口。 user-api:用户服务定义的数据接口。 user-provider-service:用户服务接口的实现,用户服务的提供者。 user-consumer-service:用户服务的最外层,供nginx访问调用的服务,用户服务的消费者。 video-api:同用户服务api。 video-provider:同用户服务provider。 video-consumer:同用户服务consumer。

3.2 表关系如下

在这里插入图片描述

3.3 共享session会话(缓存模块common-cache) 3.3.1 为什么需要共享session?

先说一下我们为什么需要共享session会话,因为我们的项目是由多个微服务组成,当用户服务接收到用户的登录请求并登录成功时我们给用户返回一个sessionId并保存在用户的浏览器中的cookie里,用户此时再请求用户服务就会携带cookie当中的sessionId而服务器端就可以根据用户携带的sessionId取出保存在服务器的用户信息,但是此时如果用户去请求视频服务就不能取出保存在服务器的用户信息,因为视频服务根本就不知道你是否登录过,所以这就需要我们将登录成功的用户信息进行共享而不仅仅是用户服务才可以访问。

3.3.2 怎么实现共享session?

我们在写shrio的相关配置时,都知道需要自定义shrio的安全管理器,也就是重写DefaultWebSecurityManager,我们看一下实例化这个安全管理器类中间有哪些组件会被初始化。 首先是DefaultWebSecurityManager的构造器。

public DefaultWebSecurityManager() { super(); ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator()); this.sessionMode = HTTP_SESSION_MODE; setSubjectFactory(new DefaultWebSubjectFactory()); setRememberMeManager(new CookieRememberMeManager()); setSessionManager(new ServletContainerSessionManager()); }

进入DefaultWebSecurityManager的父类DefaultSecurityManager,查看DefaultSecurityManager的构造器。

public DefaultSecurityManager() { super(); this.subjectFactory = new DefaultSubjectFactory(); this.subjectDAO = new DefaultSubjectDAO(); }

进入DefaultSecurityManager的父类SessionsSecurityManager,查看SessionsSecurityManager的构造器。

public SessionsSecurityManager() { super(); this.sessionManager = new DefaultSessionManager(); applyCacheManagerToSessionManager(); }

在这个构造器中我们看到了实例化了一个默认的session管理器DefaultSessionManager。我们点进去看看。可以看到DefaultSessionManager中默认的就是使用的是内存来保存session(MemorySessionDAO就是对session进行操作的类)。

public DefaultSessionManager() { this.deleteInvalidSessions = true; this.sessionFactory = new SimpleSessionFactory(); this.sessionDAO = new MemorySessionDAO(); }

根据上面我们的分析,如果要想在各个微服务中共享session就不能把session放在某个微服务所在服务器的内存中,需要把session单独拿出来共享,因此我们就需要写一个自定义的SessionDAO来覆盖默认的MemorySessionDAO,下面来看看怎么实现自定义的SessionDAO。 在这里插入图片描述

根据上面sessionDAO关系图我们可以知道,AbstractSessionDAO主要有两个子类,一个是已经实现好的EnterpriseCacheSessionDAO,另一个就是MemorySessionDAO,现在我们需要替换默认的MemorySessionDAO,要么我们继承AbstractSessionDAO实现其中的读写session的方法,要么直接使用它已经给我们实现好的EnterpriseCacheSessionDAO。在这里我选择直接使用EnterpriseCacheSessionDAO类。

public EnterpriseCacheSessionDAO() { setCacheManager(new AbstractCacheManager() { @Override protected Cache createCache(String name) throws CacheException { return new MapCache(name, new ConcurrentHashMap()); } }); }

不过在上面类的构造方法中我们可以发现它默认是给我们new了一个AbstractCacheManager缓存管理器,并且使用的是ConcurrentHashMap来保存会话session,因此如果我们要用这个EnterpriseCacheSessionDAO类来实现缓存操作,那么我们就需要需要写一个自定义的CacheManager来覆盖它默认的CacheManager。

3.3.3 具体实现 首先导入我们需要的依赖包 org.springframework.boot spring-boot-starter-data-redis io.lettuce lettuce-core redis.clients jedis org.apache.commons commons-pool2 org.apache.shiro shiro-core 1.4.0 org.apache.shiro shiro-spring 1.4.0 编写我们自己的CacheManager @Component("myCacheManager") public class MyCacheManager implements CacheManager { @Override public Cache getCache(String s) throws CacheException { return new MyCache(); } } Jedis客户端(这里不用RedisTemplate,因为经过实际测试和网上查阅资料RedisTemplate的查询效率远不如Jedis客户端。) public class JedisClient { private static Logger logger = LoggerFactory.getLogger(JedisClient.class); protected static final ThreadLocal threadLocalJedis = new ThreadLocal(); private static JedisPool jedisPool; private static final String HOST = "localhost"; private static final int PORT = 6379; private static final String PASSWORD = "1234"; //控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8。 private static int MAX_IDLE = 16; //可用连接实例的最大数目,默认值为8; //如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。 private static int MAX_ACTIVE = -1; //超时时间 private static final int TIMEOUT = 1000 * 5; //等待可用连接的最大时间,单位毫秒,默认值为-1。表示用不超时 private static int MAX_WAIT = 1000 * 5; // 连接数据库(0-15) private static final int DATABASE = 2; static { initialPool(); } public static JedisPool initialPool() { JedisPool jp = null; try { JedisPoolConfig config = new JedisPoolConfig(); config.setMaxIdle(MAX_IDLE); config.setMaxTotal(MAX_ACTIVE); config.setMaxWaitMillis(MAX_WAIT); config.setTestOnCreate(true); config.setTestWhileIdle(true); config.setTestOnReturn(true); jp = new JedisPool(config, HOST, PORT, TIMEOUT, PASSWORD, DATABASE); jedisPool = jp; threadLocalJedis.set(getJedis()); } catch (Exception e) { e.printStackTrace(); logger.error("redis服务器异常", e); } return jp; } /** * 获取jedis实例 * * @return jedis */ public static Jedis getJedis() { boolean success = false; Jedis jedis = null; int i = 0; while (!success) { i++; try { if (jedisPool != null) { jedis = threadLocalJedis.get(); if (jedis == null) { jedis = jedisPool.getResource(); } else { if (!jedis.isConnected() && !jedis.getClient().isBroken()) { threadLocalJedis.set(null); jedis = jedisPool.getResource(); } return jedis; } } else { throw new RuntimeException("redis连接池初始化失败"); } } catch (Exception e) { logger.error(Thread.currentThread().getName() + "第" + i + "次获取失败"); success = false; e.printStackTrace(); logger.error("redis服务器异常", e); } if (jedis != null) { success = true; } if (i >= 10 && i Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } if (i >= 20 && i Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } if (i >= 30 && i Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } if (i >= 40) { System.out.println("redis彻底连不上了~~~~(>_


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3