史上最全面的分散式微服務許可權控制、會話管理的詳細設計和實現
先說下為什麼寫這篇文章,因為實際專案需要,需要對我們現在專案頁面小到每個部件都要做許可權控制,然後查了下網上常用的許可權框架,一個是shrio,一個是spring security,看了下對比,都說shrio比較輕量,比較好用,然後我也就選擇了shrio來做整個專案的許可權框架,同時結合網上大佬做過的一些spring boot+shrio整合案例,只能說大家圖都畫的挺好的....,看著大家的功能流程圖仔細想想是那麼回事,然後自己再實踐就走不動了,各種坑都有啊。。。,迴歸到具體實現真的是步步都是坑。在實踐的過程中想了下面幾種方案,有些要麼是還沒開始coding就已經想著走不通了,有些就是程式碼敲了一半了發現行不通了,在本專案中我也參考了RCBA許可權設計模型。
1、將shrio和閘道器gateway放在同一個服務中,但是這就帶來一個問題,眾所周知,shrio的資料中心realm需要用到使用者服務當中的資料(查詢使用者、角色、許可權之間的關係及資料),因此這裡shrio就需要使用服務發現元件(我這裡用的dubbo)去發現使用者服務,但是使用者服務中的登入又需要用到shrio的認證,到這裡可能有人要說了,可以在使用者服務中再去遠端呼叫shrio服務啊,如果這種方法可以的話大家就可以用這種方法就不用往下看了....**所以這就造成兩個服務耦合在一塊兒去了,這種方法直接pass掉。**
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20201109195844431.png#pic_center)
2、在每一個服務中都共享一個shrio配置模組,這種方式同樣也有問題,和上面出現的問題類似,現在shrio是個單獨的模組,需要用到使用者服務,可以使用dubbo遠端呼叫,而使用者服務需要將shrio配置模組通過maven匯入進來,現在啟動使用者服務,肯定會報錯:在shrio配置模組中沒有找到服務的提供者。因此這種方案也可以pass掉了。
相信上面兩種方案肯定不止我一個人這麼做過,只能說shrio還是適合單體架構啊....當然,也不是說shrio不能做微服務的許可權控制,在經過我長達一週的鑽研和嘗試之後,終於還是發現微服務用shrio怎樣做許可權設計了,下面說一下我的方案。、
二、設計方案
結合上面兩種行不通的方法,我們取長補短,新的方案如下。
方案一
既然使用者服務和shrio模組需要分開但是兩者又是需要互相依賴,我們可以針對使用者服務專門配置一個shrio模組,其他服務共享一個shrio模組。當然這兩個shrio模組需要共享session會話
、
三、具體實現
示例專案使用springboot+mysql+mybatis-plus實現,服務發現和註冊工具採用dubbo+zookeeper(這裡我主要是想學習下這兩個元件的用法,大家也可以使用eureka+feign)。
3.1 專案的結構
common模組:整個專案的公共模組,common-core就包含了其他微服務需要的一些常量資料、返回值、異常,common-cache模組中包含了所有微服務需要的shrio快取配置,除了使用者服務其他服務需要的授權模組common-auth。
gateway-service服務:閘道器服務,所有其他服務的入口。
user-api:使用者服務定義的資料介面。
user-provider-service:使用者服務介面的實現,使用者服務的提供者。
user-consumer-service:使用者服務的最外層,供nginx訪問呼叫的服務,使用者服務的消費者。
video-api:同用戶服務api。
video-provider:同用戶服務provider。
video-consumer:同用戶服務consumer。
3.2 表關係
3.3 共享session會話(快取模組common-cache)
3.3.1 為什麼需要共享session?
先說一下我們為什麼需要共享session會話,因為我們的專案是由多個微服務組成,當用戶服務接收到使用者的登入請求並登入成功時我們給使用者返回一個sessionId並儲存在使用者的瀏覽器中的cookie裡,使用者此時再請求使用者服務就會攜帶cookie當中的sessionId而伺服器端就可以根據使用者攜帶的sessionId取出儲存在伺服器的使用者資訊,但是此時如果使用者去請求視訊服務就不能取出儲存在伺服器的使用者資訊,因為視訊服務根本就不知道你是否登入過,所以這就需要我們將登入成功的使用者資訊進行共享而不僅僅是使用者服務才可以訪問。
3.3.2 怎麼實現共享session?
我們在寫shrio的相關配置時,都知道需要自定義shrio的安全管理器,也就是重寫DefaultWebSecurityManager,我們看一下例項化這個安全管理器類中間有哪些元件會被初始化。
首先是DefaultWebSecurityManager的構造器。
public DefaultWebSecurityManager() { super(); ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator()); this.sessionMode = HTTP_SESSION_MODE; setSubjectFactory(new DefaultWebSubjectFactory()); setRememberMeManager(new CookieRememberMeManager()); setSessionManager(new ServletContainerSessionManager()); }
進入DefaultWebSecurityManager的父類DefaultSecurityManager,檢視DefaultSecurityManager的構造器。
public DefaultSecurityManager() { super(); this.subjectFactory = new DefaultSubjectFactory(); this.subjectDAO = new DefaultSubjectDAO(); }
進入DefaultSecurityManager的父類SessionsSecurityManager,檢視SessionsSecurityManager的構造器。
public SessionsSecurityManager() { super(); this.sessionManager = new DefaultSessionManager(); applyCacheManagerToSessionManager(); }
在這個構造器中我們看到了例項化了一個預設的session管理器DefaultSessionManager。我們點進去看看。可以看到DefaultSessionManager中預設的就是使用的是記憶體來儲存session(MemorySessionDAO就是對session進行操作的類)。
public SessionsSecurityManager() { super(); this.sessionManager = new DefaultSessionManager(); applyCacheManagerToSessionManager(); }
根據上面我們的分析,如果要想在各個微服務中共享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,不能直接序列化,需要我們自己將每個物件轉成位元組陣列才可以進行操作。 當然,如果我們使用的是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; } } ``` - 使用者登入介面如下: ```java @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共享,簡直完美!!!
原文見我的CSDN原部落格連結https://blog.csdn.net/to10086/article/details/10957394