通过这篇文章你可以了解到:
- SSH 三大框架(spring + springMVC + Hiberante) 与 shiro 安全验证框架如何整合;
- 通过一个示例,快速理解 shiro 框架。
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;
}
}
有几个知识点说明一下:
- @Repository 注解 表示将这个 dao 类交给 spring 管理,且说明了这是一个操作数据库的类
- @Resource 注解 表示自动注入类,当然也可以用 @Autowired 替换(注意两个注解还是有一点点区别的哦)
- @Transactional 注解 表示该注解的方法受到 spring 事务管理,也就是说这一个方法就是一个事务,必须加上这个注解,否则 spring 无法为 hibernate 开启 session。
- 使用 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 的配置中,有一些知识点需要注意:
<context-param>
配置 spring.xml 的加载路径,需要放在最前面(也在 shiroFilter 之前);shiroFilter
这个过滤器采用了委派代理模式 Delegating Proxy ,其代理的是 bean shiroFilter,也就是说,shiroFilter 的核心是在 spring bean 中定义的,调用 web.xml 的 shiroFilter 实质上调用是 spring bean 中的 shiroFilter。关于 shiroFilter 的配置将在下面一节讲到。- 为了符合 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>
需要注意:
- bean shiroFilter 要与 web.xml 中的 filter shiroFilter 名称一样。这里的 shiroFilter 配置也就是 web.xml 中所用到的委派代理的实质内容。
shiroFilter
属性中配置了filterChainDefinitions
,这个属性中配置的是需要对哪些资源的请求进行拦截,anon
表示该资源不需要 shiro 控制,authc
表示需要经过 shiro 的身份和权限验证,通过验证的才能访问的资源。配置支持通配符,可参考 shiro 官网模版的提示。- 配置
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 有多种配置选择:
- Realm 中的信息内容可以是固定死的,比如在 Realm 中我们用
if
来判断一个用户名是否为 "zhangsan",那么这个系统就只允许帐号为"zhangsan"的人使用,其他人都不能使用; - Realm 域信息也可以写在本地文件中,但是不够灵活;
- Realm 域中的内容也可以通过读取数据库中的信息,达到动态更新 Realm 内容的目的。
自定义 Realm 须要继承抽象类 AuthorizingRealm
,并且重写两个方法:
doGetAuthorizationInfo
:获取角色授权的验证信息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("用户名或密码错误!");
}
}
}
从代码上我们可以看到:
doGetAuthorizationInfo
方法为了获取用户的权限验证信息,需要借助我们编写的逻辑功能方法:findRoleNameByUsername(String username)
和findPermissionNameByUserName(String username)
,作用是按已登录的用户名,查询出该用户对应的全部角色,以及角色下对应的所有权限,并将这些信息加入到SimpleAuthorizationInfo
对象中,shiro 在进行权限验证时,通过自定义 Realm 返回的SimpleAuthorizationInfo
就可以自动为我们拦截不符合权限以外的非法操作。- 例子中,获取用户登录身份认证的逻辑比较简单,通过
userDAO.findUserForLogin(user)
查询数据库中匹配用户名和密码的记录,若能找到对应的记录,则登录认证通过,否则登录认证失败。shiro 中判断一个用户登录失败的方式是直接抛出一个AuthenticationException
异常。
8.2 UserDAO 中增加查询角色和权限的方法
在自定义 Realm 类中,用到了 UserDAO 中的获取角色名集合和权限集合的方法,我们在 UserDAO 中做定义。
在 6.2 一节中,我们已经创建 UserDAO 实现类,并进行了测试,现在我们须要在 IUserDAO 接口和实现类中增加两个方法: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 // 指定当前方法的事务
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 的配合,实现对不同角色或权限进行跳转拦截控制。