zoukankan      html  css  js  c++  java
  • 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)测试列表页面:

  • 相关阅读:
    配置SecondaryNameNode
    hadoop 根据secondary namenode恢复namenode
    Hadoop如何修改HDFS文件存储块大小
    hadoop1.2.1 datanode 由于权限无法启动 expected: rwxr-xr-x
    CentOS 7 下,如何设置DNS服务器
    Eclipse+pydev环境搭建
    Python numpy
    Leetcode#54 Spiral Matrix
    Leetcode#53 Maximum Subarray
    Leetcode#40 Combination Sum II
  • 原文地址:https://www.cnblogs.com/yy3b2007com/p/12127821.html
Copyright © 2011-2022 走看看