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

本文主要是介绍史上最全面的分布式微服务权限控制、会话管理的详细设计和实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、微服务权限设计

先说下为什么写这篇文章,因为实际项目需要,需要对我们现在项目页面小到每个部件都要做权限控制,然后查了下网上常用的权限框架,一个是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() {@Overrideprotected Cache<Serializable, Session> createCache(String name) throws CacheException {return new MapCache<Serializable, Session>(name, new ConcurrentHashMap<Serializable, Session>());}});}

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

3.3.3 具体实现
  • 首先导入我们需要的依赖包
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId></exclusion></exclusions></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><!--导入shrio相关--><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-core</artifactId><version>1.4.0</version></dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.4.0</version></dependency></dependencies>
  • 编写我们自己的CacheManager
@Component("myCacheManager")
public class MyCacheManager implements CacheManager {@Overridepublic <K, V> Cache<K, V> 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<Jedis> threadLocalJedis = new ThreadLocal<Jedis>();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 < 20) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}if (i >= 20 && i < 30) {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}if (i >= 30 && i < 40) {try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}}if (i >= 40) {System.out.println("redis彻底连不上了~~~~(>_<)~~~~");return null;}}if (threadLocalJedis.get() == null) {threadLocalJedis.set(jedis);}return jedis;}/*** 设置key-value** @param key* @param value*/public static void setValue(byte[] key, byte[] value) {Jedis jedis = null;try {jedis = getJedis();jedis.set(key, value);} catch (Exception e) {threadLocalJedis.set(null);logger.error("redis服务器异常", e);throw new RuntimeException("redis服务器异常");} finally {if (jedis != null) {close(jedis);}}}/*** 设置key-value,过期时间** @param key* @param value* @param seconds*/public static void setValue(byte[] key, byte[] value, int seconds) {Jedis jedis = null;try {jedis = getJedis();jedis.setex(key, seconds, value);} catch (Exception e) {threadLocalJedis.set(null);logger.error("redis服务器异常", e);throw new RuntimeException("redis服务器异常");} finally {if (jedis != null) {close(jedis);}}}public static byte[] getValue(byte[] key) {Jedis jedis = null;try {jedis = getJedis();if (jedis == null || !jedis.exists(key)) {return null;}return jedis.get(key);} catch (Exception e) {threadLocalJedis.set(null);logger.error("redis服务器异常", e);throw new RuntimeException("redis服务器异常");} finally {if (jedis != null) {close(jedis);}}}public static long delkey(byte[] key) {Jedis jedis = null;try {jedis = getJedis();if (jedis == null || !jedis.exists(key)) {return 0;}return jedis.del(key);} catch (Exception e) {threadLocalJedis.set(null);logger.error("redis服务器异常", e);throw new RuntimeException("redis服务器异常");} finally {if (jedis != null) {close(jedis);}}}public static void close(Jedis jedis) {if (threadLocalJedis.get() == null && jedis != null) {jedis.close();}}public static void clear() {if (threadLocalJedis.get() == null) {return;}Set<String> keys = threadLocalJedis.get().keys("*");keys.forEach(key -> delkey(key.getBytes()));}}
  • 自定义我们自己的Cache实现类
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.session.mgt.SimpleSession;import java.io.*;
import java.time.Duration;
import java.util.Collection;
import java.util.Set;public class MyCache<S, V> implements Cache<Object, Object> {//设置缓存的过期时间(30分钟)private Duration cacheExpireTime = Duration.ofMinutes(30);/*** 根据对应的key获取值value** @param s* @return* @throws CacheException*/@Overridepublic Object get(Object s) throws CacheException {System.out.println("get()方法....");byte[] bytes = JedisClient.getValue(objectToBytes(s));return bytes == null ? null : (SimpleSession) bytesToObject(bytes);}/*** 将K-V保存到redis中* 注意:保存的value是string类型** @param s* @param o* @return* @throws CacheException*/@Overridepublic Object put(Object s, Object o) throws CacheException {JedisClient.setValue(objectToBytes(s), objectToBytes(o), (int) cacheExpireTime.getSeconds());return s;}public byte[] objectToBytes(Object object) {ByteArrayOutputStream outputStream = new ByteArrayOutputStream();byte[] bytes = null;try {ObjectOutputStream op = new ObjectOutputStream(outputStream);op.writeObject(object);bytes = outputStream.toByteArray();} catch (IOException e) {e.printStackTrace();}return bytes;}public Object bytesToObject(byte[] bytes) {ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);Object object = null;try {ObjectInputStream ois = new ObjectInputStream(inputStream);object = ois.readObject();} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();}return object;}/*** 删除缓存,根据key** @param s* @return* @throws CacheException*/@Overridepublic Object remove(Object s) throws CacheException {return JedisClient.delkey(objectToBytes(s));}/*** 清空所有的缓存** @throws CacheException*/@Overridepublic void clear() throws CacheException {JedisClient.clear();}/*** 缓存的个数** @return*/@Overridepublic int size() {return JedisClient.getJedis().dbSize().intValue();
//        return redisTemplate.getConnectionFactory().getConnection().dbSize().intValue();}@Overridepublic Set keys() {return JedisClient.getJedis().keys("*");}@Overridepublic Collection values() {return null;}}

注意上面objectToBytes和bytesToObject方法是先将session转换成字节数组然后再存到redis中,从redis拿出来也是将字节数组转换成session对象,否则会报错。这是因为shrio使用的是自己包的simpleSession类,而这个类中的字段都是transient,不能直接序列化,需要我们自己将每个对象转成字节数组才可以进行操作。 当然,如果我们使用的是RedisTemplate,在配置的时候我们就不用写这两个方法了,直接使用默认的JDK序列化方式即可。

 	private transient Serializable id;private transient Date startTimestamp;private transient Date stopTimestamp;private transient Date lastAccessTime;private transient long timeout;private transient boolean expired;private transient String host;private transient Map<Object, Object> attributes;
  • 因为这里这个缓存模块是一个独立模块需要给其他微服务使用的,所以要想其他微服务可以自动配置我们自定义的缓存管理器CacheManager组件,我们还需要在resources文件夹下面新建一个文件夹META-INF,并在META-INF文件夹下面新建spring.factories文件。spring.factories中的内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.qzwang.common.cache.config.MyCacheManager
3.4 授权模块common-auth
  • 首先导入我们需要的依赖包
 <dependencies><dependency><groupId>com.qzwang</groupId><artifactId>user-dubbo-api</artifactId><version>1.0-SNAPSHOT</version></dependency><!--dubbo--><dependency><groupId>com.gitee.reger</groupId><artifactId>spring-boot-starter-dubbo</artifactId><version>1.1.3</version></dependency><!--加入共享会话缓存模块--><dependency><groupId>com.qzwang</groupId><artifactId>common-cache</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies>
  • 自定义realm,实现对用户访问权限的校验,注意,这里只实现权限校验,不实现用户认证,所以用户认证doGetAuthenticationInfo方法直接返回null就行了。

import com.alibaba.dubbo.config.annotation.Reference;
import com.qzwang.user.api.service.UserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;public class UserRealm extends AuthorizingRealm {@Reference(version = "0.0.1")private UserService userService;// 授权@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {//获取用户名String userName = (String) principalCollection.getPrimaryPrincipal();SimpleAuthorizationInfo authenticationInfo = new SimpleAuthorizationInfo();System.out.println("username=" + userName);//给用户设置角色authenticationInfo.setRoles(userService.selectRolesByUsername(userName));//给用户设置权限authenticationInfo.setStringPermissions(userService.selectPermissionByUsername(userName));return authenticationInfo;}@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {return null;}
}
  • shrio的配置中心,shrio的一些核心配置,包括shrio的安全管理器、过滤器都在这个类进行设置。
import com.qzwang.common.cache.config.MyCacheManager;
import com.qzwang.common.cache.config.MySessionDao;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {// ShiroFilterFactoryBean@Bean(name = "shiroFilterFactoryBean")public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();// 拦截Map<String, String> filterMap = new LinkedHashMap<>();filterMap.put("/**", "authc");shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);//shiroFilterFactoryBean.setLoginUrl("/user/index");// 设置安全管理器shiroFilterFactoryBean.setSecurityManager(securityManager);return shiroFilterFactoryBean;}// DefaultWebSecurityManager// @Qualifier中可以直接是bean的方法名,也可以给bean设置一个name,比如@Bean(name="myRealm"),在@Qulifier中就可以通过name来获取这个bean@Bean(name = "SecurityManager")public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm,@Qualifier("myDefaultWebSessionManager") DefaultWebSessionManager defaultWebSessionManager,@Qualifier("myCacheManager") MyCacheManager myCacheManager) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();// 关联UserRealmsecurityManager.setRealm(userRealm);securityManager.setSessionManager(defaultWebSessionManager);securityManager.setCacheManager(myCacheManager);return securityManager;}// 创建Realm对象, 需要自定义类@Beanpublic UserRealm userRealm() {return new UserRealm();}/*** 下面DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor必须定义,* 否则不能使用@RequiresRoles和@RequiresPermissions** @return*/@Bean@ConditionalOnMissingBeanpublic DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();defaultAAP.setProxyTargetClass(true);return defaultAAP;}@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);return authorizationAttributeSourceAdvisor;}/*** 设置自定义session管理器*/@Beanpublic DefaultWebSessionManager myDefaultWebSessionManager(SimpleCookie simpleCookie) {DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();defaultWebSessionManager.setSessionIdCookie(simpleCookie);defaultWebSessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());return defaultWebSessionManager;}@Beanpublic SimpleCookie simpleCookie() {SimpleCookie simpleCookie = new SimpleCookie("myCookie");simpleCookie.setPath("/");simpleCookie.setMaxAge(30);return simpleCookie;}}
3.5 用户消费者服务user-consumer
  • 先导入我们需要的依赖包。
<dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.11</version><scope>test</scope></dependency><dependency><groupId>com.qzwang</groupId><artifactId>user-dubbo-api</artifactId><version>1.0-SNAPSHOT</version></dependency><!-- dubbo+zookeeper+zkclient --><dependency><groupId>com.gitee.reger</groupId><artifactId>spring-boot-starter-dubbo</artifactId><version>1.1.3</version></dependency><dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.6.2</version></dependency><dependency><groupId>com.101tec</groupId><artifactId>zkclient</artifactId><version>0.11</version></dependency><!--导入缓存管理--><dependency><groupId>com.qzwang</groupId><artifactId>common-cache</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies>
  • 这个服务的缓存用公共模块的缓存(common-cache),shrio配置需要用我们自己的配置,这里realm中的认证和授权我们都需要实现。

import com.alibaba.dubbo.config.annotation.Reference;
import com.qzwang.user.api.model.User;
import com.qzwang.user.api.service.UserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.stereotype.Component;@Component
public class UserRealm extends AuthorizingRealm {@Reference(version = "0.0.1")private UserService userService;// 授权@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {//获取用户名String userName = (String) principalCollection.getPrimaryPrincipal();System.out.println("userName=" + userName);SimpleAuthorizationInfo authenticationInfo = new SimpleAuthorizationInfo();//给用户设置角色authenticationInfo.setRoles(userService.selectRolesByUsername(userName));//给用户设置权限authenticationInfo.setStringPermissions(userService.selectPermissionByUsername(userName));return authenticationInfo;}// 认证@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {String userName = (String) authenticationToken.getPrincipal();User user = userService.selectByUsername(userName);if (user != null) {AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), "myRealm");return authenticationInfo;}return null;}
}
  • shrio的相关配置。
import com.qzwang.common.cache.config.MyCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.LinkedHashMap;
import java.util.Map;@Configuration
public class ShiroConfig {// ShiroFilterFactoryBean@Bean(name = "shiroFilterFactoryBean")public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();// 设置安全管理器shiroFilterFactoryBean.setSecurityManager(securityManager);// 添加shiro的内置过滤器/*anon: 无需认证就能访问authc: 必须认证了才能访问UserController: 必须拥有 记住我 功能才能访问perms: 拥有某个资源权限才能访问role: 拥有某个角色权限才能访问*/// 拦截Map<String, String> filterMap = new LinkedHashMap<>();// 授权// filterMap.put("/UserController/add", "perms[UserController:add]");filterMap.put("/user/testFunc", "authc");shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);// 设置未授权页面shiroFilterFactoryBean.setUnauthorizedUrl("/user/unAuth");// 设置登录的请求// shiroFilterFactoryBean.setLoginUrl("/user/index");return shiroFilterFactoryBean;}// DefaultWebSecurityManager// @Qualifier中可以直接是bean的方法名,也可以给bean设置一个name,比如@Bean(name="myRealm"),在@Qulifier中就可以通过name来获取这个bean@Bean(name = "SecurityManager")public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm,@Qualifier("myDefaultWebSessionManager") DefaultWebSessionManager defaultWebSessionManager,@Qualifier("myCacheManager") MyCacheManager myCacheManager) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();// 关联UserRealmsecurityManager.setRealm(userRealm);securityManager.setCacheManager(myCacheManager);securityManager.setSessionManager(defaultWebSessionManager);return securityManager;}/*** 下面DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor必须定义,* 否则不能使用@RequiresRoles和@RequiresPermissions** @return*/@Bean@ConditionalOnMissingBeanpublic DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();defaultAAP.setProxyTargetClass(true);return defaultAAP;}@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);return authorizationAttributeSourceAdvisor;}/*** 设置自定义session管理器*/@Beanpublic DefaultWebSessionManager myDefaultWebSessionManager(SimpleCookie simpleCookie) {DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();defaultWebSessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());defaultWebSessionManager.setSessionIdCookie(simpleCookie);return defaultWebSessionManager;}@Beanpublic SimpleCookie simpleCookie() {SimpleCookie simpleCookie = new SimpleCookie("myCookie");simpleCookie.setPath("/");simpleCookie.setMaxAge(30);return simpleCookie;}}
  • 配置用户未认证异常拦截
import com.qzwang.common.core.config.ExceptionConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;import java.util.Properties;@Configuration
public class AuthorizationExceptionConfig {Logger logger = LoggerFactory.getLogger(ExceptionConfig.class);/*** 捕获未认证的方法** @return*/@Beanpublic SimpleMappingExceptionResolver simpleMappingExceptionResolver() {SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver();Properties properties = new Properties();properties.setProperty("org.apache.shiro.authz.AuthorizationException", "/user/unAuth");simpleMappingExceptionResolver.setExceptionMappings(properties);return simpleMappingExceptionResolver;}
}
  • 用户登录接口如下:
@RestController
@RequestMapping("/user")
public class UserController {@Reference(version = "0.0.1")private UserService userService;@RequestMapping(value = "/login", method = RequestMethod.POST)public R login(@RequestBody User user) {UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());Subject subject = SecurityUtils.getSubject();try {subject.login(token);return R.ok();} catch (Exception e) {e.printStackTrace();return R.failed();}}@RequestMapping(value = "/unAuth", method = RequestMethod.GET)public R unAuth() {return R.failed("该用户未授权!");}@RequiresRoles("admin")@RequestMapping(value = "/testFunc", method = RequestMethod.GET)public R testFunc() {return R.ok("yes success!!!");}
}

1、用户先登录。
在这里插入图片描述
2、访问/user/testFunc接口,注意此接口需要admin角色,但是现在数据库中zhangsan用户并没有该角色,因此也就没有权限访问该接口。
在这里插入图片描述
3、现在在数据库中给zhangsan添加一个admin角色,再进行测试。
在这里插入图片描述

3.6 视频消费者服务video-consumer

这个服务我主要测试一下是否可以实现共享session会话,实现权限控制。

  • 首先导入需要的模块
<dependencies><dependency><groupId>com.qzwang</groupId><artifactId>common-auth</artifactId><version>0.0.1</version></dependency><!-- dubbo+zookeeper+zkclient --><dependency><groupId>com.gitee.reger</groupId><artifactId>spring-boot-starter-dubbo</artifactId><version>1.1.3</version></dependency><dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.6.2</version></dependency><dependency><groupId>com.101tec</groupId><artifactId>zkclient</artifactId><version>0.11</version></dependency></dependencies>
  • 下面写一个接口测试一下,注意。因为我们这里导入的是公共授权common-auth模块,在这个模块中配置每个接口需要认证才能访问,我们首先测试一下未登录访问该接口。
@RestController
@RequestMapping("/video")
public class VideoController {@RequestMapping("/getVideo")public R getVideo() {return R.ok();}
}

在这里插入图片描述
可以看到它跳到shrio默认的登录页面去了。下面我们再测试登录成功之后在访问该接口。
在这里插入图片描述
可以看到,用户的会话信息是实现共享了,下面再测试给该接口加权限试试。

@RestController
@RequestMapping("/video")
public class VideoController {@RequestMapping("/getVideo")@RequiresRoles("admin")public R getVideo() {return R.ok();}
}

在zhangsan没有权限的情况下是不能访问该接口的。
在这里插入图片描述
由于上面配置的未授权接口/user/unAuth是在用户服务中,提示找不到该接口,这里需要给这些微服务配置一个网关gateway(这里就不展开怎么配置了,这不是本篇的重点)。上面当用户有admin角色时访问该接口测试如下。
在这里插入图片描述
因此经过测试公共模块common-Auth实现了用户会话和权限realm数据的redis共享,简直完美!!!

至于上面的yml配置、provider服务、common-core等因为篇幅原因就没有把代码全都放出来了,如果需要,我后面再把全部代码放出来。

这篇关于史上最全面的分布式微服务权限控制、会话管理的详细设计和实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/902894

相关文章

python使用watchdog实现文件资源监控

《python使用watchdog实现文件资源监控》watchdog支持跨平台文件资源监控,可以检测指定文件夹下文件及文件夹变动,下面我们来看看Python如何使用watchdog实现文件资源监控吧... python文件监控库watchdogs简介随着Python在各种应用领域中的广泛使用,其生态环境也

el-select下拉选择缓存的实现

《el-select下拉选择缓存的实现》本文主要介绍了在使用el-select实现下拉选择缓存时遇到的问题及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录项目场景:问题描述解决方案:项目场景:从左侧列表中选取字段填入右侧下拉多选框,用户可以对右侧

Linux中chmod权限设置方式

《Linux中chmod权限设置方式》本文介绍了Linux系统中文件和目录权限的设置方法,包括chmod、chown和chgrp命令的使用,以及权限模式和符号模式的详细说明,通过这些命令,用户可以灵活... 目录设置基本权限命令:chmod1、权限介绍2、chmod命令常见用法和示例3、文件权限详解4、ch

最新版IDEA配置 Tomcat的详细过程

《最新版IDEA配置Tomcat的详细过程》本文介绍如何在IDEA中配置Tomcat服务器,并创建Web项目,首先检查Tomcat是否安装完成,然后在IDEA中创建Web项目并添加Web结构,接着,... 目录配置tomcat第一步,先给项目添加Web结构查看端口号配置tomcat    先检查自己的to

使用Nginx来共享文件的详细教程

《使用Nginx来共享文件的详细教程》有时我们想共享电脑上的某些文件,一个比较方便的做法是,开一个HTTP服务,指向文件所在的目录,这次我们用nginx来实现这个需求,本文将通过代码示例一步步教你使用... 在本教程中,我们将向您展示如何使用开源 Web 服务器 Nginx 设置文件共享服务器步骤 0 —

Python pyinstaller实现图形化打包工具

《Pythonpyinstaller实现图形化打包工具》:本文主要介绍一个使用PythonPYQT5制作的关于pyinstaller打包工具,代替传统的cmd黑窗口模式打包页面,实现更快捷方便的... 目录1.简介2.运行效果3.相关源码1.简介一个使用python PYQT5制作的关于pyinstall

使用Python实现大文件切片上传及断点续传的方法

《使用Python实现大文件切片上传及断点续传的方法》本文介绍了使用Python实现大文件切片上传及断点续传的方法,包括功能模块划分(获取上传文件接口状态、临时文件夹状态信息、切片上传、切片合并)、整... 目录概要整体架构流程技术细节获取上传文件状态接口获取临时文件夹状态信息接口切片上传功能文件合并功能小

python实现自动登录12306自动抢票功能

《python实现自动登录12306自动抢票功能》随着互联网技术的发展,越来越多的人选择通过网络平台购票,特别是在中国,12306作为官方火车票预订平台,承担了巨大的访问量,对于热门线路或者节假日出行... 目录一、遇到的问题?二、改进三、进阶–展望总结一、遇到的问题?1.url-正确的表头:就是首先ur

C#实现文件读写到SQLite数据库

《C#实现文件读写到SQLite数据库》这篇文章主要为大家详细介绍了使用C#将文件读写到SQLite数据库的几种方法,文中的示例代码讲解详细,感兴趣的小伙伴可以参考一下... 目录1. 使用 BLOB 存储文件2. 存储文件路径3. 分块存储文件《文件读写到SQLite数据库China编程的方法》博客中,介绍了文

Redis主从复制实现原理分析

《Redis主从复制实现原理分析》Redis主从复制通过Sync和CommandPropagate阶段实现数据同步,2.8版本后引入Psync指令,根据复制偏移量进行全量或部分同步,优化了数据传输效率... 目录Redis主DodMIK从复制实现原理实现原理Psync: 2.8版本后总结Redis主从复制实