Java-Shiro(九):Shiro集成Redis實現Session統一管理


聲明:本證項目基於《Java-Shiro(六):Shiro Realm講解(三)Realm的自定義及應用》構建項目為基礎。

版本源碼:https://github.com/478632418/springmv_without_web_xml/tree/master/mybaits-test-dynamic-sql-03

在實際應用中使用Redis管理Shiro默認的Session(SessionManager)是必要的,因為默認的SessionManager內部默認采用了內存方式存儲Session相關信息();當配置了內部cacheManager時(默認配置采用EhCache--內存或磁盤緩存),會將已經登錄的用戶的Session信息存儲到內存或磁盤。無論是采用純內存方式或者EhCache(內存或磁盤)方式都不適合企業生產應用(特別並發認證用戶較多的系統)。

閱讀本章請帶着這幾個問題:

1)如何集成redis存儲認證信息,實現分布式session一致?
2)如何統計在線用戶數?
3)如何剔除用戶?
4)如何實現一個用戶最多允許登錄幾次(單點登錄)?
5)當一個用戶已經登錄 或者 rememberMe,后台管理員修改了該用戶的角色,或者調整了(增、刪、改)角色與資源之間的關系,登錄用戶的角色、資源信息如何同步被修改?
6)修改(增、刪、改)資源信息,資源信息的url如何動態添加到shiroFilter.filterChainDefinitions?

准備

