Spring+SpringMVC+Hibernate 與 shiro 整合步驟

通過這篇文章你可以了解到:

  1. SSH 三大框架(spring + springMVC + Hiberante) 與 shiro 安全驗(yàn)證框架如何整合;
  2. 通過一個示例,快速理解 shiro 框架。

[TOC]

1. 業(yè)務(wù)需求分析

用戶 N - 角色 N - 權(quán)限 N

我們可以想象一下,在平時工作中的職務(wù),比如:業(yè)務(wù)經(jīng)理,部門主管等,他們擁有很多的權(quán)力,而一個公司中不會只有一個業(yè)務(wù)經(jīng)理,也不會只有一個部門主管,如果我們要給不同的人分配職務(wù)權(quán)力時,每次都是具體的條條框框去分配,人累心也累。而如果我們事先將具體的職務(wù)權(quán)力都賦予給某個具體的職務(wù)頭銜,那么就只需要把已經(jīng)定義好的職務(wù)頭銜賦予給某個人員就可以了,擁有該職務(wù)頭銜的人,也就間接獲得了對應(yīng)的職務(wù)權(quán)力,就省時省力又開心了。

這里的人員我們可以定義為用戶 User;將職務(wù)頭銜定義為角色 Role;將具體的權(quán)力定義為權(quán)限 Permission。

用戶 和 權(quán)限之間沒有直接關(guān)系,雖然在程序中也可以掛上鉤,但是不建議這樣做,這會違背數(shù)據(jù)庫的第三范式,會造成大量的冗余數(shù)據(jù)。

2. 創(chuàng)建數(shù)據(jù)庫

使用 MySQL 5.5,我們首先創(chuàng)建一個數(shù)據(jù)庫:shiro_demo

然后在數(shù)據(jù)庫中添加剛剛業(yè)務(wù)分析需要的實(shí)體表、多對多中間關(guān)系表。

use shiro_demo;

-- 3個實(shí)體:用戶N - N角色N - N權(quán)限
-- 2個實(shí)體中間表:用戶多對多角色,角色多對多權(quán)限

-- 用戶表 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
);

-- 權(quán)限表 tb_permission
create table tb_permission(
    permission_id int PRIMARY KEY auto_increment,
    permission_name varchar(100)
);

-- 創(chuàng)建 3 個實(shí)體之間的多對多關(guān)系實(shí)體
-- 用戶和角色之間的多對多關(guān)系中間表 tb_user_role
-- 建立這個多對多中間表目的是符合第三范式,減少不合理的冗余
create table tb_user_role(
    ur_id int PRIMARY KEY auto_increment,
    ur_user_id  int ,   ## 關(guān)聯(lián)用戶表的外鍵
    ur_role_id  int ## 關(guān)聯(lián)角色表的外鍵
);

-- 角色和權(quán)限之間的多對多關(guān)系中間表 tb_role_permission
create table tb_role_permission(
    rp_id int PRIMARY KEY auto_increment,
    rp_role_id int ,    ## 關(guān)聯(lián)角色表的外鍵
    rp_permission_id int ## 關(guān)聯(lián)權(quán)限表的外鍵
);

-- 插入數(shù)據(jù)
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 設(shè)置 'admin' 角色
insert into tb_user_role(ur_user_id, ur_role_id) values (1, 1);
-- 給 'admin' 角色設(shè)置 相應(yīng)的權(quán)限
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. 創(chuàng)建 maven webapp 工程

循環(huán)漸進(jìn),我們先來讓 hibernate 跑起來。先做這一塊的單元測試,沒有問題了之后再進(jìn)行下一步。

先導(dǎo)入 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數(shù)據(jù)庫連接池 -->
<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. 創(chuàng)建實(shí)體類(POJO)

配置實(shí)體類 User:

public class TbUserEntity {

    private int userId;
    private String userName;
    private String userPassword;
    private String userPasswordSalt;
    private Set<TbRoleEntity> roles; // 用戶對應(yīng)的角色集合
    
    // ... 省略 getter/setter 方法
}

配置實(shí)體類 Role:

public class TbRoleEntity {

