Shiro在前后臺分離架構項目中的應用

Shiro是Apache的強大靈活的開源安全框架

能提供認證、授權、企業會話管理、安全加密、緩存等功能。

與Spring Security的比較

Apache Shiro Spring Security
簡單靈活 復雜、笨重
可脫離Spring 必須依賴Spring
粒度較粗 粒度更細

Shiro的幾個關鍵要素

  • Subject

    主體(官方解釋,不明白為毛要命名為主體,一眼看到這么個東西讓人很難理解),其實很簡單,Subject就是應用和Shiro管理器交流的橋梁,基本上所有對權限的操作都是通過Subject進行的,比如登錄,比如注銷,Subject就可以看成是Shiro里的用戶。

  • SecurityManager

    安全管理器,所有與安全相關的操作都會由SecurityManager來處理,而且,通過查看源碼可以看到,Subject的所有操作都是借助于SecurityManager來完成的,它是Shiro的核心。

  • Realm

    域(這個概念也是比較抽象的),可以有一個或多個,Shiro中所有的安全驗證數據都是由Realm提供的,而且Shiro不知道應用的權限存儲以何種方式存儲,所以我們一般都需要實現自己的Realm;可以這樣看,Subject提供驗證數據入口,Realm提供驗證的數據源,而真正的驗證功能由Shiro的認證器來完成。

  • Authenticator

    認證器,負責主體認證的,即認證器都用來實現用戶在什么情況下算是認證通過了。

  • Authrizer

    授權器,或者訪問控制器,用來對主體(Subject)進行授權,覺得主體有哪些操作的權限,能訪問應用中的那些功能。

  • SessionManager

    Session管理器,但是這個地方的Session與當初學習Servlet時接觸到的Session基本類似,但是這個Session是由Shiro自己去維護的,與Web環境無關,可以應用到Web環境中,也可以應用到普通的JavaSE環境。

  • SessionDAO

    數據訪問對象,用于會話的CRUD,比如將Session存儲到Redis,或者數據庫,或者內存,都可以通過SessionDAO來實現,可以使用默認的SessionDAO,也可以自定義實現。

  • CacheManager

    緩存控制器,用來管理用戶、角色、權限等的緩存。

  • Cryptography

    密碼模塊,Shiro提供了一些常見的加密組件用于密碼加密/解密。

Shiro內置的過濾器

  • anon,authBasic,authc,user,logout
  • perms,roles,ssl,port
過濾器簡稱過濾器簡稱 對應的java類
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter
logout org.apache.shiro.web.filter.authc.LogoutFilter

Shiro在前后臺分離架構的項目中的應用

Shiro在傳統web項目中的應用與前后臺分離項目中的區別

傳統項目中,前后臺在一個工程里,頁面的跳轉,請求的訪問,一般都是由后臺來控制,中間不需要做太多的轉換。

而在前后臺分離項目中,前后臺在不同的工程里,也在不同的服務器上,頁面的跳轉由前端路由來控制(其實也沒啥頁面的跳轉,隨著前端框架如雨后竹筍一般的冒出來,前端應用都往單頁面應用的方向發展),后臺只負責提供數據以及安全驗證,對于頁面的東西后臺已經不做關注。在這種情況下,在使用Shiro時就需要有一些自定義的東西了。

需要關注的幾個點

  • 通過Redis存儲Session
  • 由Shiro來跳轉的請求地址
  • 配置不需要驗證的請求接口

具體實現

作為一個SpringBoot洗腦流,不管是什么新東西,最先想到的就是通過SpringBoot來集成。這里通過SpringBoot,集成Shiro、Swagger(模擬前臺通過JSON請求后臺)、Redis(暫時只存儲Session),使用Swagger來模擬請求,測試Shiro的權限控制。

