史上最全面的分佈式微服務權限控制、會話管理的詳細設計和實現

先說下爲何寫這篇文章,由於實際項目須要,須要對咱們如今項目頁面小到每一個部件都要作權限控制,而後查了下網上經常使用的權限框架,一個是shrio,一個是spring security,看了下對比,都說shrio比較輕量,比較好用,而後我也就選擇了shrio來作整個項目的權限框架,同時結合網上大佬作過的一些spring boot+shrio整合案例,只能說你們圖都畫的挺好的....,看着你們的功能流程圖仔細想一想是那麼回事,而後本身再實踐就走不動了,各類坑都有啊。。。,迴歸到具體實現真的是步步都是坑。在實踐的過程當中想了下面幾種方案,有些要麼是還沒開始coding就已經想着走不通了,有些就是代碼敲了一半了發現行不通了,在本項目中我也參考了RCBA權限設計模型

java

一、將shrio和網關gateway放在同一個服務中,可是這就帶來一個問題,衆所周知,shrio的數據中心realm須要用到用戶服務當中的數據(查詢用戶、角色、權限之間的關係及數據),所以這裏shrio就須要使用服務發現組件(我這裏用的dubbo)去發現用戶服務,可是用戶服務中的登陸又須要用到shrio的認證,到這裏可能有人要說了,能夠在用戶服務中再去遠程調用shrio服務啊,若是這種方法能夠的話你們就能夠用這種方法就不用往下看了....**因此這就形成兩個服務耦合在一起去了,這種方法直接pass掉。**
![在這裏插入圖片描述](https://img-blog.csdnimg.cn/20201109195844431.png#pic_center)
二、在每個服務中都共享一個shrio配置模塊,這種方式一樣也有問題,和上面出現的問題相似,如今shrio是個單獨的模塊,須要用到用戶服務,可使用dubbo遠程調用,而用戶服務須要將shrio配置模塊經過maven導入進來,如今啓動用戶服務,確定會報錯:在shrio配置模塊中沒有找到服務的提供者。所以這種方案也能夠pass掉了。mysql


 

相信上面兩種方案確定不止我一我的這麼作過,只能說shrio仍是適合單體架構啊....固然,也不是說shrio不能作微服務的權限控制,在通過我長達一週的鑽研和嘗試以後,終於仍是發現微服務用shrio怎樣作權限設計了,下面說一下個人方案。、nginx


2、設計方案

結合上面兩種行不通的方法,咱們取長補短,新的方案以下。
方案一git

既然用戶服務和shrio模塊須要分開可是二者又是須要互相依賴,咱們能夠針對用戶服務專門配置一個shrio模塊,其餘服務共享一個shrio模塊。固然這兩個shrio模塊須要共享session會話web

redis

 


 

3、具體實現

示例項目使用springboot+mysql+mybatis-plus實現,服務發現和註冊工具採用dubbo+zookeeper(這裏我主要是想學習下這兩個組件的用法,你們也可使用eureka+feign)。spring

 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。sql

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的構造器。apache

 

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!!!");
}
}

 


一、用戶先登陸。

二、訪問/user/testFunc接口,注意此接口須要admin角色,可是如今數據庫中zhangsan用戶並無該角色,所以也就沒有權限訪問該接口。

三、如今在數據庫中給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

相關文章
相關標籤/搜索