    private int roleId;
    private String roleName;
    private Set<TbPermissionEntity> permissions; // 角色對應(yīng)的權(quán)限集合
  
    // ... 省略 getter/setter 方法
}

配置實(shí)體類 Permission:

public class TbPermissionEntity {

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

5. 配置 Hibernate 和 Mapping

hibernate 的配置我們有兩種方式可以選擇,一種是 hibernate 傳統(tǒng)的 xml 配置方式,另一種是 JPA(Java 持久化 API)支持的注解方式。因?yàn)樯婕暗蕉鄬Χ嚓P(guān)系的配置,雖然 JPA 注解的方式也是支持的,但是配置起來比較繁瑣,所以在例子中我們還是用 XML 配置文件方式,兩者實(shí)現(xiàn)的效果是一樣的。

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>

按照我們創(chuàng)建表的對應(yīng)方向,我們只需要在 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"/> <!-- 主鍵生成策略:依據(jù)本地?cái)?shù)據(jù)庫特性 -->
        </id>
        <property name="userName" column="user_name"/>
        <property name="userPassword" column="user_password"/>
        <property name="userPasswordSalt" column="user_password_salt"/>

        <!-- 配置多對多關(guān)系 -->
        <!--
            需要在實(shí)體類中配置對應(yīng)的 Set 集合
            name:表示該 Set 集合屬性名
            table:表示數(shù)據(jù)庫中確定兩個表之間多對多關(guān)系的表
            <key column="">:指定的字段名是當(dāng)前配置文件 <class> 所對應(yīng)的表在中間表中的外鍵
         -->
        <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"/>

        <!-- 配置多對多關(guān)系 -->
        <!--
            需要在實(shí)體類中配置對應(yīng)的 Set 集合
            name:表示該 Set 集合屬性名
            table:表示數(shù)據(jù)庫中確定兩個表之間多對多關(guān)系的表
            <key column="">:指定的字段名是當(dāng)前配置文件 <class> 所對應(yīng)的表在中間表中的外鍵
         -->
        <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("該用戶擁有的角色數(shù)量:" + user.getRoles().size());
        TbRoleEntity role = user.getRoles().iterator().next();
        System.out.println("該角色擁有的權(quán)限數(shù)量:" + role.getPermissions().size());
        session.close();
        sessionFactory.close();
    }
}

在這里小結(jié)一下:由 hibernate 完成查詢數(shù)據(jù)庫中用戶、角色、權(quán)限等信息的工作。接下來 hibernate 將這些信息交給 shiro 進(jìn)行安全驗(yàn)證的處理。

6. 配置 Spring

導(dǎo)入 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>

需要注意的是:

因?yàn)?spring mvc 的核心類 DispatcherServlet 是依賴于 Servlet的,所以還需要導(dǎo)入 Servlet。

6.1 spring 與 hibernate 整合

為了避免一個 Spring ContextApplication 配置文件中的內(nèi)容太多太雜,我們考慮將 spring-hibernate 的整合配置單獨(dú)放在一個 xml 文件中,首先創(chuàng)建一個 spring-hibernate.xml ,內(nèi)容如下:

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

然后我們再創(chuàng)建一個 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"/> 引入剛剛創(chuàng)建的spring-hibernate.xml 配置文件,也算是實(shí)現(xiàn)了配置文件之間的 “解耦” 吧。

6.2 創(chuàng)建 UserDAO

創(chuàng)建一個 IUserDAO 接口(面向接口編程):

package com.uzipi.shiro.user.dao;

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

public interface IUserDAO {

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

然后創(chuàng)建接口的實(shí)現(xiàn)類 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 // 加入事務(wù)管理
    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;
    }
}

有幾個知識點(diǎn)說明一下:

  1. @Repository 注解 表示將這個 dao 類交給 spring 管理,且說明了這是一個操作數(shù)據(jù)庫的類
  2. @Resource 注解 表示自動注入類,當(dāng)然也可以用 @Autowired 替換(注意兩個注解還是有一點(diǎn)點(diǎn)區(qū)別的哦)
  3. @Transactional 注解 表示該注解的方法受到 spring 事務(wù)管理,也就是說這一個方法就是一個事務(wù),必須加上這個注解,否則 spring 無法為 hibernate 開啟 session。
  4. 使用 hibernate 的 HQL 語句進(jìn)行查詢,寫法類似 SQL,但是可以用面向?qū)ο蟮姆绞讲僮鲾?shù)據(jù)實(shí)體。

