史上最全的微服务权限控制方案,完美实现!

科技   2024-11-21 11:55   上海  

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入芋道快速开发平台知识星球。下面是星球提供的部分资料: 

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:

  • Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 双版本 
来源:blog.csdn.net/to10086/
article/details/109573948

一、微服务权限设计

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

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

二、设计方案

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

方案一

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

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

三、具体实现

示例项目使用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<SVimplements Cache<ObjectObject{


    //设置缓存的过期时间(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共享,简直完美!!!


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)

Java基基
一个苦练基本功的 Java 公众号,所以取名 Java 基基
 最新文章