Spring+SpringMVC+Hibernate 与 shiro 整合步骤

通过这篇文章你可以了解到:

  1. SSH 三大框架(spring + springMVC + Hiberante) 与 shiro 安全验证框架如何整合;
  2. 通过一个示例,快速理解 shiro 框架。

[TOC]

1. 业务需求分析

用户 N - 角色 N - 权限 N

我们可以想象一下,在平时工作中的职务,比如:业务经理,部门主管等,他们拥有很多的权力,而一个公司中不会只有一个业务经理,也不会只有一个部门主管,如果我们要给不同的人分配职务权力时,每次都是具体的条条框框去分配,人累心也累。而如果我们事先将具体的职务权力都赋予给某个具体的职务头衔,那么就只需要把已经定义好的职务头衔赋予给某个人员就可以了,拥有该职务头衔的人,也就间接获得了对应的职务权力,就省时省力又开心了。

这里的人员我们可以定义为用户 User;将职务头衔定义为角色 Role;将具体的权力定义为权限 Permission。

用户 和 权限之间没有直接关系,虽然在程序中也可以挂上钩,但是不建议这样做,这会违背数据库的第三范式,会造成大量的冗余数据。

2. 创建数据库

使用 MySQL 5.5,我们首先创建一个数据库:shiro_demo

然后在数据库中添加刚刚业务分析需要的实体表、多对多中间关系表。

use shiro_demo;

-- 3个实体:用户N - N角色N - N权限
-- 2个实体中间表:用户多对多角色,角色多对多权限

-- 用户表 tb_user
create table tb_user(
    user_id int PRIMARY KEY auto_increment,
    user_name varchar(50) not null,
    user_password varchar(50) not null,
    user_password_salt  varchar(100)
);

-- 角色表 tb_role
create table tb_role(
    role_id int primary key auto_increment,
    role_name   varchar(50) not null
);

-- 权限表 tb_permission
create table tb_permission(
    permission_id int PRIMARY KEY auto_increment,
    permission_name varchar(100)
);

-- 创建 3 个实体之间的多对多关系实体
-- 用户和角色之间的多对多关系中间表 tb_user_role
-- 建立这个多对多中间表目的是符合第三范式,减少不合理的冗余
create table tb_user_role(
    ur_id int PRIMARY KEY auto_increment,
    ur_user_id  int ,   ## 关联用户表的外键
    ur_role_id  int ## 关联角色表的外键
);

-- 角色和权限之间的多对多关系中间表 tb_role_permission
create table tb_role_permission(
    rp_id int PRIMARY KEY auto_increment,
    rp_role_id int ,    ## 关联角色表的外键
    rp_permission_id int ## 关联权限表的外键
);

-- 插入数据
insert into tb_user(user_name, user_password) values ("zhangsan","123456");
insert into tb_role(role_name) values ("admin");
insert into tb_permission(permission_name) values ("user:insert");
insert into tb_permission(permission_name) values ("hotel:insert");
-- 给用户 zhangsan 设置 'admin' 角色
insert into tb_user_role(ur_user_id, ur_role_id) values (1, 1);
-- 给 'admin' 角色设置 相应的权限
insert into tb_role_permission(rp_role_id, rp_permission_id) values (1,1);
insert into tb_role_permission(rp_role_id, rp_permission_id) values (1,2);

3. 创建 maven webapp 工程

循环渐进,我们先来让 hibernate 跑起来。先做这一块的单元测试,没有问题了之后再进行下一步。

先导入 hibernate 的依赖包,pom.xml:

<!-- hibernate core -->
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-core</artifactId>
  <version>5.2.12.Final</version>
</dependency>

<!-- mysql-connector -->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.45</version>
</dependency>
<!-- c3p0数据库连接池 -->
<dependency>
  <groupId>com.mchange</groupId>
  <artifactId>c3p0</artifactId>
  <version>0.9.5.2</version>
</dependency>

<!-- junit 单元测试 -->
<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.12</version>
</dependency>

4. 创建实体类(POJO)

配置实体类 User:

public class TbUserEntity {

    private int userId;
    private String userName;
    private String userPassword;
    private String userPasswordSalt;
    private Set<TbRoleEntity> roles; // 用户对应的角色集合
    
    // ... 省略 getter/setter 方法
}

配置实体类 Role:

public class TbRoleEntity {