大家可能覺得奇怪,為什么要在 配置 Spring 這一節(jié)中創(chuàng)建 UserDAO,目的很簡單,就是為了用這個 DAO 來測試一下我們的 Spring 和 Hibernate 是否整合成功嘛 _

6.3 測試 spring 與 hibernate 的整合

寫一個測試類,用到了 spring-test(不得不說,spring 提供的配套功能真多):

我們先導(dǎo)入 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 的注解
 * 幫助我們快速創(chuàng)建 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);
        // 斷言從數(shù)據(jù)庫中查詢出來的結(jié)果與我們給定的字符串相等
        Assert.assertEquals("zhangsan", userForLogin.getUserName());
    }
}

運(yùn)行測試,斷言成功,說明 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 到上下文環(huán)境 -->
    <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>

    <!-- 啟動監(jiān)聽器,需要放在 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 的配置中,有一些知識點(diǎn)需要注意:

  1. <context-param> 配置 spring.xml 的加載路徑,需要放在最前面(也在 shiroFilter 之前);
  2. shiroFilter 這個過濾器采用了委派代理模式 Delegating Proxy ,其代理的是 bean shiroFilter,也就是說,shiroFilter 的核心是在 spring bean 中定義的,調(diào)用 web.xml 的 shiroFilter 實(shí)質(zhì)上調(diào)用是 spring bean 中的 shiroFilter。關(guān)于 shiroFilter 的配置將在下面一節(jié)講到。
  3. 為了符合 web.xml 的文檔規(guī)范,<listener> 需要放在 <filter><servlet> 之間。

7. 配置 Shiro 與 spring 整合

首先我們先要導(dǎo)入 shiro 與 spring 整合的依賴包,pom.xml:

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

然后根據(jù) Apache shiro 官方網(wǎng)站提供的配置模版:

創(chuàng)建 spring-shiro.xml 文件,復(fù)制 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中委派代理的實(shí)質(zhì)內(nèi)容就在這里定義 -->
    <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 中所用到的委派代理的實(shí)質(zhì)內(nèi)容。
  2. shiroFilter 屬性中配置了 filterChainDefinitions ,這個屬性中配置的是需要對哪些資源的請求進(jìn)行攔截,anon 表示該資源不需要 shiro 控制,authc 表示需要經(jīng)過 shiro 的身份和權(quán)限驗(yàn)證,通過驗(yàn)證的才能訪問的資源。配置支持通配符,可參考 shiro 官網(wǎng)模版的提示。
  3. 配置 securityManager 需要指明 realm,這里我們使用到了自定義 Realm,下面我們會創(chuàng)建這個自定義 Realm 類,當(dāng)然我們也可以使用本地文件配置方式的 Realm,或者 shiro 提供的 jdbcRealm 模版(這個模板對數(shù)據(jù)庫表的表名和字段名要求比較嚴(yán)格,可拓展性比較弱,適合小型快速開發(fā)的項(xiàng)目)

接著我們將 spring-shiro.xml 引入到 spring.xml ,實(shí)現(xiàn) spring 與 shiro 的整合。

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

8. 創(chuàng)建自定義 Realm

Realm 是 shiro 框架的身份、權(quán)限等信息的數(shù)據(jù)源。

當(dāng)我們使用 shiro 去驗(yàn)證某個用戶的身份信息(比如帳號、密碼)或者是要驗(yàn)證某個用戶所擁有的角色和權(quán)限時,shiro 就會從這個 Realm 中查找對應(yīng)的身份、角色、權(quán)限等信息。

創(chuàng)建自定義的 Realm,其實(shí)就是在創(chuàng)建一個我們自定義的登錄身份認(rèn)證和權(quán)限驗(yàn)證的邏輯。

