史上最全的微服务权限控制方案

这篇具有很好参考价值的文章主要介绍了史上最全的微服务权限控制方案。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、微服务权限设计

先说下为什么写这篇文章,因为实际项目需要,需要对我们现在项目页面小到每个部件都要做权限控制,然后查了下网上常用的权限框架,一个是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?

因为我们的项目是由多个微服务组成,当用户服务接收到用户的登录请求并登录成功时我们给用户返回一个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<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 {

    @Override
    public <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
     */
    @Override
    public 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
     */

    @Override
    public 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
     */
    @Override
    public Object remove(Object s) throws CacheException {
        return JedisClient.delkey(objectToBytes(s));
    }

    /**
     * 清空所有的缓存
     *
     * @throws CacheException
     */

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

    /**
     * 缓存的个数
     *
     * @return
     */
    @Override
    public int size() {
        return JedisClient.getJedis().dbSize().intValue();
//        return redisTemplate.getConnectionFactory().getConnection().dbSize().intValue();
    }

    @Override
    public Set keys() {
        return JedisClient.getJedis().keys("*");
    }

    @Override
    public Collection values() {
        return null;
    }

}

注意上面objectToBytesbytesToObject方法是先将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;
    // 授权
    @Override
    protected 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;
    }
    @Override
    protected 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();
        // 关联UserRealm
        securityManager.setRealm(userRealm);
        securityManager.setSessionManager(defaultWebSessionManager);
        securityManager.setCacheManager(myCacheManager);
        return securityManager;
    }

    // 创建Realm对象, 需要自定义类
    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }


    /**
     * 下面DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor必须定义,
     * 否则不能使用@RequiresRoles和@RequiresPermissions
     *
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }


    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 设置自定义session管理器
     */
    @Bean
    public DefaultWebSessionManager myDefaultWebSessionManager(SimpleCookie simpleCookie) {
        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
        defaultWebSessionManager.setSessionIdCookie(simpleCookie);
        defaultWebSessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());
        return defaultWebSessionManager;
    }
    @Bean
    public 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;
    // 授权
    @Override
    protected 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;
    }


    // 认证
    @Override
    protected 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();
        // 关联UserRealm
        securityManager.setRealm(userRealm);
        securityManager.setCacheManager(myCacheManager);
        securityManager.setSessionManager(defaultWebSessionManager);

        return securityManager;
    }
    /**
     * 下面DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor必须定义,
     * 否则不能使用@RequiresRoles和@RequiresPermissions
     *
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }


    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 设置自定义session管理器
     */
    @Bean
    public DefaultWebSessionManager myDefaultWebSessionManager(SimpleCookie simpleCookie) {
        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
        defaultWebSessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());
        defaultWebSessionManager.setSessionIdCookie(simpleCookie);
        return defaultWebSessionManager;
    }

    @Bean
    public 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
     */
    @Bean
    public 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共享,简直完美!!!文章来源地址https://www.toymoban.com/news/detail-439358.html