    private int roleId;
    private String roleName;
    private Set<TbPermissionEntity> permissions; // 角色对应的权限集合
  
    // ... 省略 getter/setter 方法
}

配置实体类 Permission:

public class TbPermissionEntity {

    private int permissionId;
    private String permissionName;
  
    // ... 省略 getter/setter 方法
}

5. 配置 Hibernate 和 Mapping

hibernate 的配置我们有两种方式可以选择,一种是 hibernate 传统的 xml 配置方式,另一种是 JPA(Java 持久化 API)支持的注解方式。因为涉及到多对多关系的配置,虽然 JPA 注解的方式也是支持的,但是配置起来比较繁琐,所以在例子中我们还是用 XML 配置文件方式,两者实现的效果是一样的。

5.1 Hibernate 主配置文件

配置 hibernate.cfg.xml

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        <property name="connection.url">jdbc:mysql://localhost:3306/shiro_demo</property>
        <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
        <property name="connection.username">root</property>
        <property name="connection.password">Cs123456</property>

        <!-- xml 配置 -->
        <value>classpath:mapper/TbUserEntity.hbm.xml</value>
        <value>classpath:mapper/TbRoleEntity.hbm.xml</value>
        <value>classpath:mapper/TbPermissionEntity.hbm.xml</value>
      
        <!-- JPA 注解配置 -->
        <!--<mapping class="com.uzipi.shiro_spring_hibernate.user.entity.TbPermissionEntity"/>-->
        <!--<mapping class="com.uzipi.shiro_spring_hibernate.user.entity.TbRoleEntity"/>-->
        <!--<mapping class="com.uzipi.shiro_spring_hibernate.user.entity.TbUserEntity"/>-->
        <!-- DB schema will be updated if needed -->
        <!-- <property name="hbm2ddl.auto">update</property> -->
    </session-factory>
</hibernate-configuration>

按照我们创建表的对应方向,我们只需要在 user 和 role 这两个 xml 文件中加上多对多的配置即可。

5.2 User Mapping 配置文件:

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
    "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>

    <class name="com.uzipi.shiro_spring_hibernate.user.entity.TbUserEntity" table="tb_user" schema="shiro_demo">
        <id name="userId" column="user_id">
            <generator class="native"/> <!-- 主键生成策略:依据本地数据库特性 -->
        </id>
        <property name="userName" column="user_name"/>
        <property name="userPassword" column="user_password"/>
        <property name="userPasswordSalt" column="user_password_salt"/>

        <!-- 配置多对多关系 -->
        <!--
            需要在实体类中配置对应的 Set 集合
            name:表示该 Set 集合属性名
            table:表示数据库中确定两个表之间多对多关系的表
            <key column="">:指定的字段名是当前配置文件 <class> 所对应的表在中间表中的外键
         -->
        <set name="roles" table="tb_user_role">
            <key column="ur_user_id"></key>
            <many-to-many column="ur_role_id"
                          class="com.uzipi.shiro_spring_hibernate.user.entity.TbRoleEntity"/>
        </set>
    </class>
</hibernate-mapping>

5.3 Role Mapping 配置文件

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
    "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>

    <class name="com.uzipi.shiro_spring_hibernate.user.entity.TbRoleEntity" table="tb_role" schema="shiro_demo">
        <id name="roleId" column="role_id">
            <generator class="native"/>
        </id>
        <property name="roleName" column="role_name"/>

        <!-- 配置多对多关系 -->
        <!--
            需要在实体类中配置对应的 Set 集合
            name:表示该 Set 集合属性名
            table:表示数据库中确定两个表之间多对多关系的表
            <key column="">:指定的字段名是当前配置文件 <class> 所对应的表在中间表中的外键
         -->
        <set name="permissions" table="tb_role_permission">
            <key column="rp_role_id"></key>
            <many-to-many column="rp_permission_id"
                          class="com.uzipi.shiro_spring_hibernate.user.entity.TbPermissionEntity"/>
        </set>
    </class>
</hibernate-mapping>

5.4 Permission Mappint 配置文件

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
    "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>

    <class name="com.uzipi.shiro.user.entity.TbPermissionEntity" table="tb_permission"
           schema="shiro_demo">
        <id name="permissionId" column="permission_id"/>
        <property name="permissionName" column="permission_name"/>
    </class>