比如,有的時候業(yè)務(wù)需求規(guī)定,不能僅僅靠用戶名和密碼來判斷一個用戶的身份,有可能還需要通過用戶的手機(jī)、微信等等方式來驗(yàn)證,那么僅靠 shiro 提供的模版 Realm 就不太夠用,需要我們創(chuàng)建自定義 Realm。

Realm 有多種配置選擇:

  1. Realm 中的信息內(nèi)容可以是固定死的,比如在 Realm 中我們用 if 來判斷一個用戶名是否為 "zhangsan",那么這個系統(tǒng)就只允許帳號為"zhangsan"的人使用,其他人都不能使用;
  2. Realm 域信息也可以寫在本地文件中,但是不夠靈活;
  3. Realm 域中的內(nèi)容也可以通過讀取數(shù)據(jù)庫中的信息,達(dá)到動態(tài)更新 Realm 內(nèi)容的目的。

自定義 Realm 須要繼承抽象類 AuthorizingRealm,并且重寫兩個方法:

  1. doGetAuthorizationInfo:獲取角色授權(quán)的驗(yàn)證信息
  2. doGetAuthenticationInfo:獲取登錄身份的認(rèn)證信息

雖然 shiro 沒有強(qiáng)制性地規(guī)定,但我們還是需要重寫一下 getName() 方法,該方法用于獲取當(dāng)前 Realm 的名稱。

8.1 創(chuàng)建 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 名稱,可以自定義,最好是不容易重復(fù)的
     */
    @Override
    public String getName(){
        return this.getClass().toString();
    }

    /**
     * 權(quán)限驗(yàn)證的方法
     * @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); // 將角色名集合加入驗(yàn)證信息
        simpleAuthorizationInfo.setStringPermissions(permissionNameSet); // 權(quán)限名加入驗(yàn)證信息
        return simpleAuthorizationInfo;
    }

    /**
     * 登錄認(rèn)證的方法
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        // 轉(zhuǎn)型
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername(); // 獲取 用戶名
        // 獲取 密碼,字符數(shù)組需要轉(zhuǎn)型為 String
        String password = new String(upToken.getPassword());
        TbUserEntity user = new TbUserEntity();
        user.setUserName(username);
        user.setUserPassword(password);
        // 以下是登錄認(rèn)證的邏輯
        TbUserEntity userForLogin = userDAO.findUserForLogin(user);
        if (userForLogin != null){
            // 身份認(rèn)證成功,返回 SimpleAuthenticationInfo 對象
            return new SimpleAuthenticationInfo(
                    userForLogin.getUserName(), // 參數(shù)1:用戶名
                    userForLogin.getUserPassword(), // 參數(shù)2:密碼
                    this.getName() // 參數(shù)3:當(dāng)前 Realm 的名稱
                    );
        } else {
            // 身份認(rèn)證失敗
            throw new AuthenticationException("用戶名或密碼錯誤!");
        }
    }
}

從代碼上我們可以看到:

  1. doGetAuthorizationInfo 方法為了獲取用戶的權(quán)限驗(yàn)證信息,需要借助我們編寫的邏輯功能方法:findRoleNameByUsername(String username)findPermissionNameByUserName(String username) ,作用是按已登錄的用戶名,查詢出該用戶對應(yīng)的全部角色,以及角色下對應(yīng)的所有權(quán)限,并將這些信息加入到 SimpleAuthorizationInfo 對象中,shiro 在進(jìn)行權(quán)限驗(yàn)證時,通過自定義 Realm 返回的 SimpleAuthorizationInfo 就可以自動為我們攔截不符合權(quán)限以外的非法操作。
  2. 例子中,獲取用戶登錄身份認(rèn)證的邏輯比較簡單,通過 userDAO.findUserForLogin(user) 查詢數(shù)據(jù)庫中匹配用戶名和密碼的記錄,若能找到對應(yīng)的記錄,則登錄認(rèn)證通過,否則登錄認(rèn)證失敗。shiro 中判斷一個用戶登錄失敗的方式是直接拋出一個 AuthenticationException 異常。

8.2 UserDAO 中增加查詢角色和權(quán)限的方法

在自定義 Realm 類中,用到了 UserDAO 中的獲取角色名集合和權(quán)限集合的方法,我們在 UserDAO 中做定義。

在 6.2 一節(jié)中,我們已經(jīng)創(chuàng)建 UserDAO 實(shí)現(xiàn)類,并進(jìn)行了測試,現(xiàn)在我們須要在 IUserDAO 接口和實(shí)現(xiàn)類中增加兩個方法: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 // 指定當(dāng)前方法的事務(wù)
    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();
        // 查詢結(jié)果是否為空
        if (list == null || list.isEmpty()){
            return null;
        }
        return list.get(0);
    }

    @Override
    @Transactional // 指定當(dāng)前方法的事務(wù)
    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();
        // 查詢結(jié)果是否為空
        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 // 指定當(dāng)前方法的事務(wù)
    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();
        // 查詢結(jié)果是否為空
        if (list == null || list.isEmpty()){
            return null;
        }
        TbUserEntity user = list.get(0); // 查詢到用戶
        Set<String> permissionNameSet = new HashSet<>();
        // 遍歷用戶對應(yīng)的所有角色
        for (TbRoleEntity role : user.getRoles()) {
            Set<TbPermissionEntity> permissionSet = new HashSet<>();
            // 遍歷角色對應(yīng)的所有權(quán)限
            for (TbPermissionEntity permission : permissionSet) {
                permissionNameSet.add(permission.getPermissionName());
            }
        }
        return permissionNameSet;
    }
}

9. 創(chuàng)建 Controller

創(chuàng)建 AuthController 實(shí)現(xiàn)登錄認(rèn)證相關(guān)的跳轉(zhuǎn)控制

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 {

    /**
     * 跳轉(zhuǎn)到登錄頁
     * @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"; // 登錄身份驗(yàn)證成功,跳轉(zhuǎn)到個人頁 home.jsp
        } catch (AuthenticationException ace){
            ace.printStackTrace();
        }
        return "login"; // 登錄認(rèn)證失敗,返回 login.jsp 頁面要求繼續(xù)認(rèn)證
    }

    /**
     * 退出登錄
     * @param username
     * @param password
     * @return
     */
    @RequestMapping("/logout.do")
    public String logout(String username, String password){
        Subject subject = SecurityUtils.getSubject();
        // 當(dāng)前用戶是否為登錄狀態(tài),已登錄狀態(tài)則登出
        if (subject.isAuthenticated()) {
            subject.logout();
        }
        return "login"; // 退出登錄,并返回到登錄頁面
    }

}

