Java-Shiro(六):Shiro Realm讲解(三)Realm的自定义及应用

本片文章源码位置:《https://github.com/478632418/springmv_without_web_xml/tree/master/mybaits-test-dynamic-sql》
1)环境整理:

为了把自定义Realm的用法接近于真实场景,本章将会基于SpringMvc+Mybatis+Shiro整合后来展示:在真实环境中如何使用自定义Realm。

为什么要定义Realm?

1)真实环境中往往会有自定义的账户管理系统,因此使用IniRealm,PropertiesRealm这些配置文件方式不适用:总不能系统中每新增或删除一个账户,都要去修改配置文件;

2)JdbcRealm也是最可能能凑合使用第一个Realm,但是因为JdbcRealm内部已经内置好了表结构,对于与实际应用也不一定能恰好满足。

因此,大多应用使用Shiro时,都会自定义Realm来操作认证、授权数据。

SpringMVC+Mybatis+Shiro环境整合:

pom.xml

需要引入shiro相关组件、shiro-spring整合组件、springmvc组件+thymeleaf、mybatis组件、mybatis-spring组件、common包、jdbc driver包、druid等。
包版本信息:

    <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>

shiro以及shiro-spring相关依赖:

        <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>
View Code

mybatis以及mybatis-spring相关依赖:

        <!--访问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>
View Code

springmvc+spring+aop相关+thymeleaf相关依赖:

        <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>

        <!-- 编译依赖 -->
        <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>

        <!--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>
View Code

jdbc driver+druid相关依赖:

        <!--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>
View Code

其他相关依赖:

        <!--日志支持 -->
        <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>

        <!-- 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>
View Code

另外搭建项目还需要web.xml,而且除了web.xml配置文件外,DispatcherServlet使用springmvc-web.xml;ContextLoaderListener使用springmvc-mybatis.xml & spring-shiro.xml。

Web.xml

web.xml中需要引入shiroFilter(shiro框架过滤验证器)、multipartFilter(用来上传文件使用)、hiddenHttpMethodFilter(实现put、heade请求使用)、characterEncodingFilter(后端数据输出到前端乱码问题):

<?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:applicationContext-mybatis.xml,
            classpath:applicationContext-shiro.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

本章重点需要关注shiroFilter这块过滤器引入到web.xml中:

    <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>

其中shiroFilter这个<filter-name>必须要和applicationContext-shiro.xml中的shiroFilter bean名称一致,否则会抛出找不到shiroFilter的bean异常。

springmvc-servlet.xml

这个配置文件是<sevlet>DispatcherServlet中需要加载的配置文件:

<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>
    
    <!-- 配置启用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>
    
    <!-- 单独使用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" />  
    </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。
2)返回视图:return "abc.jsp" ,将使用jsp视图解析器,jsp的视图模板文件在/WEB-INF/views/下;
3)返回视图:return "abc.html",将使用 thymeleaf视图解析器,thymeleaf的视图模板文件在/WEB-INF/templates/下。

applicationContext-mybatis.xml

该配置文件时ContextLoaderListener需要加载的配置文件之一,目的实现mybatis集成到spring框架中。

<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 ">
        
    <!--扫描Service、Dao里面的注解,这里没有定义service-->
    <context:component-scan base-package="com.dx.test.dao"></context:component-scan>
    
    <!-- 文件上传注意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>

    <!-- 数据库连接池配置文件Dao层 -->
    <!-- 加载配置文件 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!-- 数据库连接池,使用dbcp -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="driverClassName" value="${driver}" />
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${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

该配置文件主要配置了以下几项内容:

1)component-scan:用来扫描 dao/service等bean,当然他们需要在类上注解了@Service/@Repository/@Mapper等;

2)multipartResolver上传文件使用的文件解析器bean;

3)mybatis需要依赖的数据库连接池dataSource;

4)mybatis的sqlSessionFactory bean,配置时需要sqlSessionFactory与dataSource关联,而且可以指定“配置MyBaties全局配置文件:mybatisConfig.xml”等信息;

5)配置扫描Dao接口包,动态实现Dao接口,注入到spring容器中;

6)事务管理器transactionManager。