</hibernate-mapping>

5.5 测试 Hibernate 配置是否成功

/**
 * 测试一下Hibernate
 */
public class HibernateTest {
    @Test
    public void testHiberante(){
        Configuration configure = new Configuration().configure();
        SessionFactory sessionFactory = configure.buildSessionFactory();
        Session session = sessionFactory.openSession();
        TbUserEntity user = session.get(TbUserEntity.class, 1);
        System.out.println("user = " + user.getUserName());
        System.out.println("该用户拥有的角色数量:" + user.getRoles().size());
        TbRoleEntity role = user.getRoles().iterator().next();
        System.out.println("该角色拥有的权限数量:" + role.getPermissions().size());
        session.close();
        sessionFactory.close();
    }
}

在这里小结一下:由 hibernate 完成查询数据库中用户、角色、权限等信息的工作。接下来 hibernate 将这些信息交给 shiro 进行安全验证的处理。

6. 配置 Spring

导入 Spring 的依赖包,pom.xml:

<!-- javax.servlet-api  spring 依赖于 servlet -->
<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>javax.servlet-api</artifactId>
  <version>4.0.0</version>
  <scope>provided</scope>
</dependency>
<!-- spring-webmvc -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-webmvc</artifactId>
  <version>4.3.12.RELEASE</version>
</dependency>
<!-- spring-web -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-web</artifactId>
  <version>4.3.12.RELEASE</version>
</dependency>
<!-- spring-orm -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-orm</artifactId>
  <version>4.3.12.RELEASE</version>
</dependency>
<!-- spring-tx transaction -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-tx</artifactId>
  <version>4.3.12.RELEASE</version>
</dependency>

需要注意的是:

因为 spring mvc 的核心类 DispatcherServlet 是依赖于 Servlet的,所以还需要导入 Servlet。

6.1 spring 与 hibernate 整合

为了避免一个 Spring ContextApplication 配置文件中的内容太多太杂,我们考虑将 spring-hibernate 的整合配置单独放在一个 xml 文件中,首先创建一个 spring-hibernate.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:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- 整合 Hibernate 配置 BEGIN -->
    <!-- dataSource -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver" />
        <property name="jdbcUrl" value="jdbc:mysql://wangchm-PC:3306/shiro_demo" />
        <property name="user" value="root" />
        <property name="password" value="Cs123456" />
    </bean>
    <!-- sessionFactory -->
    <bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="mappingLocations">
            <list>
                <value>mapper/TbUserEntity.hbm.xml</value>
                <value>mapper/TbRoleEntity.hbm.xml</value>
                <value>mapper/TbPermissionEntity.hbm.xml</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.format_sql">true</prop>
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop>
            </props>
        </property>
    </bean>
    <!-- transactionManager -->
    <bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>
    <tx:annotation-driven transaction-manager="transactionManager"/>
    <!-- 整合 Hibernate 配置 END -->

</beans>

然后我们再创建一个 spring.xml ,这个才是 spring 框架的核心配置文件:

<?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"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context-4.3.xsd">

    <context:annotation-config />
    <context:component-scan base-package="com.uzipi.shiro"></context:component-scan>
    <mvc:annotation-driven />
    <mvc:default-servlet-handler />

    <!-- 引入 spring 与 hibernate 整合配置 -->
    <import resource="spring-hibernate.xml"/>

</beans>

注意到了吗?在 spring.xml 文件中,我们使用 <import resource="spring-hibernate.xml"/> 引入刚刚创建的spring-hibernate.xml 配置文件,也算是实现了配置文件之间的 “解耦” 吧。

6.2 创建 UserDAO

创建一个 IUserDAO 接口(面向接口编程):

package com.uzipi.shiro.user.dao;

import com.uzipi.shiro.user.entity.TbUserEntity;

public interface IUserDAO {

    /**
     * 登录
     * @param user
     * @return
     */
    TbUserEntity findUserForLogin(TbUserEntity user);
}

然后创建接口的实现类 UserDAO:

package com.uzipi.shiro.user.dao.impl;

import com.uzipi.shiro.user.dao.IUserDAO;
import com.uzipi.shiro.user.entity.TbUserEntity;
import org.hibernate.Criteria;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.CriteriaQuery;
import org.hibernate.query.criteria.internal.CriteriaBuilderImpl;
import org.hibernate.query.criteria.internal.CriteriaQueryImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Repository
public class UserDAO implements IUserDAO {