10. 創(chuàng)建 JSP 頁面

10.1 創(chuàng)建 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 創(chuàng)建 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 的標(biāo)簽:

<shiro:principal/> 用于顯示當(dāng)前登錄認(rèn)證通過的用戶;

<shiro:hasRole name="admin">
  當(dāng)前登陸認(rèn)證通過的用戶,如果擁有 "admin" 角色(也就是通過自定義 Realm 配置的角色),就可以渲染顯示標(biāo)簽對中的內(nèi)容,否則在最終頁面中不渲染。
</shiro:hasRole>

至此,spring + spring mvc + hibernate + shiro 的框架整合就已經(jīng)完成了。

后面我還會寫一篇文章,具體講解如何通過 shiro 和 controller 的配合,實(shí)現(xiàn)對不同角色或權(quán)限進(jìn)行跳轉(zhuǎn)攔截控制。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,545評論 19 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,268評論 6 342
  • 說明:本文很多觀點(diǎn)和內(nèi)容來自互聯(lián)網(wǎng)以及各種資料,如果侵犯了您的權(quán)益,請及時聯(lián)系我,我會刪除相關(guān)內(nèi)容。 權(quán)限管理 基...
    寇寇寇先森閱讀 7,752評論 8 76
  • 這是寫在第197日精進(jìn)后的五分鐘,在簡書發(fā)表的時候發(fā)生了一件大事。日精進(jìn)195是我在簡書發(fā)表的第15篇文章,剛收到...
    小黃丫閱讀 198評論 0 1
  • 我先和你在一座小城,開一家小咖啡店
    易月閱讀 230評論 0 0

友情鏈接更多精彩內(nèi)容