1)新建maven項目pom.xml配置

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <org.springframework.version>5.2.0.RELEASE</org.springframework.version>
        <com.alibaba.version>1.1.21</com.alibaba.version>
        <mysql.version>8.0.11</mysql.version>
        <org.mybatis.version>3.4.6</org.mybatis.version>
        <org.mybatis.spring.version>2.0.3</org.mybatis.spring.version>
        <org.aspectj.version>1.9.4</org.aspectj.version>
        <jackson.version>2.10.1</jackson.version>
        <shiro.version>1.4.2</shiro.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${org.springframework.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${org.springframework.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>${org.springframework.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${org.springframework.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${org.springframework.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${org.springframework.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${org.springframework.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>${org.springframework.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>${org.springframework.version}</version>
        </dependency>

        <!--AOP aspectjweaver 支持 -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>${org.aspectj.version}</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjrt -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>${org.aspectj.version}</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.thymeleaf/thymeleaf -->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf</artifactId>
            <version>3.0.9.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.thymeleaf/thymeleaf-spring5 -->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
            <version>3.0.9.RELEASE</version>
        </dependency>

        <!--訪問RDBMS-MySQL依賴 -->
        <!--MyBatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>${org.mybatis.version}</version>
        </dependency>
        <!-- Mybatis自身實現的Spring整合依賴 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>${org.mybatis.spring.version}</version>
        </dependency>

        <!--MySql數據庫驅動 -->
        <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${com.alibaba.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>

        <!--Rest Support支持 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.module</groupId>
            <artifactId>jackson-module-parameter-names</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <!--form 設置為enctype="multipart/form-data",多文件上傳,在applicationContext.xml中配置了bean 
            multipartResolver時,需要依賴該包。 -->
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.4</version>
        </dependency>

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.5</version>
        </dependency>

        <!-- 編譯依賴 -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

        <!--日志支持 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.26</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.26</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>${shiro.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>${shiro.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-cas</artifactId>
            <version>${shiro.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${shiro.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>${shiro.version}</version>
        </dependency>
        <!--thymeleaf-shiro-extras-->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>
        
        <!-- redis依賴包 -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-redis -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>2.2.3.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.13</version>
        </dependency>
        
        <!-- https://mvnrepository.com/artifact/commons-lang/commons-lang -->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/junit/junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <!-- <scope>test</scope> -->
        </dependency>
    </dependencies>
    
    <repositories>
        <repository>
            <id>aliyun_maven</id>
            <name>aliyun maven</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public</url>
            <releases>
                <enabled>false</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>
View Code

2)web.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
    id="WebApp_ID" version="3.1">
    <display-name>ssms</display-name>
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
        <welcome-file>index.htm</welcome-file>
        <welcome-file>index.jsp</welcome-file>
        <welcome-file>default.html</welcome-file>
        <welcome-file>default.htm</welcome-file>
        <welcome-file>default.jsp</welcome-file>
        <welcome-file>/index</welcome-file>
    </welcome-file-list>
    <!-- 加載spring容器 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:appplcationContext-base.xml,
            classpath:applicationContext-redis.xml,
            classpath:applicationContext-shiro.xml,
            classpath:applicationContext-mybatis.xml
        </param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!-- Shiro Filter is defined in the spring application context: -->
    <!-- 1. 配置 Shiro 的 shiroFilter.                               <br>
         2. DelegatingFilterProxy 實際上是 Filter 的一個代理對象. 默認情況下, Spring 會到 IOC 容器中查找和 <filter-name> 對應的 filter bean. 
         也可以通過 targetBeanName 的初始化參數來配置 filter bean 的 id. -->
    <filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <param-name>targetFilterLifecycle</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- 文件上傳與下載過濾器:form表單中存在文件時,該過濾器可以處理http請求中的文件,被該過濾器過濾后會用post方法提交, form表單需設為enctype="multipart/form-data" -->
    <!-- 注意:必須放在HiddenHttpMethodFilter過濾器之前 -->
    <filter>
        <filter-name>multipartFilter</filter-name>
        <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
        <init-param>
            <param-name>multipartResolverBeanName</param-name>
            <!--spring中配置的id為multipartResolver的解析器 -->
            <param-value>multipartResolver</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>multipartFilter</filter-name>
        <!--<servlet-name>springmvc</servlet-name> -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- 注意:HiddenHttpMethodFilter必須作用於dispatcher前 請求method支持 put 和 delete 必須添加該過濾器 
        作用:可以過濾所有請求,並可以分為四種 使用該過濾器需要在前端頁面加隱藏表單域 <input type="hidden" name="_method" 
        value="請求方式(put/delete)"> post會尋找_method中的請求式是不是put 或者 delete,如果不是 則默認post請求 -->
    <filter>
        <filter-name>hiddenHttpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
        <!--可以通過配置覆蓋默認'_method'值 -->
        <init-param>
            <param-name>methodParam</param-name>
            <param-value>_method</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>hiddenHttpMethodFilter</filter-name>
        <!--servlet為springMvc的servlet名 -->
        <servlet-name>springmvc</servlet-name>
        <!--<url-pattern>/*</url-pattern> -->
    </filter-mapping>

    <!-- 后端數據輸出到前端亂碼問題 -->
    <filter>
        <filter-name>characterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>characterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- springmvc前端控制器 -->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc-servlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>
View Code

注意:

1)在web.xml中引入shiroFilter、multipartFilter、hiddenHttpMethodFilter、characterEncodingFilter;

2)ContextLoaderListener需要加載applicationContext-base.xml、applicaitonContext-mybatis.xml、applicationContext-shiro.xml、applicationContext-redis.xml 4個配置文件;

3)DispatcherServlet需要加載springmvc-servlet.xml配置文件。

3)springmvc-servlet.xml

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.0.xsd 
        http://www.springframework.org/schema/mvc 
        http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd 
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/aop 
        http://www.springframework.org/schema/aop/spring-aop-4.0.xsd 
        http://www.springframework.org/schema/tx 
        http://www.springframework.org/schema/tx/spring-tx-4.0.xsd ">

    <!-- 開啟controller注解支持 -->
    <!-- 注意事項請參考:http://jinnianshilongnian.iteye.com/blog/1762632 -->
    <context:component-scan base-package="com.dx.test.controller" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        <context:include-filter type="annotation"
                                expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
    </context:component-scan>
    <!--使用mvc:annotation-driven代替上邊注解映射器和注解適配器 配置 如果使用mvc:annotation-driven就不用配置上面的
        RequestMappingHandlerMapping和RequestMappingHandlerAdapter-->
    <!-- 使用注解驅動:自動配置處理器映射器與處理器適配器 -->
    <!-- <mvc:annotation-driven /> -->
    <mvc:annotation-driven></mvc:annotation-driven>

    <!-- 開啟aop,對類代理 -->
    <aop:config proxy-target-class="true"></aop:config>

    <!-- 單獨使用jsp視圖解析器時,可以取消掉注釋,同時注釋掉:下邊的‘配置多個視圖解析’配置-->
    <!--
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/view/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
    -->

    <!-- 使用thymeleaf解析  -->
    <bean id="templateResolver" class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
        <property name="prefix" value="/WEB-INF/templates/"/>
        <!--<property name="suffix" value=".html" />-->
        <property name="templateMode" value="HTML"/>
        <property name="characterEncoding" value="UTF-8"/>
        <property name="cacheable" value="false"/>
    </bean>

    <bean id="templateEngine" class="org.thymeleaf.spring5.SpringTemplateEngine">
        <property name="templateResolver" ref="templateResolver"/>
        <property name="additionalDialects">
            <set>
                <bean class="at.pollux.thymeleaf.shiro.dialect.ShiroDialect"/>
            </set>
        </property>
    </bean>

    <!--單獨使用thymeleaf視圖引擎時,可以取消掉注釋,同時注釋掉:下邊的‘配置多個視圖解析’配置 -->
    <!--
    <bean class="org.thymeleaf.spring5.view.ThymeleafViewResolver">  
      <property name="templateEngine" ref="templateEngine" />  
      <property name="characterEncoding" value="UTF-8"/>  
    </bean>
    -->

    <!--  配置多個視圖解析 參考:https://blog.csdn.net/qq_19408473/article/details/71214972-->
    <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
        <property name="viewResolvers">
            <!--
            此時,
            返回視圖:return "abc.jsp" ,將使用jsp視圖解析器,jsp的視圖模板文件在/WEB-INF/views/下;
            返回視圖:return "abc.html",將使用 thymeleaf視圖解析器,thymeleaf的視圖模板文件在/WEB-INF/templates/下。
            -->
            <list>
                <!--used thymeleaf  -->
                <bean class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
                    <property name="characterEncoding" value="UTF-8"/>
                    <property name="templateEngine" ref="templateEngine"/>
                    <property name="viewNames" value="*.html"/>
                    <property name="order" value="2"/>
                </bean>
                <!-- used jsp -->
                <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
                    <property name="prefix" value="/WEB-INF/views/"/>
                    <!--<property name="suffix" value=".jsp"/>-->
                    <property name="viewNames" value="*.jsp"/>
                    <property name="order" value="1"/>
                </bean>
            </list>
        </property>
    </bean>

</beans>
View Code

注意:

1)配置文件中配置了兩個視圖引擎:jsp、thymeleaf。

返回視圖:return "abc.jsp" ,將使用jsp視圖解析器,jsp的視圖模板文件在/WEB-INF/views/下;
返回視圖:return "abc.html",將使用 thymeleaf視圖解析器,thymeleaf的視圖模板文件在/WEB-INF/templates/下。

2)開啟aop,對類代理<aop:config proxy-target-class="true"></aop:config>

3)開啟controller注解支持<context:component-scan base-package="com.dx.test.controller" use-default-filters="false">...</context:component-scan>

4)使用注解驅動:自動配置處理器映射器與處理器適配器 <mvc:annotation-driven></mvc:annotation-driven>

5)關於thymeleaf視圖引擎需要注意:引入了解析thymeleaf *.html中shiro標簽處理,在templateEngine bean下設置了additionalDialects屬性。具體處理請參考《Java-Shiro(八):Shiro集成SpringMvc、Themeleaf,如何實現Themeleaf視圖引擎下解析*.html中shiro權限驗證

4)applicaitonContext-base.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 掃描Service、Dao里面的注解,這里沒有定義service -->
    <context:component-scan base-package="com.dx.test.dao"/>
    <!-- 掃描@Controller注解類 -->
    <context:component-scan base-package="com.dx.test.controller"/>
    <!-- 加載Listener component -->
    <context:component-scan base-package="com.dx.test.listener"/>
    <!-- 掃描shrio相關類(包含了@Service ShiroService組件) -->
    <context:component-scan base-package="com.dx.test.shiro"/>

    <!-- 文件上傳注意id -->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!-- 配置默認編碼 -->
        <property name="defaultEncoding" value="utf-8"></property>
        <!-- 配置文件上傳的大小 -->
        <property name="maxUploadSize" value="1048576"></property>
    </bean>

</beans>
View Code

注解:該配置文件主要用來指定系統需要掃描哪幾個包下類:

1)掃描包含@Service注解的包(dao/service相關類);

2)掃描包含@Controller注解的包;

3)掃描shiro定義的組件先關包(shiro包下定了@Service修飾的ShiroService);

4)掃描listener下的包(@Component修飾的ApplicationListener目的實現項目啟動后執行業務操作);

5)定義上傳組件bean。

4)applicaitonContext-mybatis.xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-4.0.xsd 
        http://www.springframework.org/schema/mvc 
        http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd 
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context-4.0.xsd 
        http://www.springframework.org/schema/aop 
        http://www.springframework.org/schema/aop/spring-aop-4.0.xsd 
        http://www.springframework.org/schema/tx 
        http://www.springframework.org/schema/tx/spring-tx-4.0.xsd ">

    <!-- 數據庫連接池配置文件Dao層 -->
    <!-- 加載配置文件 -->
    <context:property-placeholder location="classpath:jdbc.properties" ignore-unresolvable="true" />
    
    <!-- 數據庫連接池,使用dbcp -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="driverClassName" value="${jdbc.driver}" />
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="maxActive" value="10"/>
        <property name="maxIdle" value="5"/>
    </bean>
    <!-- sqlSessionFactory配置 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <!-- 配置MyBaties全局配置文件:mybatis-config.xml -->
        <property name="configLocation" value="classpath:mybatisConfig.xml" />
        <!-- 掃描entity包 使用別名 -->
        <!-- <property name="typeAliasesPackage" value="com.dx.test.model" /> -->
        <!-- 掃描sql配置文件:mapper需要的xml文件 -->
        <property name="mapperLocations" value="classpath:*dao/*.xml" />
    </bean>

    <!-- 4.配置掃描Dao接口包,動態實現Dao接口,注入到spring容器中 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- 注入sqlSessionFactory -->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
        <!-- 給出需要掃描Dao接口包 -->
        <property name="basePackage" value="com.dx.test.dao" />
    </bean>

    <!-- 事務管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>    
    </bean>
    
 </beans>
View Code

其中配置中依賴了jdbc.properties

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
jdbc.username=root
jdbc.password=123456

備注:

2)配置文件中主要配置了mybatis依賴的dataSource bean,以及sqlSessionFactory bean,MapperScannerConfigurer掃描@Mapper定義或者*mapper.xml

3)配置事務管理器 transactionManager。

5)applicationContext-redis.xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
    http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
      <!-- 加載配置文件 -->
    <context:property-placeholder location="classpath:jedis.properties" ignore-unresolvable="true" />
    
    <!-- 連接池配置 -->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <!-- 最大連接數 -->
        <property name="maxTotal" value="${redis.maxTotal}" />
        <!-- 最大空閑連接數 -->
        <property name="maxIdle" value="${redis.maxIdle}" />
        <!-- 每次釋放連接的最大數目 -->
        <property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}" />
        <!-- 釋放連接的掃描間隔(毫秒) -->
        <property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}" />
        <!-- 連接最小空閑時間 -->
        <property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}" />
        <!-- 連接空閑多久后釋放, 當空閑時間>該值 且 空閑連接>最大空閑連接數 時直接釋放 -->
        <property name="softMinEvictableIdleTimeMillis" value="${redis.softMinEvictableIdleTimeMillis}" />
        <!-- 獲取連接時的最大等待毫秒數,小於零:阻塞不確定的時間,默認-1 -->
        <property name="maxWaitMillis" value="${redis.maxWaitMillis}" />
        <!-- 在獲取連接的時候檢查有效性, 默認false -->
        <property name="testOnBorrow" value="${redis.testOnBorrow}" />
        <!-- 在空閑時檢查有效性, 默認false -->
        <property name="testWhileIdle" value="${redis.testWhileIdle}" />
        <!-- 連接耗盡時是否阻塞, false報異常,ture阻塞直到超時, 默認true -->
        <property name="blockWhenExhausted" value="${redis.blockWhenExhausted}" />
    </bean>
 
    <bean id="jedisPool" class="redis.clients.jedis.JedisPool">
        <constructor-arg name="host" value="${redis.host}"></constructor-arg>
        <constructor-arg name="port" value="${redis.port}"></constructor-arg>
        <constructor-arg name="poolConfig" ref="jedisPoolConfig"></constructor-arg>
    </bean>

    <!-- 需要密碼 -->
    <bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
          p:host-name="${redis.host}"
          p:port="${redis.port}"
          p:password="${redis.pass}"
          p:pool-config-ref="jedisPoolConfig"/>

    <bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
        <property name="connectionFactory"     ref="connectionFactory" />
        <property name="keySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="valueSerializer">
            <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
        </property>
    </bean>
</beans>
View Code

上線配置文件中依賴了jedis.properties文件內容:

redis.maxTotal=2000
redis.maxIdle=50
redis.numTestsPerEvictionRun=1024
redis.timeBetweenEvictionRunsMillis=30000
redis.minEvictableIdleTimeMillis=1800000
redis.softMinEvictableIdleTimeMillis=10000
redis.maxWaitMillis=15000
redis.testOnBorrow=false
redis.testWhileIdle=false
redis.testOnReturn=false
redis.blockWhenExhausted=true
redis.host=127.0.0.1
redis.port=6379
redis.pass=

備注:

文件主要配置兩種用來操作redis的bean:

1)定義了redis-client下redisPool bean;

2)定義了spring-data下redisTemplate bean。

6)applicationContext-shiro.xml

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
    http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">

    <!-- 憑證匹配器 -->
    <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
        <!-- 加密算法 -->
        <property name="hashAlgorithmName" value="md5"></property>
        <!-- 迭代次數 -->
        <property name="hashIterations" value="8"></property>
    </bean>

    <!-- 配置自定義Realm -->
    <bean id="myRealm" class="com.dx.test.shiro.MyRealm">
        <!-- 將憑證匹配器設置到realm中,realm按照憑證匹配器的要求進行散列 -->
        <property name="credentialsMatcher" ref="credentialsMatcher"></property>
        <!--啟用緩存,默認SimpleAccountRealm關閉,默認AuthenticatingRealm、AuthorizingRealm、CachingRealm開啟-->
        <property name="cachingEnabled" value="true"/>
        <!-- 一般情況下不需要對 認證信息進行緩存 -->
        <!--啟用身份驗證緩存,即緩存AuthenticationInfo,默認false-->
        <property name="authenticationCachingEnabled" value="false"/>
        <!--啟用授權緩存,即緩存AuthorizationInfo的信息,默認為true-->
        <property name="authorizationCachingEnabled" value="true"/>
        <!--<property name="authenticationCacheName" value="authenticationCache"></property>-->
        <!--<property name="authenticationCache" ref="redisCache"></property>-->
    </bean>
    <!--cacheManager-->
    <!-- // 采用EHCache混合緩存
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/>
    </bean>
    -->
    <!-- // 采用本地內存方式緩存
    <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/>
    -->
    <bean id="redisCache" class="com.dx.test.shiro.RedisCache">
        <constructor-arg name="timeout" value="30"></constructor-arg>
        <constructor-arg name="redisTemplate" ref="redisTemplate"></constructor-arg>
    </bean>

    <bean id="cacheManager" class="com.dx.test.shiro.RedisCacheManager">
        <property name="keyPrefix" value="shiro_redis_cache:"></property>
        <property name="redisTemplate" ref="redisTemplate"></property>
        <property name="timeout" value="30"></property>
    </bean>

    <!-- sessionIdCookie的實現,用於重寫覆蓋容器默認的JSESSIONID -->
    <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
        <!-- cookie的name,對應的默認是 JSESSIONID -->
        <constructor-arg name="name" value="SHAREJSESSIONID"/>
        <!-- jsessionId的path為 / 用於多個系統共享jsessionId -->
        <property name="path" value="/"/>
    </bean>

    <bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"></bean>
    <bean id="sessionDao" class="com.dx.test.shiro.RedisSessionDao">
        <property name="keyPrefix" value="shiro_redis_session:"></property>
        <property name="redisTemplate" ref="redisTemplate"></property>
        <property name="sessionIdGenerator" ref="sessionIdGenerator"></property>
        <property name="sessionTimeout" value="30"></property>
    </bean>

    <!-- 會話管理器-->
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <!--刪除在session過期時跳轉頁面時自動在URL中添加JSESSIONID-->
        <property name="sessionIdUrlRewritingEnabled" value="false"/>
        <!-- 設置超時時間 -->
        <property name="globalSessionTimeout" value="1800000"/>
        <!-- 刪除失效的session -->
        <property name="deleteInvalidSessions" value="true"/>
        <!-- 定時檢查失效的session -->
        <property name="sessionValidationSchedulerEnabled" value="true"/>
        <!-- 集群共享session -->
        <property name="sessionIdCookieEnabled" value="true"/>
        <property name="sessionIdCookie" ref="sessionIdCookie"/>
        <property name="sessionDAO" ref="sessionDao"/>
    </bean>

    <!--手動指定cookie-->
    <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
        <constructor-arg value="rememberMe"/>
        <property name="httpOnly" value="true"/>
        <!-- 7天 -->
        <property name="maxAge" value="604800"/>
        <property name="domain" value="*"/>
        <property name="path" value="/"/>
    </bean>
    <!-- rememberMe管理器 -->
    <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
        <!--注入自定義cookie(主要是設置壽命, 默認的一年太長)-->
        <property name="cookie" ref="rememberMeCookie"/>
    </bean>

    <!-- securityManager安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!--<property name="realm" ref="myRealm"></property>-->
        <property name="realms">
            <list>
                <ref bean="myRealm"></ref>
            </list>
        </property>
        <property name="cacheManager" ref="cacheManager"></property>
        <property name="sessionManager" ref="sessionManager"></property>
        <property name="rememberMeManager" ref="rememberMeManager"></property>
    </bean>

    <bean id="kickout" class="com.dx.test.shiro.KickoutSessionFilter">
        <constructor-arg name="sessionManager" ref="sessionManager"></constructor-arg>
        <constructor-arg name="cacheName" value="shiro_redis_kickout_cache"></constructor-arg>
        <constructor-arg name="cacheManager" ref="cacheManager"></constructor-arg>
        <constructor-arg name="kickoutAfter" value="true"></constructor-arg>
        <constructor-arg name="kickoutUrl" value="/login"></constructor-arg>
        <constructor-arg name="maxSession" value="2"></constructor-arg>
    </bean>

    <!-- id屬性值要對應 web.xml中shiro的filter對應的bean -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"></property>
        <!-- loginUrl認證提交地址,如果沒有認證將會請求此地址進行認證,請求地址將由formAuthenticationFilter進行表單認證 -->
        <property name="loginUrl" value="/login"></property>
        <!-- 認證成功統一跳轉到first.action,建議不配置,shiro認證成功會默認跳轉到上一個請求路徑 -->
        <!-- <property name="successUrl" value="/first.action"></property> -->
        <!-- 通過unauthorizedUrl指定沒有權限操作時跳轉頁面,這個位置會攔截不到,下面有給出解決方法 -->
        <!-- <property name="unauthorizedUrl" value="/refuse.jsp"></property> -->
        <property name="filters">
            <util:map>
                <entry key="kickout" value-ref="kickout"></entry>
            </util:map>
        </property>
        <!-- 過濾器定義,從上到下執行,一般將/**放在最下面 -->
        <property name="filterChainDefinitions">
            <!--
            過濾器簡稱        對應的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
            ————————————————
            版權聲明:本文為CSDN博主「a745233700」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
            原文鏈接:https://blog.csdn.net/a745233700/article/details/81350191
            -->
            <value>
                # 對靜態資源設置匿名訪問
                /images/** = anon
                /js/** = anon
                /styles/** = anon
                /validatecode.jsp=anon
                /index=anon

                # 請求logout.action地址,shiro去清除session
                /logout.action = logout

                # /**=anon 所有的url都可以匿名訪問,不能配置在最后一排,不然所有的請求都不會攔截
                # /**=authc 所有的url都必須通過認證才可以訪問
                /** = kickout,authc
            </value>
        </property>
    </bean>

    <!-- 解決shiro配置的沒有權限訪問時,unauthorizedUrl不跳轉到指定路徑的問題 -->
    <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
            <props>
                <!--登錄-->
                <prop key="org.apache.shiro.authz.UnauthenticatedException">
                    redirect:/web/page/login.do
                </prop>
                <!--授權-->
                <prop key="org.apache.shiro.authz.UnauthorizedException">
                    redirect:/web/page/unauthorized.do
                </prop>
            </props>
        </property>
        <property name="defaultErrorView" value="/index/error.do"/>
    </bean>

    <!-- 保證實現了Shiro內部lifecycle函數的bean執行 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

    <!-- 配置啟用Shiro的注解功能 -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
          depends-on="lifecycleBeanPostProcessor">
        <property name="proxyTargetClass" value="true"></property>
    </bean>
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>

</beans>
View Code

備注:

applicaitonContext-shiro.xml配置內容包含:
1)自定myRealm,指定shiro是否開啟認證、授權緩存,指定shiro的憑證匹配器credentialsMatcher;

2)配置啟用Shiro的注解功能,配置了DefaultAdvisorAutoProxyCreator、AuthorizationAttributeSourceAdvisor、lifecycleBeanPostProcessor幾個bean;

3)定了cacheManager(記錄緩存授權信息到redis)、sessionDao(用來記錄用戶session對象到redis) bean;

4)另外還定義了sessionManager(內部依賴於sessionDao、sessionIdCookie、sessionIdGenerate bean)、rememberMeManager(內部依賴於rememberMeCookie)、cacheManager 基本bean,都指定給了securityManager bean的屬性;

5)定了kickout,用來實現將在認證用戶與sessionId關聯起來,實現方式在redis中記錄用戶和sessionId;

6)shiroFilter是shiro與springmvc關聯起來的核心bean,shiroFilter的名字必須和web.xml中定義的shiroFilter名字一致。

待解決問題

1)如何實現分布式站點session共享

如果在分布式web站點中想實現session共享,必須借助於類似redis這種分布式一致性的介質。本章也主要是使用redis來實現的,具體實現:

1)在pom.xml中引入redis-client、spring-data依賴包,具體參考上邊介紹的pom.xml

2)web.xml的ContextLoaderListener加載監聽文件applicationContext-redis.xml,具體參考上邊applicationContext-redis.xml配置文件內容;

3)web.xml的ContextLoaderListener加載監聽文件applicaitonContext-shiro.xml中添加shiroFilter的sessionManager,並引入自定RedisSessionDao類;

    <!-- sessionIdCookie的實現,用於重寫覆蓋容器默認的JSESSIONID -->
    <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
        <!-- cookie的name,對應的默認是 JSESSIONID -->
        <constructor-arg name="name" value="SHAREJSESSIONID"/>
        <!-- jsessionId的path為 / 用於多個系統共享jsessionId -->
        <property name="path" value="/"/>
    </bean>

    <bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"></bean>
    <bean id="sessionDao" class="com.dx.test.shiro.RedisSessionDao">
        <property name="keyPrefix" value="shiro_redis_session:"></property>
        <property name="redisTemplate" ref="redisTemplate"></property>
        <property name="sessionIdGenerator" ref="sessionIdGenerator"></property>
        <property name="sessionTimeout" value="30"></property>
    </bean>

    <!-- 會話管理器-->
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <!--刪除在session過期時跳轉頁面時自動在URL中添加JSESSIONID-->
        <property name="sessionIdUrlRewritingEnabled" value="false"/>
        <!-- 設置超時時間 -->
        <property name="globalSessionTimeout" value="1800000"/>
        <!-- 刪除失效的session -->
        <property name="deleteInvalidSessions" value="true"/>
        <!-- 定時檢查失效的session -->
        <property name="sessionValidationSchedulerEnabled" value="true"/>
        <!-- 集群共享session -->
        <property name="sessionIdCookieEnabled" value="true"/>
        <property name="sessionIdCookie" ref="sessionIdCookie"/>
        <property name="sessionDAO" ref="sessionDao"/>
    </bean>

    <!-- securityManager安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!--<property name="realm" ref="myRealm"></property>-->
        <property name="realms">
            <list>
                <ref bean="myRealm"></ref>
            </list>
        </property>
        <property name="cacheManager" ref="cacheManager"></property>
        <property name="sessionManager" ref="sessionManager"></property>
        <property name="rememberMeManager" ref="rememberMeManager"></property>
    </bean>

注意:

1)sessionIdGenerator支持自定義生成器、和內置的JavaUuidSessionIdGenerator、RandomSessionIdGenerator;

2)其中sessionManager中的sessionDao是自定的RedisSessionDao,這個類內部提供了對session的緩存,但是用戶也可以自定義實現:可以使用內存、關系(非關系)數據庫、文件系統、redis等方式去實現。

4)自定義RedisSessionDao.java

public class RedisSessionDao extends AbstractSessionDAO {
    private static Logger logger = LoggerFactory.getLogger(RedisSessionDao.class);
    private RedisTemplate redisTemplate;
    private String keyPrefix = "shiro_redis_session:";
    /**
     * 單位minutes
     */
    private Long sessionTimeout = 30L;

    public RedisTemplate getRedisTemplate() {
        return redisTemplate;
    }

    public void setRedisTemplate(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public String getKeyPrefix() {
        return keyPrefix;
    }

    public void setKeyPrefix(String keyPrefix) {
        this.keyPrefix = keyPrefix;
    }

    public void setSessionTimeout(Long sessionTimeout) {
        this.sessionTimeout = sessionTimeout;
    }

    private String getKey(String key) {
        return getKeyPrefix() + key;
    }

    private void saveSession(Session session) throws UnknownSessionException {
        if (session != null && session.getId() != null) {
            String key = this.getKey(session.getId().toString());
            session.setTimeout(this.sessionTimeout * 60 * 1000);
            redisTemplate.opsForValue().set(key, session, this.sessionTimeout*60*1000, TimeUnit.SECONDS);
        } else {
            logger.error("session or session id is null");
        }
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
        logger.debug("更新seesion,id=[{}]", session.getId() != null ? session.getId().toString() : "null");
        this.saveSession(session);
    }

    @Override
    public void delete(Session session) {
        logger.debug("刪除seesion,id=[{}]", session.getId().toString());
        try {
            String key = getKey(session.getId().toString());
            redisTemplate.delete(key);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }

    }

    @Override
    public Collection<Session> getActiveSessions() {
        logger.info("獲取存活的session");

        Set<Session> sessions = new HashSet<>();
        Set<String> keys = redisTemplate.keys(getKey("*"));
        if (keys != null && keys.size() > 0) {
            for (String key : keys) {
                Session s = (Session) redisTemplate.opsForValue().get(key);
                sessions.add(s);
            }
        }
        return sessions;
    }

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = this.generateSessionId(session);
        this.assignSessionId(session, sessionId);
        logger.debug("創建seesion,id=[{}]", session.getId() != null ? session.getId().toString() : "null");
        // 當有游客進入或者remeberme都會調用
        this.saveSession(session);

        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        logger.debug("獲取seesion,id=[{}]", sessionId.toString());
        Session readSession = null;
        try {
            readSession = (Session) redisTemplate.opsForValue().get(getKey(sessionId.toString()));
        } catch (Exception e) {
            logger.error(e.getMessage());
        }
        return readSession;
    }
}

注意:

1)redisTemplate是applicationContext-redis.xml中定義的bean,因此這里可以直接在applicationContext-shiro.xml中使用ref="redisTemplate"引入;

2)keyPrefix需要在定義sessionDao bean時指定,默認‘shiro_redis_session:’;

3)timeout需要在定義sessionDao bean時指定,其用來指定存儲到redis的session過期時間,默認為30,單位:minute;

4)默認存儲到redis的session信息的key格式為:shiro_redis_session:xx-xx-xx-xx-xx-xx-xx。

2)如何控制一個用戶允許登錄次數?

上邊我們定義了sessionDao、sessionManager,並在securityManager中引入了sessionManager,從而實現了在redis中存儲session信息,redis中的session信息格式為:shiro_redis_session:xx-xx-xx-xx-xx-xx-xx。

此時,redis中還包含了另外一個用戶信息,那就是登錄用戶的授權信息,因為在myShiro中開啟了緩存授權信息的開關,且在securityManager中引用了cacheManager。且cacheManager也是我們自定的RedisCacheManager,那么在第一次認證觸發后,就會將認證信息存儲到redis中。它的存儲格式為key為SimpleAuthorizationInfo對象的一個字節碼。

如果要實現控制一個用戶最多登錄次數,需要知道用戶與session之間的關系。就目前的數據而言還不能完美的將用戶和session關聯起來,因此我們就需要通過其他方案實現用戶與session關聯起來。這我們采用自定義一個AccessControlFilter,在shiroFilter的filterChainDefinitions中引入該filter,用來攔截url,之后在攔截器中拿到認證后的用戶和session信息,然后用戶和session信息關聯存儲到redis中,這樣實現了用戶與session關聯起來,進而可以實現控制一個用戶最多登錄次數。

1)自定義AccessControlFilter

/**
 * 用來緩存已經通過認證的用戶,使得用戶與sessionId關聯起。
 * */
public class KickoutSessionFilter extends AccessControlFilter {
    /**
     * 踢出之后跳轉的url
     */
    private String kickoutUrl;
    /**
     * 踢出之前登錄的/之后登錄的用戶 默認踢出之前登錄的用戶
     */
    private boolean kickoutAfter = false;
    /**
     * 同一個帳號最大會話數 默認5
     */
    private Integer maxSession = 5;
    /**
     * 設置sessionManager
     */
    private SessionManager sessionManager;
    /**
     * redis緩存cache對象別名
     */
    private String cacheName = "shiro_redis_kickout_cache";
    /**
     * 獲取cacheManager下的Cache接口實現對象
     */
    private Cache<String, Deque<Serializable>> cache;

    /**
     * 構造函數
     *
     * @param sessionManager session管理器
     * @param cacheManager   cache管理器
     * @param cacheName      redis緩存cache對象別名
     * @param kickoutAfter   踢出之前登錄的/之后登錄的用戶 默認踢出之前登錄的用戶
     * @param kickoutUrl     踢出之后跳轉的url
     * @param maxSession     同一個帳號最大會話數 默認5
     */
    public KickoutSessionFilter(SessionManager sessionManager, String cacheName, CacheManager cacheManager, String kickoutUrl, Boolean kickoutAfter, Integer maxSession) {
        this.sessionManager = sessionManager;
        this.cacheName = cacheName;
        this.cache = cacheManager.getCache(this.cacheName);
        this.kickoutAfter = kickoutAfter;
        this.kickoutUrl = kickoutUrl;
        this.maxSession = maxSession;
    }

    /**
     * 是否有權限訪問
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return false;
    }

    /**
     * 沒有權限訪問時,才執行該方法。
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);
        if (!subject.isAuthenticated() && !subject.isRemembered()) {
            //如果沒有登錄,直接進行之后的流程
            return true;
        }

        Session session = subject.getSession();
        SysUser user = (SysUser) subject.getPrincipal();
        String username = user.getUsername();
        Serializable sessionId = session.getId();

        //讀取緩存   沒有就存入
        Deque<Serializable> deque = cache.get(username);

        //如果此用戶沒有session隊列,也就是還沒有登錄過,緩存中沒有
        //就new一個空隊列,不然deque對象為空,會報空指針
        if (deque == null) {
            deque = new LinkedList<Serializable>();
        }

        //如果隊列里沒有此sessionId,且用戶沒有被踢出;放入隊列
        if (!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
            //將sessionId存入隊列
            deque.push(sessionId);
            //將用戶的sessionId隊列緩存
            cache.put(username, deque);
        }

        //如果隊列里的sessionId數超出最大會話數,開始踢人
        while (deque.size() > maxSession) {
            Serializable kickoutSessionId = null;
            //如果踢出后者
            if (kickoutAfter) {
                kickoutSessionId = deque.removeFirst();
                //踢出后再更新下緩存隊列
                cache.put(username, deque);
            } else { //否則踢出前者
                kickoutSessionId = deque.removeLast();
                //踢出后再更新下緩存隊列
                cache.put(username, deque);
            }

            try {
                //獲取被踢出的sessionId的session對象
                Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                if (kickoutSession != null) {
                    //設置會話的kickout屬性表示踢出了
                    kickoutSession.setAttribute("kickout", true);
                }
            } catch (Exception e) {//ignore exception
            }
        }

        //如果被踢出了,直接退出,重定向到踢出后的地址
        if ((Boolean) session.getAttribute("kickout") != null && (Boolean) session.getAttribute("kickout") == true) {
            //會話被踢出了
            try {
                //退出登錄
                subject.logout();
            } catch (Exception e) { //ignore
            }
            saveRequest(request);

            Map<String, String> resultMap = new HashMap<String, String>(2);
            //判斷是不是Ajax請求
            if ("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"))) {
                resultMap.put("state", "300");
                resultMap.put("message", "您已經在其他地方登錄,請重新登錄!");
                //輸出json串
                out(response, resultMap);
            } else {
                //重定向
                WebUtils.issueRedirect(request, response, kickoutUrl);
            }
            return false;
        }
        return true;
    }

    private void out(ServletResponse hresponse, Map<String, String> resultMap)
            throws IOException {
        try {
            hresponse.setCharacterEncoding("UTF-8");
            PrintWriter out = hresponse.getWriter();
            out.println(JSON.toJSONString(resultMap));
            out.flush();
            out.close();
        } catch (Exception e) {
            System.err.println("KickoutSessionFilter.class 輸出JSON異常,可以忽略。");
        }
    }
}

注意:

1)上邊代碼中存儲到redis的key格式為:redis_shiro_cache:username;

2)存儲到redis的value的格式為:LinkedList(sessionid-00,sessionId-01,。。。);

3)實際上控制登錄個數也就是通過判定redis中value的LinkedList的元素個數;

4)定義該bean時,需要在構造函數中指定以下幾個參數:

* sessionManager session管理器
* cacheManager cache管理器
* cacheName redis緩存cache對象別名
* kickoutAfter 踢出之前登錄的/之后登錄的用戶 默認踢出之前登錄的用戶
* kickoutUrl 踢出之后跳轉的url
* maxSession 同一個帳號最大會話數 默認5

2)applicaitonContext-shiro.xml中shiroFilter.filterChainDefinitions下引入自定filter到url下

    <bean id="kickout" class="com.dx.test.shiro.KickoutSessionFilter">
        <constructor-arg name="sessionManager" ref="sessionManager"></constructor-arg>
        <constructor-arg name="cacheName" value="shiro_redis_kickout_cache"></constructor-arg>
        <constructor-arg name="cacheManager" ref="cacheManager"></constructor-arg>
        <constructor-arg name="kickoutAfter" value="true"></constructor-arg>
        <constructor-arg name="kickoutUrl" value="/login"></constructor-arg>
        <constructor-arg name="maxSession" value="2"></constructor-arg>
    </bean>

    <!-- id屬性值要對應 web.xml中shiro的filter對應的bean -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"></property>
        <!-- loginUrl認證提交地址,如果沒有認證將會請求此地址進行認證,請求地址將由formAuthenticationFilter進行表單認證 -->
        <property name="loginUrl" value="/login"></property>
        <property name="filters"> <util:map> <entry key="kickout" value-ref="kickout"></entry> </util:map> </property>
        <!-- 過濾器定義,從上到下執行,一般將/**放在最下面 -->
        <property name="filterChainDefinitions">
            <value>
                # 對靜態資源設置匿名訪問
                /images/** = anon
                /js/** = anon
                /styles/** = anon
                /validatecode.jsp=anon
                /index=anon

                # 請求logout.action地址,shiro去清除session
                /logout.action = logout

                # /**=anon 所有的url都可以匿名訪問,不能配置在最后一排,不然所有的請求都不會攔截
                # /**=authc 所有的url都必須通過認證才可以訪問
                /** = kickout,authc
            </value>
        </property>
    </bean>

此時redis中信息包含:

3)如何動態分配shiroFilter#filterChainDefinitions

實際上邊url中指定kickout filter是寫死的,當在permission中修改了數據后,如何實現動態分配shiroFilter#filterChainDefinitions屬性呢?

又如何動態分配kickout filter呢?就說第一次加載時如何動態配置shiroFilter#filterChainDefinitions,因為數據表sys_permission中配置的有url資源。

1)啟動時同步

自定StartupListener.java啟動類,在啟動類中加載sys_permission數據到shiroFilter#filterChainDefinitions集合中:

/**
 * 將系統中的的permission信息追加到 shiroFilter.filterChainDefinitions 下。
 * 注意:<br>
 * 在SpringMvc項目中,這個類的onApplicationEvent方法會被執行兩次,因為項目中有兩個ApplicationContext:<br>
 * 1)parent ApplicationContext:ContextLoaderListener初始化的;<br>
 * 2)child ApplicationContext:DispatcherServlet初始化的。<br>
 */
@Component("startupListener")
public class StartupListener implements ApplicationListener<ContextRefreshedEvent> {
    protected Logger logger = LoggerFactory.getLogger(getClass());
    @Autowired
    private ShiroService shiroService;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 這里只想在 parent ApplicationContext 初始化完整時,執行相應業務,因為 applicationContext-shiro.xml 是在 ContextLoaderListner 下加載的。
        if (event.getApplicationContext().getParent() == null) {
            // 獲取到上下文唯一 shiroFilter bean對象
            ShiroFilterFactoryBean shiroFilterFactoryBean = event.getApplicationContext().getBean(ShiroFilterFactoryBean.class);
            // 獲取到 shiroFilter bean中配置的 filterChainDefinitions 信息,然后與 sys_permission 中的配置信息一起 merge。
            Map<String, String> filterChainDefinitionMap = shiroService.mergeFilterChainDefinitions(shiroFilterFactoryBean.getFilterChainDefinitionMap());
            // 重新設置 shiroFilter.filterChainDefinitions 屬性。
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        }
    }
}

需要在applicationContext-base.xml中掃描該listener所在的包下的類:

    <!-- 加載Listener component -->
    <context:component-scan base-package="com.dx.test.listener"/>

2)修改了sys_permission時

當修改了sys_permission時,需要動態修改shiroFilter#filterChainDefinitions集合

在shiro包下定義個ShiroService,並使用@Service修飾,需要在applicaitonContext-base.xml中掃描shiro包的類:

    <!-- 掃描shrio相關類(包含了@Service ShiroService組件) -->
    <context:component-scan base-package="com.dx.test.shiro"/>

自定義ShiroService類,內部定義函數

@Service
public class ShiroService {
    @Autowired
    private SysPermissionMapper sysPermissionMapper;

    /**
     * 將applicationContext-shiro.xml中shiroFilter.filterChainDefinitions配置信息與sys_permission合並。
     */
    public Map<String, String> mergeFilterChainDefinitions(Map<String, String> oldFilterChainDefinitions) {
        // 權限控制map.從數據庫獲取
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();

        filterChainDefinitionMap.put("/register", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/error/**", "anon");
        filterChainDefinitionMap.put("/kickout", "anon");
        /*filterChainDefinitionMap.put("/logout", "logout");*/
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        filterChainDefinitionMap.put("/libs/**", "anon");
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        filterChainDefinitionMap.put("/verificationCode", "anon");
        List<SysPermission> permissionList = sysPermissionMapper.getAll();
        for (SysPermission permission : permissionList) {
            if (StringUtils.isNotBlank(permission.getPermissionUrl()) && StringUtils.isNotBlank(permission.getPermissionValue())) {
                String perm = "perms[" + permission.getPermissionValue() + "]";
                filterChainDefinitionMap.put(permission.getPermissionUrl(), perm + ",kickout");
            }
        }
        filterChainDefinitionMap.put("/**", "user,kickout");

        for (Map.Entry<String, String> entry : oldFilterChainDefinitions.entrySet()) {
            if (false == filterChainDefinitionMap.containsKey(entry.getKey())) {
                filterChainDefinitionMap.put(entry.getKey(), entry.getValue());
            }
        }

        return filterChainDefinitionMap;
    }

    /**
     * 重置 filterChainDefinitions
     */
    public void reloadPermission(ServletContext servletContext) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = WebApplicationContextUtils.getWebApplicationContext(servletContext).getBean(ShiroFilterFactoryBean.class);
        synchronized (shiroFilterFactoryBean) {
            AbstractShiroFilter shiroFilter = null;
            try {
                shiroFilter = (AbstractShiroFilter) shiroFilterFactoryBean
                        .getObject();
            } catch (Exception e) {
                throw new RuntimeException(
                        "get ShiroFilter from shiroFilterFactoryBean error!");
            }

            PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter.getFilterChainResolver();
            DefaultFilterChainManager defaultFilterChainManager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager();

            Map<String, String> oldFilterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();
            Map<String, String> newFilterChainDefinitionMap = mergeFilterChainDefinitions(oldFilterChainDefinitionMap);

            // 清空老的權限控制
            defaultFilterChainManager.getFilterChains().clear();
            shiroFilterFactoryBean.getFilterChainDefinitionMap().clear();

            shiroFilterFactoryBean.setFilterChainDefinitionMap(newFilterChainDefinitionMap);
            // 重新構建生成
            Map<String, String> chains = shiroFilterFactoryBean.getFilterChainDefinitionMap();
            for (Map.Entry<String, String> entry : chains.entrySet()) {
                String url = entry.getKey();
                String chainDefinition = entry.getValue().trim().replace(" ", "");
                defaultFilterChainManager.createChain(url, chainDefinition);
            }
        }
    }
}

模擬修改資源時,調用修改示例:

@Controller
@RequestMapping(value = "/role")
public class SysRoleController {
    @Autowired
    private MyRealm myRealm;
    @Autowired
    private ShiroService shiroService;

    /**
     * 模擬修改了用戶的資源信息(增刪改),
     * 1)需要同步到shiroFilter的filterChainDefinitions屬性。
     * 2)清空在線用戶的授權緩存信息。(下次用戶調用授權時,會重新執行MyShiro#doGetAuthorizationInfo(...)方法)
     * */
    @RequestMapping(value="/updatePermission",method=RequestMethod.GET)
    public String updatePermission(SysPermission sysPermission, Map<String,String> map, HttpServletRequest request){
        BaseResult baseResult = null;
        ResultEnum enu = null;

        // 模擬:在這里做了以下業務:
        // 1)修改了資源下的資源信息;
        // 2)刪除了資源;
        // 3)修改了用戶的資源信息。

        this.shiroService.reloadPermission(request.getServletContext());
        this.myRealm.clearAllCache();

        enu = ResultEnum.Success;
        baseResult = new BaseResult(enu.getCode(), enu.getMessage(), enu.getDesc());
        map.put("result", "已處理完成");

        return "role/list.html";
    }
}

3)如何統計在線用戶數、剔除用戶?

1)在ShiroService中加入如下獲取在線用戶方法,以及剔除用戶方法

@Service
public class ShiroService {
    @Autowired
    private RedisSessionDao redisSessionDao;
    @Autowired
    private SessionManager sessionManager;
    @Autowired
    private RedisCacheManager redisCacheManager;


    /**
     * 從redis中獲取到在線用戶
     */
    public List<UserOnlineVo> getOnlineUserList() {
        Collection<Session> sessions = redisSessionDao.getActiveSessions();
        Iterator<Session> it = sessions.iterator();
        List<UserOnlineVo> userOnlineVoList = new ArrayList<UserOnlineVo>();
        // 遍歷session
        while (it.hasNext()) {
            // 這是shiro已經存入session的
            // 現在直接取就是了
            Session session = it.next();
            //標記為已提出的不加入在線列表
            if (session.getAttribute("kickout") != null) {
                continue;
            }
            UserOnlineVo userOnlineVo = getOnlineUserinfoFromSession(session);
            if (userOnlineVo != null) {
                userOnlineVoList.add(userOnlineVo);
            }
        }

        return userOnlineVoList;
    }

    /**
     * 剔出在線用戶
     */
    public void kickout(Serializable sessionId, String username) {
        getSessionBysessionId(sessionId).setAttribute("kickout", true);
        //讀取緩存,找到並從隊列中移除
        Cache<String, Deque<Serializable>> cache = redisCacheManager.getCache(redisCacheManager.getKeyPrefix() + username);
        Deque<Serializable> deques = cache.get(username);
        for (Serializable deque : deques) {
            if (sessionId.equals(deque)) {
                deques.remove(deque);
                break;
            }
        }
        cache.put(username, deques);
    }

    private Session getSessionBysessionId(Serializable sessionId) {
        Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(sessionId));
        return kickoutSession;
    }

    private UserOnlineVo getOnlineUserinfoFromSession(Session session) {
        //獲取session登錄信息。
        Object obj = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
        if (null == obj) {
            return null;
        }
        //確保是 SimplePrincipalCollection對象。
        if (obj instanceof SimplePrincipalCollection) {
            SimplePrincipalCollection spc = (SimplePrincipalCollection) obj;
            obj = spc.getPrimaryPrincipal();
            if (null != obj && obj instanceof SysUser) {
                SysUser user = (SysUser) obj;
                //存儲session + user 綜合信息
                UserOnlineVo userOnlineVo = new UserOnlineVo();
                //最后一次和系統交互的時間
                userOnlineVo.setLastAccess(session.getLastAccessTime());
                //主機的ip地址
                userOnlineVo.setHost(session.getHost());
                //session ID
                userOnlineVo.setSessionId(session.getId().toString());
                //最后登錄時間
                userOnlineVo.setLastLoginTime(session.getStartTimestamp());
                //回話到期 ttl(ms)
                userOnlineVo.setTimeout(session.getTimeout());
                //session創建時間
                userOnlineVo.setStartTime(session.getStartTimestamp());
                //是否踢出
                userOnlineVo.setSessionStatus(false);
                /*用戶名*/
                userOnlineVo.setUsername(user.getUsername());
                return userOnlineVo;
            }
        }
        return null;
    }

}

2)在/webapp/WEB-INF/templates/online/下,添加list.html

