本篇博客主要讲述的是两者的集成,不涉及到各自的具体细节和功能。
由于官方给出的文档不够详细,对新手而言通过官方文档还不能够很快的搭建出SpringShiro的web工程,本博客将通过实际的案例提供具体的教程。
案例分析:
项目名称:假期系统
组织机构:部门 > 小组
角色:Admin, SeniorManager,TeamLeader,Developer
资源:假期Leave
权限:申请Apply,审核Review, 复核ReReview,查看View
角色级别:Admin > Senior Manager >Team Leader > Developer
角色权限:
| Admin | Senior Manager | Team Leader | Developer |
Apply | Y | Y | Y | Y |
Review | Y | Y | Y | N |
ReReview | Y | Y | N | N |
View | Y | Y | Y | Y |
特殊需求:
1. 角色可以绑定到不同的组织机构级别,比如SeniorManager 是基于部门的,TeamLeader则是基于小组的
2. 角色的级别关系只能在相同的部门中,不同部门之间的角色没有关联
3. 审核只能是自己上级角色审核,复核必须是审核者的上级角色复核,即Developer提出的请假申请只能TeamLeader来审核,并且由SeniorManager复核。
数据库设计
最简单的数据库设计如下:
表名 | 列名 | 描述 | 类型 |
t_dept 部门表 | id | 部门编号 | Int |
name | 部门名称 | Varchar | |
| |||
t_team 小组表 | id | 小组编号 | Int |
name | 小组名称 | Varchar | |
| |||
t_user 用户表 | id | 用户编号 | Int |
username | 用户名称 | Varchar | |
password | 用户加密密码 | Varchar | |
salt | 用户加密盐 | Varchar | |
manager_id | 用户上级领导id | Int | |
| |||
t_role 角色表 | id | 角色编号 | Int |
name | 角色名称 | Varchar | |
dept_flag | 角色是否关联部门 | Bool | |
team_flag | 角色是否关联小组 | Bool | |
| |||
t_user_role 用户所属角色表 | Id | 用户角色编号 | Int |
user_id | 用户编号 | Int | |
dept_id | 部门编号 | Int | |
team_id | 小组编号 | Int | |
role_id | 角色编号 | Int | |
| |||
t_resource 资源表 | id | 资源编号 | Int |
name | 资源名称 | Varchar | |
| |||
t_permission 权限表 | id | 权限编号 | Int |
name | 权限名称 | Varchar | |
| |||
t_role_resource_permission 角色拥有的资源权限表 | id | 编号 | Int |
role_id | 角色编号 | Int | |
resource_id | 资源编号 | Int | |
permission_id | 权限编号 | Int | |
| |||
t_leave 请假申请表 | id | 请假申请编号 | Int |
user_id | 请假者编号 | Int | |
days | 请假时长 | Int | |
fromDate | 开始日期 | Date | |
toDate | 结束日期 | Date | |
remark | 请假说明 | Varchar | |
flag | 请假状态 | Int | |
apply_date | 申请的时间 | Datetime | |
review_user_id | 审核人编号 | Int | |
review_date | 审核日期 | Date | |
review_remark | 审核说明 | Varchar | |
rereview_user_id | 复核人编号 | Int | |
rereview_date | 复核日期 | Date | |
rereview_remark | 复核说明 | Varchar |
添加测试数据:
项目搭建
Maven Project: pom.xml
新建一个maven项目,packaging设定为war,设定Spring和Shiro的版本为3.1.4.RELEASE和1.2.3,并添加必要的依赖。具体如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xx.shiro</groupId>
<artifactId>demo</artifactId>
<version>1.0.0</version>
<packaging>war</packaging>
<properties>
<java-version>1.7</java-version>
<org.springframework-version>3.1.4.RELEASE</org.springframework-version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<org.shiro-version>1.2.3</org.shiro-version>
</properties>
<dependencies>
<!-- Java EE 6 API java enterprise edition -->
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>6.0</version>
<scope>provided</scope>
</dependency>
<!-- spring-context [annotation...] -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${org.springframework-version}</version>
<exclusions>
<!-- Exclude Commons Logging in favor of SLF4j -->
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- spring modules start -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- apache shiro start -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${org.shiro-version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${org.shiro-version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${org.shiro-version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-quartz</artifactId>
<version>${org.shiro-version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${org.shiro-version}</version>
</dependency>
<!-- apache shiro end -->
<!-- jackson -->
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId>
<version>1.8.8</version>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.8.8</version>
</dependency>
<!-- java.lang.* Extension -->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<!-- java.io.InputStream and Reader Extension -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<!-- JavaServer Pages Standard Tag Library -->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- slf4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.6</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.6</version>
</dependency>
<!-- log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.15</version>
<exclusions>
<exclusion>
<groupId>com.sun.jmx</groupId>
<artifactId>jmxri</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jdmk</groupId>
<artifactId>jmxtools</artifactId>
</exclusion>
<exclusion>
<groupId>javax.jms</groupId>
<artifactId>jms</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- encode -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.6</version>
</dependency>
<dependency>
<groupId>com.xx.rtpc.shared.utils</groupId>
<artifactId>rtpc-crypt</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.4</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2</version>
</dependency>
</dependencies>
<build>
<finalName>demo</finalName>
</build>
</project>
配置web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0"
metadata-complete="false">
<display-name>shiro-example</display-name>
<!-- Spring配置文件开始 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/dispatcher-servlet.xml
</param-value>
</context-param>
<!-- listener -->
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- shiro 安全过滤器 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>*.spring</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>/home.spring</welcome-file>
</welcome-file-list>
</web-app>
Spring Configure: dispatcher-servlet.xml
在src/main/webapp/WEB-INF下面新建Spring的配置文件,命名为dispatcher-servlet.xml.具体设定如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:util="http://www.springframework.org/schema/util" xmlns:aop="http://www.springframework.org/schema/aop"
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/util http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<mvc:resources location="/resources/" mapping="/resources/**" />
<context:component-scan base-package="com.xx.shiro.demo"></context:component-scan>
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=UTF-8</value>
<value>text/plain;charset=UTF-8</value>
</list>
</property>
</bean>
<bean id="jsonHttpMessageConverter"
class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>application/json</value>
<value>text/json</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!-- 凭证匹配器 -->
<bean id="credentialsMatcher" class="com.xx.shiro.demo.credentials.DemoCredentialsMatcher">
<property name="retry" value="3"/>
<property name="hashAlgorithmName" value="md5"/>
<property name="hashIterations" value="2"/>
<property name="storedCredentialsHexEncoded" value="true"/>
</bean>
<!-- Realm实现 -->
<bean id="xxDemoRealm" class="com.xx.shiro.demo.realm.XxDemoRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"/>
<property name="userService" ref="userService" />
</bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="xxDemoRealm" />
</bean>
<!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) -->
<bean
class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod"
value="org.apache.shiro.SecurityUtils.setSecurityManager" />
<property name="arguments" ref="securityManager" />
</bean>
<!-- Shiro的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/login.spring" />
<property name="successUrl" value="/home.spring" />
<property name="filterChainDefinitions">
<value>
/login.sping = anon
/resources/** = anon
/** = user
</value>
</property>
</bean>
<!-- Shiro生命周期处理器 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
<!-- Enable shiro annotations -->
<bean
class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor" />
<bean
class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
<!-- 数据库连接 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver" />
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/shiro" />
<property name="user" value="root" />
<property name="password" value="admin" />
<property name="maxPoolSize" value="100" />
<property name="minPoolSize" value="10" />
<property name="initialPoolSize" value="10" />
<property name="maxIdleTime" value="1800" />
<property name="acquireIncrement" value="10" />
<property name="idleConnectionTestPeriod" value="600" />
<property name="acquireRetryAttempts" value="30" />
<property name="breakAfterAcquireFailure" value="false" />
<property name="preferredTestQuery" value="SELECT 1" />
</bean>
<!-- User service and dao -->
<bean id="userDao" class="com.xx.shiro.demo.dao.impl.UserDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate" />
</bean>
<bean id="userService" class="com.xx.shiro.demo.service.impl.UserServiceImpl">
<property name="userDao" ref="userDao" />
</bean>
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
</beans>
从配置文件可以看出我们要实现两个重要的模块,一个是CredentialsMatcher, 另外一个叫做Realm。CredentialsMatcher是用来验证密码的正确性,Realm则是来获取用户授权和认证的信息。
<!-- 凭证匹配器 -->
<bean id="credentialsMatcher" class="com.xx.shiro.demo.credentials.DemoCredentialsMatcher">
<property name="retry" value="3"/>
<property name="hashAlgorithmName" value="md5"/>
<property name="hashIterations" value="2"/>
<property name="storedCredentialsHexEncoded" value="true"/>
</bean>
<!-- Realm实现 -->
<bean id="xxDemoRealm" class="com.xx.shiro.demo.realm.XxDemoRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"/>
<property name="userService" ref="userService" />
</bean>
首先来看一下自定义的Realm,
它继承了抽象类AuthorizingRealm,同时AuthorizingRealm又继承了抽象类AuthenticatingRealm,所以自定义的Realm中要实现两个抽象方法,一个是获取认证信息,另外一个是获取授权信息。具体如下:
public class XxDemoRealm extends AuthorizingRealm {
UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
if (principals == null) {
throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
}
String username = (String)principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setRoles(userService.findRoles(username));
authorizationInfo.setStringPermissions(userService.findPermissions(username));
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
if (username == null) {
throw new AccountException("Null usernames are not allowed by this realm.");
}
String[] pwdSalt = userService.getUserPasswordAndSalt(username);
if (pwdSalt == null) {
throw new AccountException("No account found for user [" + username + "]");
}
return new SimpleAuthenticationInfo(username, pwdSalt[0].toCharArray(),
ByteSource.Util.bytes(username + pwdSalt[1]), getName());
}
public void setUserService(UserService userService) {
this.userService = userService;
}
}
这里我们分别返回SimpleAuthenticationInfo和SimpleAuthorizationInfo,SimpleAuthenticationInfo是由用户名,登录密码,密码盐以及realm名称组成,而SimpleAuthorizationInfo继承与SimpleAuthenticationInfo,同时又包含了角色和权限的集合。
在spring的配置中我们定义自定义realm的时候指定了凭证匹配器:
<bean id="xxDemoRealm" class="com.xx.shiro.demo.realm.XxDemoRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"/>
<property name="userService" ref="userService" />
</bean>
凭证匹配器就是用来验证用户输入的帐号密码和数据库中的密码是否匹配。
public class DemoCredentialsMatcher extends HashedCredentialsMatcher {
// 这里应该用cache来做, 不要用Map
private static Map<String, AtomicInteger> cache = new HashMap<String, AtomicInteger>();
private int retry = 5;
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String) token.getPrincipal();
AtomicInteger retryCount = cache.get(username);
if (retryCount == null) {
retryCount = new AtomicInteger(0);
cache.put(username, retryCount);
}
if (retryCount.incrementAndGet() > retry) {
throw new ExcessiveAttemptsException();
}
boolean matches = super.doCredentialsMatch(token, info);
if (matches) {
cache.remove(username);
}
return matches;
}
public void setRetry(int retry) {
this.retry = retry;
}
}
我们这里使用的是HashedCredentialsMatcher,另外添加了重试最大次数的机制,失败几次之后就不能登录了,解锁这里没有涉及到。
官方文档地址:http://shiro.apache.org/spring.html
未完待续。。。。。。