到了这里,关于史上最全的微服务权限控制方案的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 史上最全的智能合约--扣税,分红,加池子,回流,黑白名单,防机器人,增发,丢权限之分红,加池子

    前面一章分享了智能合约扣税的原理,以及用什么方法实现扣税的,下面这篇文章,分享一下自动分红以为加池子的方法和注意事项。 废话不多说,先上代码。 分红这块只是用了简单的数据技巧,比较容易理解,不像网上很多土狗合约,用了很多高数的方法,相信很多同学

    2024年02月12日
    浏览(61)
  • 全网最全的微服务链路追踪实践-SkyWalking(看这一篇就够了)

    链路追踪介绍 对于一个大型的几十个、几百个微服务构成的微服务架构系统,通常会遇到下面一些问题,比如: 1. 如何串联整个调用链路,快速定位问题? 2. 如何缕清各个微服务之间的依赖关系? 3. 如何进行各个微服务接口的性能分折? 4. 如何跟踪整个业务流程的调用处

    2024年02月03日
    浏览(52)
  • @EnableWebMvc注解让swagger-ui.html无法打开404报错问题及其解决方案(史上最全最详细)

    在工作中,通过Swagger2对项目的controller进行配置,以便于用户测试restful服务接口提高开发效率。 但是今天却出现了一个让我匪夷所思的问题就是在配置类里面加上@EnableWebMvc注解后(开启web配置支持)启动项目 发现访问Swagger的ui界面404 这个我就奇怪了,然后我尝试的把@Ena

    2023年04月17日
    浏览(42)
  • 联想电脑thinkpad x13摄像头打不开,史上最全的针对联想电脑摄像头的解决方案

    最近面试,临近面试的前30min,发现摄像头打不开。具体情况如下: 这可没把我吓坏,我可是要露脸的,最后在我的不屑努力下,我选择了手机视频面试,很干。未来的几天都在琢磨这玩意儿了,现在说说常见的解决方案。 现在的电脑,大部分都添加了物理开启摄像头的操作

    2024年02月02日
    浏览(51)
  • 史上最全从0到1搭建最新版本jenkins可持续集成,整合git和maven部署微服务自动构建发版,抓紧收藏起来吧!

    好文推荐: netty搭建websocket集群(高性能.,高并发) springboot 实现延时队列(超级实用) 2.5万字讲解DDD领域驱动设计(史上最全DDD) 传统的软件开发在系统上线的时候需要手动进行,这个过程可能相对繁琐和容易出错,发布过程如下: 代码构建:开发人员在本地环境中编写

    2024年04月13日
    浏览(45)
  • 容器编排的未来:探索基于Kubernetes的微服务编排解决方案

    作者:禅与计算机程序设计艺术 当今的云计算环境下,容器技术正在成为主流,越来越多的公司选择基于容器技术实现应用部署及运行。容器编排技术也逐渐被普遍采用。通过容器编排工具可以将复杂的分布式系统架构部署、管理及扩展起来,从而提供一个高可用、易于维护

    2024年02月14日
    浏览(39)
  • Choerodon猪齿鱼平台中的微服务数据一致性解决方案

    众所周知,微服务架构解决了很多问题,通过分解复杂的单体式应用,在功能不变的情况下,使应用被分解为多个可管理的服务,为采用单体式编码方式很难实现的功能提供了模块化的解决方案。同时,每个微服务独立部署、独立扩展,使得持续化集成成为可能。由此,单个

    2024年02月19日
    浏览(40)
  • 在CSDN学Golang场景化解决方案(基于grpc的微服务开发脚手架)

    在Golang基于gRPC的微服务开发中,可以采用TLS加密通信来确保服务与服务之间的安全通信。下面是一个简单的设计示例: 生成证书和密钥: 定义gRPC服务器: 客户端连接gRPC服务器: 在服务实现中添加TLS加密通信: 这样,在Golang基于gRPC的微服务开发中,就可以使用TLS加密通信

    2024年02月14日
    浏览(49)
  • 史上最全的排序讲解

    目录  1、插入排序 思路 实现  2、希尔排序 思路 实现  3、选择排序 思路 实现   4、堆排序 思路 实现  5、冒泡排序  思路 实现  6、快速排序  方法一:霍尔快排法 方法二:挖坑法  方法三:前后指针法   7、归并排序 思路 实现  把待排序的记录按其关键码值的大小逐

    2024年02月03日
    浏览(42)
  • Linux史上最全教程

    我们所熟知的计算机是由硬件和软件组成。 硬件:计算机系统中由电子,机械和光电子元件等组成的各种物理装置装置的统称; 简单来说硬件就是看得见摸得到的。   软件:是用户和计算机硬件之间的接口和桥梁,用户通过软件和计算机进行交流。而我们要学习的Linux就是

    2024年02月03日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包