mybatisConfig.xml是mybatis的基本配置:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!--配置全局属性-->
    <settings>
        <!-- 打开延迟加载的开关 -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!-- 将积极加载改为消极加载(即按需加载) -->
        <setting name="aggressiveLazyLoading" value="false"/>
        <!-- 打开全局缓存开关(二级缓存)默认值就是 true -->
        <setting name="cacheEnabled" value="true"/>
        <!--使用jdbc的getGeneratekeys获取自增主键值-->
        <setting name="useGeneratedKeys" value="true"/>
        <!--使用列别名替换别名  默认true select name as title form table; -->
        <setting name="useColumnLabel" value="true"/>
        <!--开启驼峰命名转换-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!--打印sql日志-->
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>
    
    <!-- 引用db.properties配置文件 -->
    <!-- 
    <properties resource="jdbc.properties"/> 
    <typeAliases>
        <package name="com.dx.test.model"/>
    </typeAliases>
    -->
    <!-- 对事务的管理和连接池的配置 -->
    <!-- 
    <environments default="mysql_jdbc">
        <environment id="oracle_jdbc">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${name}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
        <environment id="mysql_jdbc">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${name}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>
    -->
    
    <!--
    <mappers>
        <mapper resource="resources/mapper/TaskAutoExecutePlanMapper.xml"/>
    </mappers>
    -->
<!--<mappers>
        <mapper class="com.dx.test.dao.LogMapper"></mapper>
        <mapper class="com.dx.test.dao.SysUserMapper"></mapper>
        <mapper class="com.dx.test.dao.SysRoleMapper"></mapper>
        <mapper class="com.dx.test.dao.SysUserRoleMapper"></mapper>
        <mapper class="com.dx.test.dao.SysPermissionMapper"></mapper>
        <mapper class="com.dx.test.dao.SysRolePermissionMapper"></mapper>
    </mappers>-->
</configuration>
View Code

jdbc.properties是mybatis中配置数据连接信息:

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

applicationContext-shiro.xml

applicationContext-shiro.xml是ContextLoaderListener需要配置的另外一个文件,配置它的目的,注入shiroFilter bean,使得它可以在web.xml注入shiroFilter使用。总之,是为了试下shiro来监听请求,过滤请求。

<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:component-scan base-package="com.dx.test.shiro"/>
     -->
    <!-- 凭证匹配器 -->
    <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>
    </bean>
    <!-- 一个简单的jdbcRealm 来自:https://blog.csdn.net/qq_31307269/article/details/70237295 -->
    <bean id="sampleRealm"
        class="org.apache.shiro.realm.jdbc.JdbcRealm">
        <!-- dataSource数据源,可以引用spring中配置的数据源 -->
        <property name="dataSource" ref="dataSource" />
        <!-- authenticationQuery登录认证用户的查询SQL,需要用登录用户名作为条件,查询密码字段。 -->
        <property name="authenticationQuery"
            value="select t.password from my_user t where t.username = ?" />
        <!-- userRolesQuery用户角色查询SQL,需要通过登录用户名去查询。查询角色字段 -->
        <property name="userRolesQuery"
            value="select a.rolename from my_user_role t left join my_role a on t.roleid = a.id where t.username = ? " />
            <!-- permissionsQuery用户的权限资源查询SQL,需要用单一角色查询角色下的权限资源,如果存在多个角色,则是遍历每个角色,分别查询出权限资源并添加到集合中。 -->
        <property name="permissionsQuery"
            value="SELECT B.PERMISSION FROM MY_ROLE T LEFT JOIN MY_ROLE_PERMISSION A ON T.ID = A.ROLE_ID LEFT JOIN MY_PERMISSION B ON A.PERMISSION = B.ID WHERE T.ROLENAME = ? " />
        <!-- permissionsLookupEnabled默认false。False时不会使用permissionsQuery的SQL去查询权限资源。设置为true才会去执行。 -->
        <property name="permissionsLookupEnabled" value="true" />
        <!-- saltStyle密码是否加盐,默认是NO_SALT不加盐。加盐有三种选择CRYPT,COLUMN,EXTERNAL。这里按照不加盐处理。 -->
        <property name="saltStyle" value="NO_SALT" />
        <!-- credentialsMatcher密码匹配规则 -->
        <property name="credentialsMatcher" ref="credentialsMatcher" />
    </bean>

    <!-- securityManager安全管理器 -->
    <bean id="securityManager"
        class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="myRealm"></property>
    </bean>
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

    <!-- 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="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都必须通过认证才可以访问 
                /** = authc
            </value>
        </property>
    </bean>

    <!-- 解决shiro配置的没有权限访问时,unauthorizedUrl不跳转到指定路径的问题 -->
    <bean
        class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
            <props>
                <prop key="org.apache.shiro.authz.UnauthorizedException">/refuse</prop>
            </props>
        </property>
    </bean>

</beans>
View Code

针对上边配置内容需要注意以下几点:

1)其实内部使用了了散列算法,对应散列算法使用示例:

package com.dx.test;

import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.junit.Test;

import com.dx.test.util.RandomUtil;

public class SaltTest {
    @Test
    public void testSalt() {
        String password = "1234";
        String salt = RandomUtil.getSalt(12);
        int hashIterations=8;

        System.out.println(salt);
        
        /**
         *  Md5Hash(Object source, Object salt, int hashIterations)
         *  source:明文,原始密码
         *  salt:盐,通过使用随机数
         *  hashIterations:散列次数
         */
        Md5Hash md5Hash = new Md5Hash(password, salt, hashIterations);
        String md5Pwd = md5Hash.toString();
        System.out.println(md5Pwd);
     
        /**
         *  SimpleHash(String algorithmName, Object source, Object salt, int hashIterations)
         *  algorithmName:算法名称
         *  source:明文,原始密码
         *  salt:盐
         *  hashIterations:散列次数
         */
        SimpleHash simpleHash = new SimpleHash("md5", password, salt, hashIterations);
        System.out.println(simpleHash);

    }
}