<!DOCTYPE html>
<html xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8">
    <title>Online user page</title>
    <style>
        td{
            border:solid #add9c0;
            border-width:0px 1px 1px 0px;
        }
        table{
            border:solid #add9c0;
            border-width:1px 0px 0px 1px;
            border-collapse: collapse;
        }
    </style>
</head>
<body>
<h3>在線用戶列表</h3>
<table>
    <thead>
    <tr>
        <td>會話id</td>
        <td>用戶名</td>
        <td>主機地址</td>
        <td>最后訪問時間</td>
        <td>操作</td>
    </tr>
    </thead>
    <tbody>
    <shiro:hasPermission name="online:list">
    <tr th:each="m : ${list}"><!-- 其中m是個臨時變量,像for(User u : userList)那樣中的u-->
        <td th:text="${m.sessionId}"/>
        <td th:text="${m.username}"/>
        <td th:text="${m.host}"/>
        <td th:text="${m.lastAccess}"/>
        <td>
            <shiro:hasPermission name="online:remove">
                <a href="/online/delete?id=${m.sessionId}">剔除</a>
            </shiro:hasPermission>
        </td>
    </tr>
    </shiro:hasPermission>
    </tbody>
</table>
<shiro:lacksPermission name="online:list">
<p>
    Sorry, you are not allowed to access online user information.
</p>
</shiro:lacksPermission>
<shiro:lacksPermission name="online:remove">
<p>
    Sorry, you are not allowed to remove online user.
</p>
</shiro:lacksPermission>
</body>
</html>

3)添加OnelineUserController.java類

@Controller
@RequestMapping(value = "/online")
public class OnlineUserController {
    @Autowired
    private ShiroService shiroService;

    @RequestMapping(value = "/list", method = RequestMethod.GET)
    public ModelAndView list() {
        ModelAndView mv = new ModelAndView();
        mv.setViewName("online/list.html");
        List<UserOnlineVo> userOnlineVoList = this.shiroService.getOnlineUserList();
        mv.addObject("list", userOnlineVoList);

        return mv;
    }

    /**
     * 強制踢出用戶
     */
    @RequestMapping(value = "/kickout", method = RequestMethod.GET)
    @ResponseBody
    public String kickout(String sessionId, String username) {
        try {
            if (SecurityUtils.getSubject().getSession().getId().equals(sessionId)) {
                return "不能踢出自己";
            }
            shiroService.kickout(sessionId, username);
            return "踢出用戶成功";
        } catch (Exception e) {
            return "踢出用戶失敗";
        }
    }
}

4)測試列表頁面:

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM