全網最全面的『分佈式微服務權限設計』

一、微服務權限設計

先說下爲什麼寫這篇文章,因爲實際項目需要,需要對我們現在項目頁面小到每個部件都要做權限控制,然後查了下網上常用的權限框架,一個是 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

3.2 表關係如下

3.3 共享 session 會話(緩存模塊 common-cache)

3.3.1 爲什麼需要共享 session?

先說一下我們爲什麼需要共享 session 會話,因爲我們的項目是由多個微服務組成,當用戶服務接收到用戶的登錄請求並登錄成功時我們給用戶返回一個 sessionId 並保存在用戶的瀏覽器中的 cookie 裏,用戶此時再請求用戶服務就會攜帶 cookie 當中的 sessionId 而服務器端就可以根據用戶攜帶的 sessionId 取出保存在服務器的用戶信息,但是此時如果用戶去請求視頻服務就不能取出保存在服務器的用戶信息,因爲視頻服務根本就不知道你是否登錄過,所以這就需要我們將登錄成功的用戶信息進行共享而不僅僅是用戶服務纔可以訪問。

歡迎關注公衆號:SpringForAll 社區(spring4all.com),專注分享關於 Spring 的一切!回覆 “加羣” 還可加入 Spring 技術交流羣!

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;
    }

}

注意上面 objectToBytes 和 bytesToObject 方法是先將 session 轉換成字節數組然後再存到 redis 中,從 redis 拿出來也是將字節數組轉換成 session 對象,否則會報錯。這是因爲 shrio 使用的是自己包的 simpleSession 類,而這個類中的字段都是 transient,不能直接序列化,需要我們自己將每個對象轉成字節數組纔可以進行操作。

歡迎關注公衆號:SpringForAll 社區(spring4all.com),專注分享關於 Spring 的一切!回覆 “加羣” 還可加入 Spring 技術交流羣!

當然,如果我們使用的是 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(),在@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(),在@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 共享,簡直完美!!!

至於上面的 yml 配置、provider 服務、common-core等因爲篇幅原因就沒有把代碼全都放出來了,如果需要,我後面再把全部代碼放出來。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/uFPoIytU5iFkQmaaCRKF2w