以下的集成相關東西,都是建立于一個完整的SpringBoot Demo。

  • 集成Redis

    引入Redis依賴

    <!-- Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    

    引入第三方Redis序列化工具

    <!-- 高效的序列化庫kyro -->
    <dependency>
        <groupId>com.esotericsoftware</groupId>
        <artifactId>kryo-shaded</artifactId>
        <version>4.0.0</version>
    </dependency>
    

    注: Kryo是一個快速高效的Java序列化框架,旨在提供快速、高效和易用的API。無論文件、數據庫或網絡數據Kryo都可以隨時完成序列化。Kryo還可以執行自動深拷貝(克隆)、淺拷貝(克隆)。這是對象到對象的直接拷貝,非對象->字節->對象的拷貝。在后面的文章會分析一下Redis各種序列化方式的效率。

    配置Redis連接(為了方便測試,使用Redis單機版即可)

    spring:
      redis:
        database: 0
        host: localhost
        password:  # Redis服務器若設置密碼,此處必須配置
        port: 6379
        timeout: 10000 # 連接超時時間(毫秒)
        pool:
          max-active: 8 # 連接池最大連接數(使用負數表示沒有限制)
          max-idle: 8 # 連接池中的最大空閑連接
          min-idle: 0 # 連接池中的最小空閑連接
          max-wait: -1 # 連接池最大阻塞等待時間(使用負數表示沒有限制)
    
  • Swagger的集成

    為了不重復造輪子,使用swagger-spring-boot-starter(一個大牛自己針對Swagger封裝的一個SpringBoot的Starter自動配置模塊)即可。

    <!-- swagger API集成 -->
    <dependency>
        <groupId>com.spring4all</groupId>
        <artifactId>swagger-spring-boot-starter</artifactId>
        <version>1.7.1.RELEASE</version>
    </dependency>
    

    在使用Shiro之后,由于默認情況下,資源都會被Shiro攔截,所以需要對Swagger的資源手動做加載,并使用@EnableSwagger2Doc打開Swagger自動配置,并且在下面shiro攔截器配置時,將swagger相關資源配置為anno。

    @Configuration
    @EnableSwagger2Doc
    public class SwaggerConfiguration extends WebMvcConfigurerAdapter {
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/js/**").addResourceLocations("classpath:/js/");
            registry.addResourceHandler("swagger-ui.html")
                    .addResourceLocations("classpath:/META-INF/resources/");
            registry.addResourceHandler("/webjars/**")
                    .addResourceLocations("classpath:/META-INF/resources/webjars/");
        }
    }
    

    配置Swagger

    swagger:
      title: 測試Demo
      description: 測試Demo
      version: 1.0.RELEASE
      license: Apache License, Version 2.0
      license-url: https://www.apache.org/licenses/LICENSE-2.0.html
      terms-of-service-url: https://github.com/dyc87112/spring-boot-starter-swagger
      base-package: com.example
      base-path: /**
      exclude-path: /error, /ops/**
    
  • Shiro集成

    引入Shiro官方提供的與Spring類項目集成的依賴包

    <!-- shiro begin -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>${shiro.version}</version>
    </dependency>
    <!-- shiro ehcache -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
        <version>${shiro.version}</version>
    </dependency>
    

    除了上面這兩個依賴包之外,以便于以后項目做集群,使用Redis存儲Shiro的安全驗證信息,所以在Github上翻了翻,找到了下面shiro-redis包,它很好的完成了Redis與Shiro的集成,不需要開發人員自己去編碼,實現Shiro的SessionDAO接口。

    <!-- shiro與Redis整合的開源插件 -->
    <dependency>
        <groupId>org.crazycake</groupId>
        <artifactId>shiro-redis</artifactId>
        <version>3.0.0</version>
    </dependency>
    

    還沒完,Shiro的常規配置還需要通過JavaConfig的方式去配置(以SpringBoot自動配置的方式實現),廢話少說,下面代碼見真章。

    shiro的相關攔截規則配置

    security:
      shiro:
        filter:
          anon:   # 不需要Shiro攔截的請求URL
            - /api/v1/**  # swagger接口文檔
            - /swagger-ui.html
            - /webjars/**
            - /swagger-resources/**
            - /user/login   # 登錄接口
            - /user/noLogin   # 未登錄提示信息接口
          authc:   # 需要Shiro攔截的請求URL
            - /**
        loginUrl: /user/login   # 登錄接口
        noAccessUrl: /user/noLogin   # 未登錄時跳轉URL
        globalSessionTimeout: 30  # 登錄過期時長
    

    自定義的Shiro屬性配置類ShiroProperties.java

    @Data
    @ConfigurationProperties(prefix = "security.shiro")
    public class ShiroProperties {
        /**
         * 登錄Url
         */
        private String loginUrl;
        /**
         * 沒權限訪問時的轉發Url(做未登錄提示信息用)
         */
        private String noAccessUrl;
        /**
         * Shiro請求攔截規則配置(Shiro的攔截器規則,常用的anon和authc)
         */
        private Map<String, List<String>> filter;
        /**
         * Shiro Session 過期時間(分鐘)
         */
        private Long globalSessionTimeout = 30L;
    }
    

    為解決前后臺分離架構的項目下,未登錄時訪問系統的跳轉及對應的提示信息Shiro原有邏輯為未登錄則跳轉到登錄Url,在前后臺分離架構下,此種方式顯然不能滿足要求,只能修改authc默認過濾器處理流程,通過將請求轉發到一個新的Url,給出未登錄提示信息,由前臺去控制路由跳轉到登錄頁面

    @Slf4j
    public class SelfDefinedFormAuthenticationFilter extends FormAuthenticationFilter {
        // 沒有權限訪問的提示信息跳轉URL
        private String noAccessUrl;
        public String getNoAccessUrl() {
            return noAccessUrl;
        }
        public SelfDefinedFormAuthenticationFilter setNoAccessUrl(String noAccessUrl) {
            this.noAccessUrl = noAccessUrl;
            return this;
        }
        // 重寫跳轉到登錄URL的邏輯,改為轉發到未登錄URL
        @Override
        protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
            String noAccessUrl = getNoAccessUrl();
            try {
                request.getRequestDispatcher(noAccessUrl).forward(request, response);
            } catch (ServletException e) {
                e.getMessage();
            }
        }
    }
    

    自定義Realm,提供登錄驗證數據及授權邏輯

    @Slf4j
    @Component
    public class SelfDefinedShiroRealm extends AuthorizingRealm {
        /**
         * 授權
         * @param principals
         * @return
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
            return authorizationInfo;
        }
        /**
         * 認證
         * @param token
         * @return
         * @throws AuthenticationException
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
                throws AuthenticationException {
            String username = (String) token.getPrincipal();
            log.info(username);
            SimpleAuthenticationInfo authorizationInfo = new SimpleAuthenticationInfo(
                    new User(username, "123"),
                    username,
                    getName()
            );
            return authorizationInfo;
        }
    }
    

    新建配置類,配置Shiro相關配置。

    @Configuration
    @EnableConfigurationProperties(ShiroProperties.class)
    public class ShiroConfiguration {
        @Autowired
        private RedisProperties redisProperties;
        @Autowired
        private ShiroProperties shiroProperties;
        @Bean
        public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            //獲取filters
            Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
            //將自定義 的FormAuthenticationFilter注入shiroFilter中
            filters.put("authc", new SelfDefinedFormAuthenticationFilter().
                    setNoAccessUrl(shiroProperties.getNoAccessUrl()));
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
            //注意過濾器配置順序 不能顛倒
            Map<String, List<String>> filterMap = shiroProperties.getFilter();
            filterMap.forEach((filter, urls) -> {
                urls.forEach(url -> {
                    filterChainDefinitionMap.put(url, filter);
                });
            });
            // 配置shiro默認登錄界面地址,前后端分離中登錄界面跳轉應由前端路由控制,后臺僅返回json數據
    shiroFilterFactoryBean.setLoginUrl(shiroProperties.getLoginUrl());
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
            return shiroFilterFactoryBean;
        }
        /**
         * 憑證匹配器(密碼需要加密時,可使用)
         * @return
         */
        @Bean
        public HashedCredentialsMatcher hashedCredentialsMatcher() {
            HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
            // 設置加密算法 Md5Hash
            hashedCredentialsMatcher.setHashAlgorithmName("md5");
            // 設置散列加密次數 如:2=md5(md5(aaa))
            hashedCredentialsMatcher.setHashIterations(2);
            return hashedCredentialsMatcher;
        }
        @Bean
        public SecurityManager securityManager(
                AuthorizingRealm authorizingRealm,
                SessionManager sessionManager,
                RedisCacheManager redisCacheManager) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(authorizingRealm);
            // 自定義的Session管理
            securityManager.setSessionManager(sessionManager);
            // 自定義的緩存實現
            securityManager.setCacheManager(redisCacheManager);
            return securityManager;
        }
        /**
         * 自定義的SessionManager
         * @param redisSessionDAO
         * @return
         */
        @Bean
        public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
            SelfDefinedSessionManager sessionManager = new SelfDefinedSessionManager();
            sessionManager.setSessionDAO(redisSessionDAO);                sessionManager.setGlobalSessionTimeout(shiroProperties.getGlobalSessionTimeout() * 60 * 1000);
            return sessionManager;
        }
        /**
         * 配置shiro redisManager
         * 使用的是shiro-redis開源插件
         * @return
         */
        @Bean
        public RedisManager redisManager() {
            RedisManager redisManager = new RedisManager();
            redisManager.setHost(redisProperties.getHost());
            redisManager.setPort(redisProperties.getPort());
            redisManager.setTimeout(redisProperties.getTimeout());
            if (!ObjectUtils.isEmpty(redisProperties.getPassword())) {
                redisManager.setPassword(redisProperties.getPassword());
            }
            return redisManager;
        }
        /**
         * cacheManager 緩存 redis實現
         * 使用的是shiro-redis開源插件
         * @param redisManager
         * @return
         */
        @Bean
        public RedisCacheManager redisCacheManager(RedisManager redisManager) {
            RedisCacheManager redisCacheManager = new RedisCacheManager();
            redisCacheManager.setRedisManager(redisManager);
            redisCacheManager.setValueSerializer(new StringSerializer());
            return redisCacheManager;
        }
        /**
         * RedisSessionDAO shiro sessionDao層的實現 redis實現
         * 使用的是shiro-redis開源插件
         * @param redisManager
         * @return
         */
        @Bean
        public RedisSessionDAO redisSessionDAO(RedisManager redisManager) {
            RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
            redisSessionDAO.setRedisManager(redisManager);
            return redisSessionDAO;
        }
        /**
         * 開啟shiro aop注解支持
         * @param securityManager
         * @return
         */
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =
                    new AuthorizationAttributeSourceAdvisor();
          authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }
    }
    
  • 編寫簡單的Controller,測試一下

    UserController.java

      @Autowired
        private RedisSessionDAO redisSessionDAO;
        @ApiOperation("登錄")
        @PostMapping("/login")
        public Object login(@RequestBody User user) {
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getPassword());
            try {
                // 登錄
                subject.login(token);
                // 登錄成功后,獲取菜單權限信息
                if (subject.isAuthenticated()) {
                    return "登錄成功";
                }
            } catch (IncorrectCredentialsException e) {
                return "密碼錯誤";
            } catch (LockedAccountException e) {
                return "登錄失敗,該用戶已被凍結";
            } catch (AuthenticationException e) {
                return "該用戶不存在";
            } catch (Exception e) {
                return e.getMessage();
            }
            return "登錄失敗";
        }
        @ApiOperation("注銷")
        @PostMapping("/logout")
        public Object logout() {
            Subject subject = SecurityUtils.getSubject();
            redisSessionDAO.delete(subject.getSession());
            return "注銷成功";
        }
        @ApiOperation("未登錄提示信息接口")
        @RequestMapping("/noLogin")
        public Object noLogin() {
            return "未登錄,請先登錄再訪問";
        }
        @ApiOperation("需登錄才能訪問")
        @PostMapping("/home")
        public Object home() {
            return "這是主頁";
        }
    

    訪問http://localhost:8080/shiro/swagger-ui.html頁面,通過Swagger測試請求的攔截。

    1. 未登錄訪問/user/home

      返回信息“未登錄,請先登錄再訪問”,代表請求成功攔截到了,未登錄不能正常訪問系統

    2. 訪問/user/login進行登錄,然后訪問/user/home

      入參:

      {
          "userName":"admin",
          "password":"123"
      }
      

      出參:

      "登錄成功"
      

      然后訪問/user/home,成功返回"這是主頁"

    3. 注銷后在訪問/user/home

      直接請求/user/logout,訪問/user/home,提示“未登錄,請先登錄再訪問”,表示成功注銷。

    注: /user/noLogin使用的是@RequestMapping("/noLogin"),是為了保證所有請求方式(GET/POST/PUT/DELETE等)的未登錄請求都能轉發到此接口,從而正確返回未登錄提示信息。

以上相關源碼,請訪問https://github.com/ArtIsLong/shiro-spring-boot-starter.git


關注我的微信公眾號:FramePower
我會不定期發布相關技術積累,歡迎對技術有追求、志同道合的朋友加入,一起學習成長!


微信公眾號
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容