    @Resource
    private SessionFactory sessionFactory; // 注入 Hibernate session 工厂

    @Override
    @Transactional // 加入事务管理
    public TbUserEntity findUserForLogin(TbUserEntity user) {
        TbUserEntity loginUser = sessionFactory.getCurrentSession()
                .createQuery("from TbUserEntity u " +
                        " where u.userName=:userName " +
                        " and u.userPassword=:userPassword ", TbUserEntity.class)
                .setParameter("userName", user.getUserName())
                .setParameter("userPassword", user.getUserPassword())
                .getResultList().get(0);
        return loginUser;
    }
}

有几个知识点说明一下:

  1. @Repository 注解 表示将这个 dao 类交给 spring 管理,且说明了这是一个操作数据库的类
  2. @Resource 注解 表示自动注入类,当然也可以用 @Autowired 替换(注意两个注解还是有一点点区别的哦)
  3. @Transactional 注解 表示该注解的方法受到 spring 事务管理,也就是说这一个方法就是一个事务,必须加上这个注解,否则 spring 无法为 hibernate 开启 session。
  4. 使用 hibernate 的 HQL 语句进行查询,写法类似 SQL,但是可以用面向对象的方式操作数据实体。

大家可能觉得奇怪,为什么要在 配置 Spring 这一节中创建 UserDAO,目的很简单,就是为了用这个 DAO 来测试一下我们的 Spring 和 Hibernate 是否整合成功嘛 _

6.3 测试 spring 与 hibernate 的整合

写一个测试类,用到了 spring-test(不得不说,spring 提供的配套功能真多):

我们先导入 spring-text 依赖包:

<!-- spring-test -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-test</artifactId>
  <version>4.3.12.RELEASE</version>
  <scope>test</scope>
</dependency>

然后编写测试类:

import com.uzipi.shiro.user.dao.IUserDAO;
import com.uzipi.shiro.user.entity.TbUserEntity;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

