shiro簡介
Apache Shiro 是 Java 的一個安全架構。目前,使用 Apache Shiro 的人越來越多,因為它相當簡單,對比 Spring Security,可能沒有 Spring Security 做的功能強大,但是在實際工作時可能并不需要那麼複雜的東西,是以使用小而簡單的 Shiro 就足夠了
Shiro架構
從外部來看Shiro,即從應用程式角度的來觀察如何使用Shiro完成工作
Subject:應用代碼直接互動的對象是Subject,也就是說Shiro的對外API 核心就是Subject。Subject 代表了目前“使用者”,這個使用者不一定是一個具體的人,與目前應用互動的任何東西都是Subject,如網絡爬蟲,機器人等;與Subject 的所有互動都會委托給SecurityManager;Subject 其實是一個門面,SecurityManager才是實際的執行者
SecurityManager:安全管理器;即所有與安全有關的操作都會與SecurityManager互動;且其管理着所有Subject;可以看出它是Shiro的核心,它負責與Shiro的其他元件進行互動,它相當于SpringMVC中DispatcherServlet的角色
Realm:Shiro從Realm 擷取安全資料(如使用者、角色、權限),就是說SecurityManager要驗證使用者身份,那麼它需要從Realm 擷取相應的使用者進行比較以确定使用者身份是否合法;也需要從Realm 得到使用者相應的角色/權限進行驗證使用者是否能進行操作;可以把Realm 看成DataSource
導入shiro依賴
shiro配置資訊
在application.yml檔案,路徑如下圖
# Shiro
shiro:
user:
# 登入位址
loginUrl: /login
# 權限認證失敗位址
unauthorizedUrl: /unauth
# 首頁位址
indexUrl: /index
# 驗證碼開關
captchaEnabled: true
# 驗證碼類型 math 數字計算 char 字元驗證
captchaType: math
cookie:
# 設定Cookie的域名 預設空,即目前通路的域名
domain:
# 設定cookie的有效通路路徑
path: /
# 設定HttpOnly屬性
httpOnly: true
# 設定Cookie的過期時間,天為機關
maxAge: 30
# 設定密鑰,務必保持唯一性(生成方式,直接拷貝到main運作即可)Base64.encodeToString(CipherUtils.generateNewKey(128, "AES").getEncoded()) (預設啟動生成随機秘鑰,随機秘鑰會導緻之前用戶端RememberMe Cookie無效,如設定固定秘鑰RememberMe Cookie則有效)
cipherKey:
session:
# Session逾時時間,-1代表永不過期(預設30分鐘)
expireTime: 30
# 同步session到資料庫的周期(預設1分鐘)
dbSyncPeriod: 1
# 相隔多久檢查一次session的有效性,預設就是10分鐘
validationInterval: 10
# 同一個使用者最大會話數,比如2的意思是同一個賬号允許最多同時兩個人登入(預設-1不限制)
maxSession: -1
# 踢出之前登入的/之後登入的使用者,預設踢出之前登入的使用者
kickoutAfter: false
rememberMe:
# 是否開啟記住我
enabled: true
shiro配置代碼詳解
1. 加入 Ehcache 緩存管理器
CacheManager緩存控制器,來管理如使用者,角色,權限等的緩存的;因為這些資料基本上很少去改變,放到緩存中後可以提高通路的性能
EhCacheManager (com.ruoyi.framework.config.ShiroConfig)
若依架構中緩存配置使用 EhCacheManager ,配置檔案為 ehcache-shiro.xml 。
/**
* 緩存管理器 使用Ehcache實作
*/
@Bean
public EhCacheManager getEhCacheManager()
{
net.sf.ehcache.CacheManager cacheManager = net.sf.ehcache.CacheManager.getCacheManager("ruoyi");
EhCacheManager em = new EhCacheManager();
if (StringUtils.isNull(cacheManager))
{
em.setCacheManager(new net.sf.ehcache.CacheManager(getCacheManagerConfigFileInputStream()));
return em;
}
else
{
em.setCacheManager(cacheManager);
return em;
}
}
/**
* 傳回配置檔案流 避免ehcache配置檔案一直被占用,無法完全銷毀項目重新部署
*/
protected InputStream getCacheManagerConfigFileInputStream()
{
String configFile = "classpath:ehcache/ehcache-shiro.xml";
InputStream inputStream = null;
try
{
inputStream = ResourceUtils.getInputStreamForPath(configFile);
byte[] b = IOUtils.toByteArray(inputStream);
InputStream in = new ByteArrayInputStream(b);
return in;
}
catch (IOException e)
{
throw new ConfigurationException(
"Unable to obtain input stream for cacheManagerConfigFile [" + configFile + "]", e);
}
finally
{
IOUtils.closeQuietly(inputStream);
}
}
2. 加入自定義Realm
Realm域,Shiro從Realm擷取安全資料(如使用者,角色,權限),就是說SecurityManager要驗證使用者身份, 那麼它需要從Realm擷取相應的使用者進行比較以确定使用者身份是否合法;也需要從Realm得到使用者相應的角色/權限進行驗證使用者是否能進行操作;可以有1個或多個Realm,我們一般在應用中都需要實作自己的Realm
若依架構中也實作了自己的 Realm (com.ruoyi.framework.shiro.realm.UserRealm) ,在自定義Realm中主要重寫了兩個方法 doGetAuthorizationInfo (授權) ,doGetAuthenticationInfo (登入認證) 。
并且将 Realm 加入了緩存管理器中 (com.ruoyi.framework.config.ShiroConfig) 。
/**
* 自定義Realm
*/
@Bean
public UserRealm userRealm(EhCacheManager cacheManager)
{
UserRealm userRealm = new UserRealm();
userRealm.setAuthorizationCacheName(Constants.SYS_AUTH_CACHE);
userRealm.setCacheManager(cacheManager);
return userRealm;
}
其中清理所有使用者授權資訊緩存的調用時機為 更新菜單或者角色資訊,直接删除所有使用者的授權資訊,點解任意接口的時候會進行授權資訊的擷取,而這時的授權資訊的最新的,無需使用者登出再登入操作。
3. 加入 SecurityManager 安全管理器
Subject主體,代表了目前的“使用者”,這個使用者不一定是一個具體的人,與目前應用互動的任何東西都是Subject,如網絡爬蟲, 機器人等;即一個抽象概念;所有Subject都綁定到SercurityManager,與Subject的所有互動都會委托給SecurityManager;可以把Subject認為是一個門面;SecurityManager才是實際的執行者
SecurityManage安全管理器;即所有與安全有關的操作都會與SecurityManager互動;且它管理着所有Subject; 可以看出它是Shiro的核心,它負責與後邊介紹的其他元件進行互動
安全管理器 SecurityManager (com.ruoyi.framework.config.ShiroConfig)
/**
* 安全管理器
*/
@Bean
public SecurityManager securityManager(UserRealm userRealm)
{
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 設定realm.
securityManager.setRealm(userRealm);
// 記住我
securityManager.setRememberMeManager(rememberMe ? rememberMeManager() : null);
// 注入緩存管理器;
securityManager.setCacheManager(getEhCacheManager());
// session管理器
securityManager.setSessionManager(sessionManager());
return securityManager;
}
若依架構中 SecurityManager 主要參數有 Realm、RememberMe、CacheManager、SessionManager 等。
4. SessionManager、SessionDAO、SessionFactory
SessionManager如果寫過Servlet就應該知道Session的概念,Session需要有人去管理它的生命周期,這個元件就是SessionManager
SessionDAODAO大家都用過,資料庫通路對象,用于會話的CRUD,比如我們想把Session儲存到資料庫,那麼可以實作自己的SessionDAO,也可以寫入緩存,以提高性能
SessionManager (com.ruoyi.framework.config.ShiroConfig)
/**
* 會話管理器
*/
@Bean
public OnlineWebSessionManager sessionManager()
{
OnlineWebSessionManager manager = new OnlineWebSessionManager();
// 加入緩存管理器
manager.setCacheManager(getEhCacheManager());
// 删除過期的session
manager.setDeleteInvalidSessions(true);
// 設定全局session逾時時間
manager.setGlobalSessionTimeout(expireTime * 60 * 1000);
// 去掉 JSESSIONID
manager.setSessionIdUrlRewritingEnabled(false);
// 定義要使用的無效的Session定時排程器
manager.setSessionValidationScheduler(SpringUtils.getBean(SpringSessionValidationScheduler.class));
// 是否定時檢查session
manager.setSessionValidationSchedulerEnabled(true);
// 自定義SessionDao
manager.setSessionDAO(sessionDAO());
// 自定義sessionFactory
manager.setSessionFactory(sessionFactory());
return manager;
}
SessionFactory (com.ruoyi.framework.config.ShiroConfig)
/**
* 自定義sessionFactory會話
*/
@Bean
public OnlineSessionFactory sessionFactory()
{
OnlineSessionFactory sessionFactory = new OnlineSessionFactory();
return sessionFactory;
}
自定義sessionFactory會話 OnlineSessionFactory (com.ruoyi.framework.shiro.session.OnlineSessionFactory)
主要是從請求中擷取需要的資訊儲存到自定義對象 OnlineSession 中。
SessionDAO (com.ruoyi.framework.config.ShiroConfig)
/**
* 自定義sessionDAO會話
*/
@Bean
public OnlineSessionDAO sessionDAO()
{
OnlineSessionDAO sessionDAO = new OnlineSessionDAO();
return sessionDAO;
}
OnlineSessionDAO (com.ruoyi.framework.shiro.session.OnlineSessionDAO) 針對自定義的ShiroSession的db操作。
5. 設定 Shiro 過濾器配置
/**
* Shiro過濾器配置
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager)
{
CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
// Shiro的核心安全接口,這個屬性是必須的
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 身份認證失敗,則跳轉到登入頁面的配置
shiroFilterFactoryBean.setLoginUrl(loginUrl);
// 權限認證失敗,則跳轉到指定頁面
shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
// Shiro連接配接限制配置,即過濾鍊的定義
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 對靜态資源設定匿名通路
filterChainDefinitionMap.put("/favicon.ico**", "anon");
filterChainDefinitionMap.put("/ruoyi.png**", "anon");
filterChainDefinitionMap.put("/html/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/docs/**", "anon");
filterChainDefinitionMap.put("/fonts/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/ajax/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/ruoyi/**", "anon");
filterChainDefinitionMap.put("/captcha/captchaImage**", "anon");
// 退出 logout位址,shiro去清除session
filterChainDefinitionMap.put("/logout", "logout");
// 不需要攔截的通路
filterChainDefinitionMap.put("/login", "anon,captchaValidate");
// 注冊相關
filterChainDefinitionMap.put("/register", "anon,captchaValidate");
// 系統權限清單
// filterChainDefinitionMap.putAll(SpringUtils.getBean(IMenuService.class).selectPermsAll());
Map<String, Filter> filters = new LinkedHashMap<String, Filter>();
filters.put("onlineSession", onlineSessionFilter());
filters.put("syncOnlineSession", syncOnlineSessionFilter());
filters.put("captchaValidate", captchaValidateFilter());
filters.put("kickout", kickoutSessionFilter());
// 登出成功,則跳轉到指定頁面
filters.put("logout", logoutFilter());
shiroFilterFactoryBean.setFilters(filters);
// 所有請求需要認證
filterChainDefinitionMap.put("/**", "user,kickout,onlineSession,syncOnlineSession");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
限制同一使用者多裝置登入
/**
* 同一個使用者多裝置登入限制
*/
public KickoutSessionFilter kickoutSessionFilter()
{
KickoutSessionFilter kickoutSessionFilter = new KickoutSessionFilter();
kickoutSessionFilter.setCacheManager(getEhCacheManager());
kickoutSessionFilter.setSessionManager(sessionManager());
// 同一個使用者最大的會話數,預設-1無限制;比如2的意思是同一個使用者允許最多同時兩個人登入
kickoutSessionFilter.setMaxSession(maxSession);
// 是否踢出後來登入的,預設是false;即後者登入的使用者踢出前者登入的使用者;踢出順序
kickoutSessionFilter.setKickoutAfter(kickoutAfter);
// 被踢出後重定向到的位址;
kickoutSessionFilter.setKickoutUrl("/login?kickout=1");
return kickoutSessionFilter;
}
原理:不同裝置登入的時候會産生不同的sessionId,但是subject是一樣的,通過統計sessionId的個數來判斷使用者登入了多少個不同裝置
未來計劃
1、ruoyi非分離版本拆解
2、ruoyi-vue-pro:講解工作流
3、ruoyi-vue-pro:支付子產品,電商子產品
4、基于ruoyi-vue-pro項目開發
5、JEECG低代碼開發平台
請關注我,本星球會持續推出更多的開源項目代碼解析,如有更好的意見請留言回複或者私信。
#頭條創作挑戰賽#