天天看點

若依非分離版-第六十一章:Spring boot shiro安全架構

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才是實際的執行者

若依非分離版-第六十一章:Spring boot shiro安全架構

SecurityManager:安全管理器;即所有與安全有關的操作都會與SecurityManager互動;且其管理着所有Subject;可以看出它是Shiro的核心,它負責與Shiro的其他元件進行互動,它相當于SpringMVC中DispatcherServlet的角色

Realm:Shiro從Realm 擷取安全資料(如使用者、角色、權限),就是說SecurityManager要驗證使用者身份,那麼它需要從Realm 擷取相應的使用者進行比較以确定使用者身份是否合法;也需要從Realm 得到使用者相應的角色/權限進行驗證使用者是否能進行操作;可以把Realm 看成DataSource

導入shiro依賴

若依非分離版-第六十一章:Spring boot shiro安全架構

shiro配置資訊

在application.yml檔案,路徑如下圖

若依非分離版-第六十一章:Spring boot shiro安全架構
# 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的個數來判斷使用者登入了多少個不同裝置

若依非分離版-第六十一章:Spring boot shiro安全架構

未來計劃

1、ruoyi非分離版本拆解

2、ruoyi-vue-pro:講解工作流

3、ruoyi-vue-pro:支付子產品,電商子產品

4、基于ruoyi-vue-pro項目開發

5、JEECG低代碼開發平台

請關注我,本星球會持續推出更多的開源項目代碼解析,如有更好的意見請留言回複或者私信。

#頭條創作挑戰賽#