通過這篇文章你可以了解到:
- SSH 三大框架(spring + springMVC + Hiberante) 與 shiro 安全驗(yàn)證框架如何整合;
- 通過一個示例,快速理解 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)說明一下:
- @Repository 注解 表示將這個 dao 類交給 spring 管理,且說明了這是一個操作數(shù)據(jù)庫的類
- @Resource 注解 表示自動注入類,當(dāng)然也可以用 @Autowired 替換(注意兩個注解還是有一點(diǎn)點(diǎn)區(qū)別的哦)
- @Transactional 注解 表示該注解的方法受到 spring 事務(wù)管理,也就是說這一個方法就是一個事務(wù),必須加上這個注解,否則 spring 無法為 hibernate 開啟 session。
- 使用 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)需要注意:
-
<context-param>配置 spring.xml 的加載路徑,需要放在最前面(也在 shiroFilter 之前); -
shiroFilter這個過濾器采用了委派代理模式 Delegating Proxy ,其代理的是 bean shiroFilter,也就是說,shiroFilter 的核心是在 spring bean 中定義的,調(diào)用 web.xml 的 shiroFilter 實(shí)質(zhì)上調(diào)用是 spring bean 中的 shiroFilter。關(guān)于 shiroFilter 的配置將在下面一節(jié)講到。 - 為了符合 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>
需要注意:
- bean shiroFilter 要與 web.xml 中的 filter shiroFilter 名稱一樣。這里的 shiroFilter 配置也就是 web.xml 中所用到的委派代理的實(shí)質(zhì)內(nèi)容。
-
shiroFilter屬性中配置了filterChainDefinitions,這個屬性中配置的是需要對哪些資源的請求進(jìn)行攔截,anon表示該資源不需要 shiro 控制,authc表示需要經(jīng)過 shiro 的身份和權(quán)限驗(yàn)證,通過驗(yàn)證的才能訪問的資源。配置支持通配符,可參考 shiro 官網(wǎng)模版的提示。 - 配置
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 有多種配置選擇:
- Realm 中的信息內(nèi)容可以是固定死的,比如在 Realm 中我們用
if來判斷一個用戶名是否為 "zhangsan",那么這個系統(tǒng)就只允許帳號為"zhangsan"的人使用,其他人都不能使用; - Realm 域信息也可以寫在本地文件中,但是不夠靈活;
- Realm 域中的內(nèi)容也可以通過讀取數(shù)據(jù)庫中的信息,達(dá)到動態(tài)更新 Realm 內(nèi)容的目的。
自定義 Realm 須要繼承抽象類 AuthorizingRealm,并且重寫兩個方法:
-
doGetAuthorizationInfo:獲取角色授權(quán)的驗(yàn)證信息 -
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("用戶名或密碼錯誤!");
}
}
}
從代碼上我們可以看到:
-
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)限以外的非法操作。 - 例子中,獲取用戶登錄身份認(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)類中增加兩個方法:findRoleNameByUsername 和 findPermissionNameByUserName 。
新的 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)攔截控制。