2)shiro内置filter,上边提到了anon、logout、authc,实际上还有更多其他的内置filter:

Filter NameClass
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
logout org.apache.shiro.web.filter.authc.LogoutFilter
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter
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


后边会单独抽出一章节来讲述具体这些filter的原理,以及它们在shiro框架中都哪些地方会用到。

3)上边写了 /**=authc,实际上也可以对每个url指定一个具体权限,比如:

/article/edit=authc, roles[admin,user], perms[article:edit]

4)上边securityManager下配置了一个realm,实际上也可以配置多个realm

<!--  认证策略 -->
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">  
    <property name="authenticationStrategy">  
        <bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy" />  
    </property>  
</bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="authenticator" ref="authenticator" /> 
        <property name="realms" >
            <list>
                   <ref bean="myRealm"/>
                <bean id="myRealm2" class="com.my.test.Realm2" />
            </list>
        </property>
        <property name="sessionManager" ref="sessionManager"></property>
</bean>

当配置多个realms时,需要指定authenticator认证器的'authenticationStrategy(认证策略)属性'。

5)securityManger除了可以设置上边属性外,还可以设置 sesssionManager、cacheManager、rememberMeManager 属性。
sessionManager:对session进行配置管理;
cacheManager:对认证信息、授权信息进行缓存配置管理;
rememberMeManager:对rememberMe功能进行配置管理。

<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"/>

<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
    <constructor-arg value="rememberMe"/>
    <property name="httpOnly" value="true"/>
    <property name="maxAge" value="2592000"/><!-- 30 days -->
</bean>

<!-- rememberMe管理器  -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
    <property name="cipherKey"
              value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/>
    <property name="cookie" ref="rememberMeCookie"/>
</bean>

<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
    <!--sesssionDao可以自己扩展实现,可以继承其他三方实现-->
    <property name="sessionDAO" ref="sessionDAO"/>
    <!-- session的失效时长,单位毫秒 1小时: 3600000, itzixi站点设置以 6小时 为主:21600000 -->
    <!-- 设置全局会话超时时间,默认30分钟,即如果30分钟内没有访问会话将过期 1800000 -->
    <property name="globalSessionTimeout" value="21600000"/>
    <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
    <!-- 删除失效的session -->
    <property name="deleteInvalidSessions" value="true"/>
    <!-- 是否开启会话验证器,默认是开启的 -->
    <property name="sessionValidationSchedulerEnabled" value="true"/>
</bean>

<!-- 会话ID生成器 -->
<bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/>

<!-- 会话Cookie模板,使用sid存储sessionid -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
    <constructor-arg value="sid"/>
    <property name="httpOnly" value="true"/>
    <property name="maxAge" value="180000"/>
</bean>

<!-- 
    会话DAO,更多场景中会自定义或者使用三个的SessionDao,比如:
    https://gitee.com/supperzh/zb-shiro/blob/master/src/main/java/com/nbclass/config/ShiroConfig.java#L122
    https://github.com/alexxiyang/shiro-redis/tree/master/src/main/java/org/crazycake/shiro
-->
<bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
    <property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
    <property name="sessionIdGenerator" ref="sessionIdGenerator"/>
</bean>

<!-- 会话验证调度器 -->
<bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler">
    <property name="sessionValidationInterval" value="1800000"/>
    <property name="sessionManager" ref="sessionManager"/>
</bean>

<!-- 会话管理器 -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
    <property name="globalSessionTimeout" value="1800000"/>
    <property name="deleteInvalidSessions" value="true"/>
    <property name="sessionValidationSchedulerEnabled" value="true"/>
    <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
    <property name="sessionDAO" ref="sessionDAO"/>
    <property name="sessionIdCookieEnabled" value="true"/>
    <property name="sessionIdCookie" ref="sessionIdCookie"/>
</bean>

<!--  认证策略 -->
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">  
    <property name="authenticationStrategy">  
        <bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy" />  
    </property>  
</bean>

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="authenticator" ref="authenticator" /> 
    <!-- <property name="realm" ref="shiroDbRealm"/> -->
    <property name="realms" >
        <list>
               <ref bean="myRealm"/>
            <bean id="myRealm2" class="com.my.test.Realm2" />
        </list>
    </property>
    <property name="sessionMode" value="native"/>
    <property name="cacheManager" ref="cacheManager"/>
    <property name="sessionManager" ref="sessionManager"/>
    <property name="rememberMeManager" ref="rememberMeManager"/>
</bean>
View Code

自定义Realm

系统存储结构设计

自定义的Realm会使用mysql中的数据作为认证、授权数据,对应表结构设计为:sys_user、sys_role、sys_permission、sys_role_permission、sys_user_role。

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
  `id` int(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(64) NOT NULL,
  `nick_name` varchar(64) DEFAULT NULL,
  `signature` varchar(512) DEFAULT NULL,
  `email` varchar(256) NOT NULL,
  `phone` varchar(16) DEFAULT NULL,
  `password` varchar(64) NOT NULL,
  `salt` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `status` int(4) NOT NULL DEFAULT '0',
  `expire_time` date NOT NULL DEFAULT '9999-12-01',
  `create_time` datetime NOT NULL,
  `create_user` varchar(64) DEFAULT NULL,
  `create_user_id` varchar(64) DEFAULT NULL,
  `modify_time` datetime NOT NULL,
  `modify_user` varchar(64) DEFAULT NULL,
  `modify_user_id` varchar(64) DEFAULT NULL,
  `version` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `un_username` (`username`),
  KEY `idx_username` (`username`),
  KEY `idx_email` (`email`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
BEGIN;
INSERT INTO `sys_user` VALUES (1, 'admin', 'administrator', '管理员', 'admin@test.com', '010-03838232', '3bb04e43e6f8b6775d3fb125b3aa02f6', 'gT1jO9zW5oY7', 0, '9999-12-01', '2019-12-23 15:31:38', NULL, NULL, '2019-12-23 15:31:46', NULL, NULL, 0);
COMMIT;

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
  `id` int(20) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(64) NOT NULL,
  `description` varchar(512) DEFAULT NULL,
  `create_time` date NOT NULL,
  `create_user` varchar(64) DEFAULT NULL,
  `create_user_id` varchar(64) DEFAULT NULL,
  `modify_time` date NOT NULL,
  `modify_user` varchar(64) DEFAULT NULL,
  `modify_user_id` varchar(64) DEFAULT NULL,
  `version` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `un_role_name` (`role_name`) USING BTREE,
  KEY `idx_role_name` (`role_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
BEGIN;
INSERT INTO `sys_role` VALUES (1, 'admin', '管理员角色', '2019-12-23', NULL, NULL, '2019-12-23', NULL, NULL, 0);
COMMIT;

-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
  `id` int(20) NOT NULL AUTO_INCREMENT,
  `permission_name` varchar(64) NOT NULL,
  `permission_value` varchar(64) NOT NULL,
  `description` varchar(512) DEFAULT NULL,
  `create_time` date NOT NULL,
  `create_user` varchar(64) DEFAULT NULL,
  `create_user_id` varchar(64) DEFAULT NULL,
  `modify_time` date NOT NULL,
  `modify_user` varchar(64) DEFAULT NULL,
  `modify_user_id` varchar(64) DEFAULT NULL,
  `version` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `un_permission_name` (`permission_name`),
  KEY `idx_permission_name` (`permission_name`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of sys_permission
-- ----------------------------
BEGIN;
INSERT INTO `sys_permission` VALUES (1, 'queryArticle', 'article:query', NULL, '2019-12-23', NULL, NULL, '2019-12-23', NULL, NULL, 0);
INSERT INTO `sys_permission` VALUES (2, 'deleteArticle', 'article:delete', NULL, '2019-12-23', NULL, NULL, '2019-12-23', NULL, NULL, 0);
INSERT INTO `sys_permission` VALUES (3, 'updateArticle', 'article:update', NULL, '2019-12-23', NULL, NULL, '2019-12-23', NULL, NULL, 0);
COMMIT;


-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission` (
  `id` int(20) NOT NULL AUTO_INCREMENT,
  `role_id` int(20) NOT NULL,
  `permission_id` int(20) NOT NULL,
  `create_time` date NOT NULL,
  `create_user` varchar(64) DEFAULT NULL,
  `create_user_id` varchar(64) DEFAULT NULL,
  `modify_time` date NOT NULL,
  `modify_user` varchar(64) DEFAULT NULL,
  `modify_user_id` varchar(64) DEFAULT NULL,
  `version` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `un_role_id_permission_id` (`role_id`,`permission_id`),
  KEY `idx_role_id` (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of sys_role_permission
-- ----------------------------
BEGIN;
INSERT INTO `sys_role_permission` VALUES (1, 1, 1, '2019-12-23', NULL, NULL, '2019-12-23', NULL, NULL, 0);
INSERT INTO `sys_role_permission` VALUES (2, 1, 2, '2019-12-23', NULL, NULL, '2019-12-23', NULL, NULL, 0);
INSERT INTO `sys_role_permission` VALUES (3, 1, 3, '2019-12-23', NULL, NULL, '2019-12-23', NULL, NULL, 0);
COMMIT;


-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
  `id` int(20) NOT NULL AUTO_INCREMENT,
  `role_id` int(20) NOT NULL,
  `user_id` int(20) NOT NULL,
  `create_time` date NOT NULL,
  `create_user` varchar(64) DEFAULT NULL,
  `create_user_id` varchar(64) DEFAULT NULL,
  `modify_time` date NOT NULL,
  `modify_user` varchar(64) DEFAULT NULL,
  `modify_user_id` varchar(64) DEFAULT NULL,
  `version` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_role_id` (`role_id`),
  KEY `un_user_id_role_id` (`role_id`,`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
BEGIN;
INSERT INTO `sys_user_role` VALUES (1, 1, 1, '2019-12-23', NULL, NULL, '2019-12-23', NULL, NULL, 0);
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;
View Code

备注:

1)查看某个用户是否有拥有的角色接口:

    @Options(useCache = true, flushCache = Options.FlushCachePolicy.FALSE, timeout = 60000)
    @ResultMap("sysRoleResult")
    @Select(value = { //
            "<script>", //
            "SELECT t10.* ", //
            "FROM `sys_role` as t10 ", //
            "INNER JOIN `sys_user_role` as t11 on t10.`id`=t11.`role_id` ", //
            "<where> ", //
            " <if test='userId!=null and !userId.equals(0)'>", //
            "     and t11.`user_id`=#{userId}", //
            " </if>", //
            "</where>", //
            "</script>" //
    })
    List<SysRole> getByUserId(@Param("userId") Long userId);

2)查看某个用户拥有的角色下资源接口:

    @Options(useCache = true, flushCache = Options.FlushCachePolicy.FALSE, timeout = 60000)
    @ResultMap("sysPermissionResult")
    @Select(value = { //
            "<script>", //
            "SELECT t10.* ", //
            "FROM `sys_permission` as t10", //
            "INNER JOIN `sys_role_permission` as t11 on t10.`id`=t11.`permission_id` ", //
            "<where> ", //
            //"  <if test=\"roleIdList!=null && !roleIdList.isEmpty()\">",
            "     <foreach collection=\"roleIdList\" index=\"index\" item=\"item\" open=\"and t11.`role_id` in (\" separator=\",\" close=\")\">", //
            "           #{item} ", //
            "     </foreach>", //
            //"  </if>", //
            "</where>", //
            "</script>" //
    })
    List<SysPermission> getByRoleIds(@Param("roleIdList") List<Long> roleIdList);

自定义MyRealm.java

自定义MyRealm.java代码如下:

public class MyRealm extends AuthorizingRealm {
    @Autowired
    private SysUserMapper sysUserMapper;
    @Autowired
    private SysRoleMapper sysRoleMapper;
    @Autowired
    private SysPermissionMapper sysPermissionMapper;

    /**
     * 设置realm的名称
     */
    @Override
    public void setName(String name) {
        super.setName("myRealm");
    }

    /**
     * 认证使用(就是登录)<br>
     * 所谓的认证就是 和配置文件shiro.ini、数据库、内存中获取到用户的认证信息, 与用户输入的信息进行验证对比:<br>
     * 如果验证通过,就返回验证结果;如果验证失败,就返回异常信息。<br>
     * <b>验证对比过程一般如下:</b><br>
     * 1)一般情况下对比账户是否存在; <br>
     * 2)密码是否正确; <br>
     * 3)是否锁定; <br>
     * 4)是否账户过期;<br>
     * 等 <br>
     * 
     * @param token token是用户输入的:就是用户点击登录,在后台调用subject.login(token)方法传递进来的token信息。
     */
    @SuppressWarnings({ "unused", "unlikely-arg-type" })
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("‘MyRealm’执行认证操作:");
        if (token == null) {
            throw new UnsupportedTokenException();
        }

        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        if (userToken == null) {
            throw new UnsupportedTokenException();
        }

        // 获取当前需要登录的用户
        String username = userToken.getUsername();
        String userPwd = String.valueOf(userToken.getPassword());
        if (StringUtils.isBlank(username) || userPwd == null) {
            throw new IncorrectCredentialsException("用户名或密码不正确");
        }

        SysUser sysUser = this.sysUserMapper.getByUsername(username);
        if (sysUser == null) {
            throw new UnknownAccountException("用户名不存在");
        }

        Byte locked = Byte.valueOf("1");
        if (sysUser.getStatus().equals(locked)) {
            throw new LockedAccountException("用户已锁定");
        }
        Date now = new Date();
        if (sysUser.getExpireTime().before(now)) {
            throw new ExpiredCredentialsException("用户过期");
        }

        // 从数据库中取出密码,密码是加过盐的
        String password = sysUser.getPassword();
        // 从数据库中取出盐
        ByteSource byteSource = ByteSource.Util.bytes(sysUser.getSalt());

        SysUser simpleSysUser=new SysUser();
        simpleSysUser.setUserName(sysUser.getUsername());
        simpleSysUser.setEmail(sysUser.getEmail());
        simpleSysUser.setPhone(sysUser.getPhone());
        simpleSysUser.setNickName(sysUser.getNickName());
        simpleSysUser.setSignature(sysUser.getSignature());
        
        // 该信息会提交给SecurityManager,在SecurityManager内部会进行验证:
        // 用户输入的password+salt+md5+hashIterations 是否等于 db password? 等于则通过认证,否则不通过认证。
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(simpleSysUser, password.toCharArray(),
                byteSource, this.getName());
        return authenticationInfo;
    }

    /**
     * 授权使用(就是验证用户是否拥有某个角色、权限[资源])
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("‘MyRealm’执行授权操作:");
        SysUser simpleSysUser = (SysUser) principals.getPrimaryPrincipal();
        if (simpleSysUser == null) {
            throw new UnknownAccountException("用户名不存在");
        }
        String username=simpleSysUser.getUsername();
        SysUser sysUser = this.sysUserMapper.getByUsername(username);
        if (sysUser == null) {
            throw new UnknownAccountException("用户名不存在");
        }
        
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        // 1)设置授权的‘角色’
        List<SysRole> sysRoleList = this.sysRoleMapper.getByUserId(sysUser.getId());
        Set<String> roleSet = new HashSet<String>();
        List<Long> roleIdList = new ArrayList<Long>();
        for (SysRole sysRole : sysRoleList) {
             // 如果角色名字变来变去不稳定,可以采用 id(但是id也确保不了不变,只能变动时修改权限验证代码了?不过一般权限不会太容易变动。)
            roleSet.add(sysRole.getRoleName());
            roleIdList.add(sysRole.getId());
        }
        simpleAuthorizationInfo.setRoles(roleSet);

        // 2) 设置授权的‘角色’下的‘资源’
        if (sysRoleList.size() > 0) {
            Set<String> permissionSet = new HashSet<String>();
            List<SysPermission> sysPermissionList = this.sysPermissionMapper.getByRoleIds(roleIdList);
            for (SysPermission sysPermission : sysPermissionList) {
                permissionSet.add(sysPermission.getPermissionValue());
            }
            simpleAuthorizationInfo.setStringPermissions(permissionSet);
        }

        return simpleAuthorizationInfo;
    }
}

说明:
1)该com.dx.test.shiro.MyRealm就是配置在applicationContext-shiro.xml中的myRealm bean。
2)Realm这里自定义采用继承了 AuthorizingRealm,而AuthorizingRealm又继承了 AuthenticatingRealm,因:
  AuthenticatingRealm 包含了认证抽象接口:protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException
  AuthorizingRealm 包含了授权抽象接口:protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
  因此继承了AuthorizingRealm的MyRealm,拥有了两个功能:
  1)验证用户是否认证通过;
  2)验证用户是否已经授权某个角色、是否授权某个资源。

自定Realm系统如何实现认证、授权验证?

登录、登出

IndexController.java

@Controller
public class IndexController {
    /**
     * 访问/index
     */
    @RequestMapping(value = "/index", method = RequestMethod.GET)
    public ModelAndView index() {
        ModelAndView maView = new ModelAndView();
        maView.setViewName("index.jsp");
        return maView;
    }

    /**
     * 访问/login
     */
    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public ModelAndView login() {
        ModelAndView maView = new ModelAndView();
        maView.setViewName("login.jsp");
        return maView;
    }

    @RequestMapping(value = "/logout", method = RequestMethod.GET)
    public ModelAndView logout() {
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        
        ModelAndView maView = new ModelAndView();
        maView.setViewName("login.jsp");
        return maView;
    }

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public String doLogin(String username, String password,Map<String,Object> map) {
        
        BaseResult baseResult = null;
        ResultEnum enu = null;

        if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
            enu = ResultEnum.UserNameOrPasswordNull;
            baseResult = new BaseResult(enu.getCode(), enu.getMessage(), enu.getDesc());
            map.put("result", baseResult);
            return "login.jsp";
        }

        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password,true);
        try {
            subject.login(token);
        } catch (Exception e) {
            e.printStackTrace();
            enu = ResultEnum.LoginFail;
            baseResult = new BaseResult(enu.getCode(), enu.getMessage(), enu.getDesc());
            map.put("result", baseResult);
            return "login.jsp";
        }

        System.out.println("是否通过认证:" + subject.isAuthenticated());
        
        enu = ResultEnum.Success;
        baseResult = new BaseResult(enu.getCode(), enu.getMessage(), enu.getDesc());
        map.put("result", baseResult);
        return "redirect:admin/";
    }

}