/**
 * 使用 spring test 的注解
 * 帮助我们快速创建 spring context
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class SpringTest {

    @Resource
    private IUserDAO userDAO;

    @Test
    public void testSpring(){
        // 使用 Spring test 测试
        TbUserEntity user = new TbUserEntity();
        user.setUserName("zhangsan");
        user.setUserPassword("123456");
        TbUserEntity userForLogin = userDAO.findUserForLogin(user);
        // 断言从数据库中查询出来的结果与我们给定的字符串相等
        Assert.assertEquals("zhangsan", userForLogin.getUserName());
    }
}

运行测试,断言成功,说明 spring 与 hibernate 整合成功了。

6.4 配置 SpringMVC

(1)在 spring.xml 中加入视图解析器的配置

<!-- SpringMVC 视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
  <!-- 前缀 -->
  <property name="prefix" value="/WEB-INF/pages/"/>
  <!-- 后缀 -->
  <property name="suffix" value=".jsp"/>
</bean>

6.4 配置 web.xml

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>
  <welcome-file-list>
    <welcome-file>login</welcome-file>
  </welcome-file-list>

    <!-- 在 shiro 之前,需要先加载 spring 到上下文环境 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring.xml</param-value>
    </context-param>

  <!-- The filter-name matches name of a 'shiroFilter' bean inside applicationContext.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>

  <!-- Make sure any request you want accessible to Shiro is filtered. /* catches all -->
  <!-- requests.  Usually this filter mapping is defined first (before all others) to -->
  <!-- ensure that Shiro works in subsequent filters in the filter chain:             -->
  <filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

    <!-- 启动监听器,需要放在 shiroFilter 与 springMVC 的配置之间 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

  <!-- spring MVC 的配置要放在 shiroFilter 之后 -->
  <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:spring.xml</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>springMVC</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
  
</web-app>

在 web.xml 的配置中,有一些知识点需要注意:

  1. <context-param> 配置 spring.xml 的加载路径,需要放在最前面(也在 shiroFilter 之前);
  2. shiroFilter 这个过滤器采用了委派代理模式 Delegating Proxy ,其代理的是 bean shiroFilter,也就是说,shiroFilter 的核心是在 spring bean 中定义的,调用 web.xml 的 shiroFilter 实质上调用是 spring bean 中的 shiroFilter。关于 shiroFilter 的配置将在下面一节讲到。
  3. 为了符合 web.xml 的文档规范,<listener> 需要放在 <filter><servlet> 之间。

7. 配置 Shiro 与 spring 整合

首先我们先要导入 shiro 与 spring 整合的依赖包,pom.xml:

<!-- shiro-spring 整合 -->
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring</artifactId>
  <version>1.4.0</version>
</dependency>

然后根据 Apache shiro 官方网站提供的配置模版:

创建 spring-shiro.xml 文件,复制 shiro 官方提供的配置模版,并做一些修改:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       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.xsd">

    <!-- shiro 的核心,web.xml中委派代理的实质内容就在这里定义 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
      <property name="securityManager" ref="securityManager"/>
      <!-- 没有登录的用户请求,将会返回到这个地址 -->
      <property name="loginUrl" value="/login"/> 
      <!-- <property name="successUrl" value="/home.jsp"/> -->
      <!-- <property name="unauthorizedUrl" value="/unauthorized.jsp"/> -->
      <property name="filterChainDefinitions">
        <value>
          <!--/admin/** = authc, roles[admin]-->
          <!--/docs/** = authc, perms[document:read]-->
          /index = authc
        </value>
      </property>
    </bean>

    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!-- 单 Realm。如果是多 Realm 需要配置为 'realms' -->
        <property name="realm" ref="myRealm"/>
    </bean>
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

    <!-- 自定义 Realm 的类 -->
    <bean id="myRealm" class="com.uzipi.shiro.user.shiro.HibernateRealm">
    </bean>
</beans>

需要注意:

  1. bean shiroFilter 要与 web.xml 中的 filter shiroFilter 名称一样。这里的 shiroFilter 配置也就是 web.xml 中所用到的委派代理的实质内容。
  2. shiroFilter 属性中配置了 filterChainDefinitions ,这个属性中配置的是需要对哪些资源的请求进行拦截,anon 表示该资源不需要 shiro 控制,authc 表示需要经过 shiro 的身份和权限验证,通过验证的才能访问的资源。配置支持通配符,可参考 shiro 官网模版的提示。
  3. 配置 securityManager 需要指明 realm,这里我们使用到了自定义 Realm,下面我们会创建这个自定义 Realm 类,当然我们也可以使用本地文件配置方式的 Realm,或者 shiro 提供的 jdbcRealm 模版(这个模板对数据库表的表名和字段名要求比较严格,可拓展性比较弱,适合小型快速开发的项目)

接着我们将 spring-shiro.xml 引入到 spring.xml ,实现 spring 与 shiro 的整合。

<!-- 引入 spring 与 shiro 整合配置 -->
<import resource="spring-shiro.xml"/>

8. 创建自定义 Realm

Realm 是 shiro 框架的身份、权限等信息的数据源。

当我们使用 shiro 去验证某个用户的身份信息(比如帐号、密码)或者是要验证某个用户所拥有的角色和权限时,shiro 就会从这个 Realm 中查找对应的身份、角色、权限等信息。

创建自定义的 Realm,其实就是在创建一个我们自定义的登录身份认证和权限验证的逻辑。

比如,有的时候业务需求规定,不能仅仅靠用户名和密码来判断一个用户的身份,有可能还需要通过用户的手机、微信等等方式来验证,那么仅靠 shiro 提供的模版 Realm 就不太够用,需要我们创建自定义 Realm。

Realm 有多种配置选择:

  1. Realm 中的信息内容可以是固定死的,比如在 Realm 中我们用 if 来判断一个用户名是否为 "zhangsan",那么这个系统就只允许帐号为"zhangsan"的人使用,其他人都不能使用;
  2. Realm 域信息也可以写在本地文件中,但是不够灵活;
  3. Realm 域中的内容也可以通过读取数据库中的信息,达到动态更新 Realm 内容的目的。

自定义 Realm 须要继承抽象类 AuthorizingRealm,并且重写两个方法:

  1. doGetAuthorizationInfo:获取角色授权的验证信息
  2. doGetAuthenticationInfo:获取登录身份的认证信息

虽然 shiro 没有强制性地规定,但我们还是需要重写一下 getName() 方法,该方法用于获取当前 Realm 的名称。

8.1 创建 Realm 类

package com.uzipi.shiro.user.shiro;

import com.uzipi.shiro.user.dao.IUserDAO;
import com.uzipi.shiro.user.entity.TbUserEntity;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import javax.annotation.Resource;
import java.util.Set;

public class HibernateRealm extends AuthorizingRealm{

    @Resource
    private IUserDAO userDAO;  // 注入 userDAO

    /**
     * 获取一个全局唯一的 Realm 名称,可以自定义,最好是不容易重复的
     */
    @Override
    public String getName(){
        return this.getClass().toString();
    }

    /**
     * 权限验证的方法
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = principals.getPrimaryPrincipal().toString();
        Set<String> roleNameSet = userDAO.findRoleNameByUsername(username);
        Set<String> permissionNameSet = userDAO.findPermissionNameByUserName(username);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setRoles(roleNameSet); // 将角色名集合加入验证信息
        simpleAuthorizationInfo.setStringPermissions(permissionNameSet); // 权限名加入验证信息
        return simpleAuthorizationInfo;
    }

    /**
     * 登录认证的方法
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        // 转型
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername(); // 获取 用户名
        // 获取 密码,字符数组需要转型为 String
        String password = new String(upToken.getPassword());
        TbUserEntity user = new TbUserEntity();
        user.setUserName(username);
        user.setUserPassword(password);
        // 以下是登录认证的逻辑
        TbUserEntity userForLogin = userDAO.findUserForLogin(user);
        if (userForLogin != null){
            // 身份认证成功,返回 SimpleAuthenticationInfo 对象
            return new SimpleAuthenticationInfo(
                    userForLogin.getUserName(), // 参数1:用户名
                    userForLogin.getUserPassword(), // 参数2:密码
                    this.getName() // 参数3:当前 Realm 的名称
                    );
        } else {
            // 身份认证失败
            throw new AuthenticationException("用户名或密码错误!");
        }
    }
}

从代码上我们可以看到:

  1. doGetAuthorizationInfo 方法为了获取用户的权限验证信息,需要借助我们编写的逻辑功能方法:findRoleNameByUsername(String username)findPermissionNameByUserName(String username) ,作用是按已登录的用户名,查询出该用户对应的全部角色,以及角色下对应的所有权限,并将这些信息加入到 SimpleAuthorizationInfo 对象中,shiro 在进行权限验证时,通过自定义 Realm 返回的 SimpleAuthorizationInfo 就可以自动为我们拦截不符合权限以外的非法操作。
  2. 例子中,获取用户登录身份认证的逻辑比较简单,通过 userDAO.findUserForLogin(user) 查询数据库中匹配用户名和密码的记录,若能找到对应的记录,则登录认证通过,否则登录认证失败。shiro 中判断一个用户登录失败的方式是直接抛出一个 AuthenticationException 异常。

8.2 UserDAO 中增加查询角色和权限的方法

在自定义 Realm 类中,用到了 UserDAO 中的获取角色名集合和权限集合的方法,我们在 UserDAO 中做定义。

在 6.2 一节中,我们已经创建 UserDAO 实现类,并进行了测试,现在我们须要在 IUserDAO 接口和实现类中增加两个方法:findRoleNameByUsernamefindPermissionNameByUserName

新的 UserDAO 代码如下:

package com.uzipi.shiro.user.dao.impl;

import com.uzipi.shiro.user.dao.IUserDAO;
import com.uzipi.shiro.user.entity.TbPermissionEntity;
import com.uzipi.shiro.user.entity.TbRoleEntity;
import com.uzipi.shiro.user.entity.TbUserEntity;
import org.hibernate.SessionFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Repository
public class UserDAO implements IUserDAO {

    @Resource
    private SessionFactory sessionFactory; // 注入 Hibernate session 工厂

    @Override
    @Transactional // 指定当前方法的事务
    public TbUserEntity findUserForLogin(TbUserEntity user) {
        List<TbUserEntity> list = sessionFactory.getCurrentSession()
                .createQuery("from TbUserEntity u " +
                        " where u.userName=:userName " +
                        " and u.userPassword=:userPassword ", TbUserEntity.class)
                .setParameter("userName", user.getUserName())
                .setParameter("userPassword", user.getUserPassword())
                .getResultList();
        // 查询结果是否为空
        if (list == null || list.isEmpty()){
            return null;
        }
        return list.get(0);
    }

    @Override
    @Transactional // 指定当前方法的事务
    public Set<String> findRoleNameByUsername(String username) {
        List<TbUserEntity> list = sessionFactory.getCurrentSession()
                .createQuery("from TbUserEntity u " +
                        " where u.userName=:userName", TbUserEntity.class)
                .setParameter("userName", username)
                .getResultList();
        // 查询结果是否为空
        if (list == null || list.isEmpty()){
            return null;
        }
        TbUserEntity user = list.get(0);
        Set<String> roleNameSet =  new HashSet<>();
        for (TbRoleEntity role : user.getRoles()) {
            roleNameSet.add(role.getRoleName());
        }
        return roleNameSet;
    }

    @Override
    @Transactional // 指定当前方法的事务
    public Set<String> findPermissionNameByUserName(String username) {
        List<TbUserEntity> list = sessionFactory.getCurrentSession()
                .createQuery("from TbUserEntity u " +
                        " where u.userName=:userName", TbUserEntity.class)
                .setParameter("userName", username)
                .getResultList();
        // 查询结果是否为空
        if (list == null || list.isEmpty()){
            return null;
        }
        TbUserEntity user = list.get(0); // 查询到用户
        Set<String> permissionNameSet = new HashSet<>();
        // 遍历用户对应的所有角色
        for (TbRoleEntity role : user.getRoles()) {
            Set<TbPermissionEntity> permissionSet = new HashSet<>();
            // 遍历角色对应的所有权限
            for (TbPermissionEntity permission : permissionSet) {
                permissionNameSet.add(permission.getPermissionName());
            }
        }
        return permissionNameSet;
    }
}

9. 创建 Controller

创建 AuthController 实现登录认证相关的跳转控制

package com.uzipi.shiro.user.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class AuthController {

    /**
     * 跳转到登录页
     * @return
     */
    @RequestMapping("/login")
    public String forwardToLogin(){
        return "login";
    }

    /**
     * 登录
     * @param username
     * @param password
     * @return
     */
    @RequestMapping("/login.do")
    public String login(String username, String password){
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try{
            SecurityUtils.getSubject().login(token);
            return "home"; // 登录身份验证成功,跳转到个人页 home.jsp
        } catch (AuthenticationException ace){
            ace.printStackTrace();
        }
        return "login"; // 登录认证失败,返回 login.jsp 页面要求继续认证
    }

    /**
     * 退出登录
     * @param username
     * @param password
     * @return
     */
    @RequestMapping("/logout.do")
    public String logout(String username, String password){
        Subject subject = SecurityUtils.getSubject();
        // 当前用户是否为登录状态,已登录状态则登出
        if (subject.isAuthenticated()) {
            subject.logout();
        }
        return "login"; // 退出登录,并返回到登录页面
    }

}

