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