登录页面/WEB-INF/views/login.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
    <form action="login" method="POST">
        <!-- 用户名 -->
        <label for="username">用户:</label> <input id="username"
            name="username" type="text" /> <br>
        <!-- 用户密码 -->
        <label for="password">密码:</label> <input id="password"
            name="password" type="password" /> <br>
        <!-- 登录 -->
        <input type="submit" title="登录"/>
    </form>
</body>
</html>

访问 http://localhost:8080/mybaits-test-dynamic-sql/login 

输入账户:admin,密码:123456,点击登录,进入后台页面/WEB-INF/views/admin.jsp

shiro权限验证用法:

1)页面标签验证:

/WEB-INF/views/admin.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Admin page</title>
</head>
<body>
    <!-- 原文链接:http://shiro.apache.org/web.html#Web-JSP%2FGSPTagLibrary -->
    <h3>The guest tag</h3>
    <shiro:guest>
    Hi there!  Please <a href="login">Login</a> or <a href="signup">Signup</a> today!<br>
    </shiro:guest>
    
    <h3>The user tag</h3>
    <shiro:user>
    Welcome back John!  Not John? Click <a href="login">here<a>
                to login.<br>
    </shiro:user>
    
    <h3>The authenticated tag</h3>
    <shiro:authenticated>
        <a href="updateAccount">Update your contact information</a>.<br>
    </shiro:authenticated>
    
    <h3>The notAuthenticated tag</h3>
    <shiro:notAuthenticated>
    Please <a href="login.jsp">login</a> in order to update your credit card information.<br>
    </shiro:notAuthenticated>
    
    <h3>The principal tag</h3>
    Hello,<shiro:principal />, how are you today?<br> 
    This is (mostly) equivalent to the following:<br> 
    Hello,<%=org.apache.shiro.SecurityUtils.getSubject().getPrincipal().toString()%>,how are you today?<br>
    
    <h4>Typed principal</h4>
    User Name:<!-- shiro:principal type="java.lang.String" --><br> 
    This is (mostly) equivalent to the following:<br> 
    User Name:<!-- %= org.apache.shiro.SecurityUtils.getSubject().getPrincipals().oneByType(String.class).toString() %--><br>
    
    <h4>Principal property</h4>
    Hello,<shiro:principal property="username" />, how are you today?<br> 
    This is (mostly) equivalent to the following:<br> 
    Hello,<%=((com.dx.test.model.SysUser) org.apache.shiro.SecurityUtils.getSubject().getPrincipal()).getUsername()%>,how are you today?<br> 
    Or, combined with the type attribute:<br>
    Hello,<shiro:principal type="com.dx.test.model.SysUser" property="username" />, how are you today?<br> 
    this is largely equivalent to the following:<br> 
    Hello,<%=org.apache.shiro.SecurityUtils.getSubject().getPrincipals().oneByType(com.dx.test.model.SysUser.class).getUsername().toString()%>,how are you today?<br>
    
    <h3>The hasRole tag</h3>
    <shiro:hasRole name="admin">
        <button class="btn btn-primary" id="btn_add_emp">新增</button>
        <br>
        <button class="btn btn-danger btn_all_del">删除</button>
        <br>
    </shiro:hasRole>
    
    <h3>The lacksRole tag</h3>
    <shiro:lacksRole name="test">
    Sorry, you are not allowed to administer the system.<br>
    </shiro:lacksRole>
    
    <h3>The hasAnyRole tag</h3>
    <shiro:hasAnyRoles name="developer, project manager, admin">
    You are either a developer, project manager, or admin.<br>
    </shiro:hasAnyRoles>
    
    <h3>The hasPermission tag</h3>
    <shiro:hasPermission name="article:delete">
        <a href="deleteArticle.jsp">Delete a new article</a>
        <br>
    </shiro:hasPermission>
    
    <h3>The lacksPermission tag</h3>
    <shiro:lacksPermission name="artilce:view">
    Sorry, you are not allowed to view article.<br>
    </shiro:lacksPermission>

    <a href="deleteArticle">测试注解1</a>
    <a href="queryArticle">测试注解2</a>
</body>
</html>

页面显示效果:

2)使用注解方式验证:

1)AdminController.java中页面代码使用注解进行权限验证:

@Controller
@RequestMapping("/admin")
public class AdminController {
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String index() {
        return "admin.jsp";
    }

    @RequiresPermissions("article:delete")
    @RequestMapping(value = "/deleteArticle", method = RequestMethod.GET)
    public String deleteArticle() {
        return "article/list.jsp";
    }
    
    @RequiresPermissions("article:query")
    @RequestMapping(value = "/queryArticle", method = RequestMethod.GET)
    public String queryArticle() {
        return "article/list.jsp";
    }
    
}

2)查看页面显示效果验证:shiro标签 控制权限用法:

点击“测试注解1”、“测试注解2”验证使用注解方式验证权限的用法(@RequiresPermissions("article:query"))。

 

本章中很多代码都未在本文展示,具体可以参考源码:https://github.com/478632418/springmv_without_web_xml/tree/master/mybaits-test-dynamic-sql

 

 

posted @ 2019-12-25 21:00  cctext  阅读(2150)  评论(0编辑  收藏  举报