10. 创建 JSP 页面

10.1 创建 login.jsp 页面

<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>用户登录</title>
    <base href="<%=request.getContextPath()%>/"/>
</head>
<body>
    <form action="login.do" method="post">
        <input type="text" name="username" placeholder="请输入用户名"/> <br>
        <input type="password" name="password" placeholder="请输入密码"/> <br>
        <input type="checkbox" name="rememberMe" />记住我 <br>
        <input type="submit" value="登录" />
    </form>
</body>
</html>

10.2 创建 home.jsp 页面

<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<!DOCTYPE html>
<html>
<head>
    <title>登录成功页</title>
    <base href="<%=request.getContextPath()%>/"/>
</head>
<body>
    你好,<shiro:principal/>
    <br>
    <shiro:hasRole name="admin">
        你的角色是:管理员
    </shiro:hasRole>
    <br>
    <a href="logout.do">安全退出</a>
</body>
</html>

使用 shiro 的标签:

<shiro:principal/> 用于显示当前登录认证通过的用户;

<shiro:hasRole name="admin">
  当前登陆认证通过的用户,如果拥有 "admin" 角色(也就是通过自定义 Realm 配置的角色),就可以渲染显示标签对中的内容,否则在最终页面中不渲染。
</shiro:hasRole>

至此,spring + spring mvc + hibernate + shiro 的框架整合就已经完成了。

后面我还会写一篇文章,具体讲解如何通过 shiro 和 controller 的配合,实现对不同角色或权限进行跳转拦截控制。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容