网络上大部分的博文是关于Apache shiro与Spring MVC的整合,以及使用教程,最近在做一个物流项目的时候使用的是Apache shiro与Spring进行整合,期间遇到了一些问题,花费了一些时间才得到解决,所以本文首先会从介绍shiro框架到理解shiro以及使用shiro框架几个角度进行描述如何正确的使用与理解该框架:
- 1、权限概述(正确理解认证、授权的基本概念)
- 2、常见的权限控制的方式(URL拦截的方式、方法注解的方式)
- 3、权限涉及到的数据表以及模型关系
- 4、Apache shiro框架
- 5、整合到项目中
1、权限概述(正确理解认证、授权的基本概念)
- 最初学习角色管理的相关内容的时候主要使用的RBAC权限控制管理模型,记得那会儿最先利用RBAC模型是在本科的时候完成一个人力资源管理系统,很是花费了一些时间。(不过该模型结构清晰,使用方便,便于连接查询,shiro的表设计与RBAC的涉及实质上也是大同小异的);
具体RBAC的学习初步可以参见百度百科的有关介绍:
- 有关概念介绍
- 在我们开发的一个大中小型系统中提供的功能有许多,并不是所有的用户都有权限可以进行访问以及操作,总是需要对于有些功能进行分组,并且进行访问限制;
- 认证:系统提供的用于识别用户身份的功能,通常是在登录功能(当前系统登录之后,是谁?如何识别到你是谁登录了系统);
- 授权:系统提供的赋予用户可以操作系统某些功能能力(当前登录系统的用户具有哪些权限功能);
- 单点登录(SSO):一处登录处处使用(可以联想我们在使用淘宝的时候,如果实在同一个终端,比如浏览器中登录了一次,之后我们在使用淘宝旗下的大部分产品的时候不需要进行第二次登录),利用Cookie实现,并且至少需要独立出一台服务器存储用户的登录信息,也就是说用户不是直接登录到系统,需要进行一台服务器的单独认证,如果在服务器中存在有相关用户的登录信息,就可以不用第二次登录;
- Remember me :记住我功能(利用的是Cookie实现);
2、常见的权限控制的方式(URL拦截的方式、方法注解的方式)
两种方式实际上都会遵从的后台流程图如下所示:
而所谓的URL拦截无非就是在中间的一层使用配置文件的配置URL拦截;
而方法注解也就是在对应的业务方法上就像注解标示标示,例如:
@RequiresPermissions(value="XXX.xxx")
3、权限涉及到的数据表以及模型关系
4、Apache shiro框架
提供的进行权限控制的方式:
- 1、URL拦截进行权限控制
- 2、方法注解权限控制
- 3、页面标签权限控制
- 4、代码方式权限控制(了解)
- 下图来在shiro框架的官方手册:
- 下图主要是shiro框架的运行流程,来自shiro框架的官方手册:
- Application Code:应用程序代码,由开发人员负责(也就是当前的项目代码部分)
- Subject:主体,当前用户,由框架提供的
- SecurityManager:安全管理器,由框架提供的,整个shiro框架最核心的组件。
- Realm:安全数据桥,类似于项目中的DAO,访问安全数据的,框架提供,开发人员也可自己编写
5、整合到项目中
- 导入jar包(shiro的jar有很多,针对不同的项目导入不同的jar包,但是为了防止第一次学习的时候出错,所有使用的是shiro-all-1.2.jar版本的jar包);
- 配置以及代码详细
步骤一: 在web.xml中配置一个过滤器,是由spring提供的,用于整合shiro:
- web.xml文件(一定要注意配置shiro框架以及Spring,Struts之间的顺序问题,否则报错!)
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID"
version="2.5">
<display-name>XXX系统</display-name>
<!-- 中文乱码问题解决过滤器 -->
<filter>
<filter-name>characterFilter</filter-name>
<filter-class>
org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- spring提供的解决hibernate延迟加载问题的过滤器 -->
<filter>
<filter-name>openSessionInView</filter-name>
<filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>openSessionInView</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- spring配置文件位置 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!-- spring核心监听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Spring配置文件结束 -->
<!-- 配置由Spring提供的过滤器,用于整合shiro框架 -->
<!-- 在项目启动的过程中,当前过滤器会从Spring工厂中提取同名对象 -->
<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>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
<!-- struts核心控制器 -->
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
<!-- 欢迎页面 -->
<welcome-file-list>
<welcome-file>login.jsp</welcome-file>
</welcome-file-list>
</web-app>
步骤二: 在applicationContext.xml中配置bean,ID必须为shiroFilter:
- applicationContext.xml文件配置
- shiro 框架由于大量的使用了代理模式,所以在使用的过程中如果配置不当,可能会出现问题,另外在使用注解开发时候尽量的使用Spring的注解,不要使用JDK自带的原生注解,减少出错的几率
<!-- 配置shiro的过滤器 -->
<!-- 当前对象用于创建shiro框架需要的过滤器对象 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- 注入安全管理器 -->
<property name="securityManager" ref="securityManager"></property>
<!-- 注入系统的登录访问路径 -->
<!-- 跳转到登录页面 -->
<property name="loginUrl" value="/login.jsp"></property>
<!-- 成功页面 -->
<property name="successUrl" value="/index.jsp"></property>
<!-- 权限不足的错误提示页面 -->
<property name="unauthorizedUrl" value="/500.html"></property>
<!-- 基于URL拦截权限控制 -->
<property name="filters">
<map>
<entry key="authc">
<bean class="org.apache.shiro.web.filter.authc.PassThruAuthenticationFilter"/>
</entry>
</map>
</property>
<!--
URL路径自上而下进行匹配
-->
<!--
anon过滤器处理原则 :随便访问
authc需要进行权限认证
-->
<property name="filterChainDefinitions">
<value>
/css/** = anon
/easyuidemo/** = anon
/images/** = anon
/js/** = anon
/json/** = anon
/user/** = anon
/validatecode.jsp* = anon
/login.jsp* = anon
/user/userAction_login.action = anon
/* = authc
</value>
</property>
</bean>
<bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">
</bean>
<!-- 定义安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- 注入realm -->
<property name="realm" ref="XXXRealm"></property>
<!-- 注入缓存管理器 -->
<property name="cacheManager" ref="cacheManager"></property>
</bean>
<!-- 注册缓存管理器 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml"></property>
</bean>
<bean id="lifecycleBeanPostProcessor"
class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- 自定义Realm -->
<bean id="XXXRealm" class="com.online.XXX.shiro.XXXRealm">
</bean>
<aop:aspectj-autoproxy proxy-target-class="true"/>
<!-- 使用shiro的注解需要的配置代码 -->
<!-- 开启shiro自动代理 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor">
<!-- 指定强制使用cglib为action创建代理对象 -->
<property name="proxyTargetClass" value="true"></property>
</bean>
<aop:config proxy-target-class="true"></aop:config>
<!-- 配置切面类 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"></property>
</bean>
步骤三: 登录系统使用shrio框架管理,修改Action中login方法:
/**
* 登录系统,使用shiro框架操作
* @return
*/
public String login() {
//从session中获取自动生成的验证码
String key = (String) ActionContext.getContext().getSession().get("key");
//shiro的基本认证方法
if (StringUtils.isNotBlank(key) && checkCode.equals(key)) {
//使用shiro方式进行认证
String username = model.getUsername();
String password = model.getPassword();
password = MD5Utils.md5(password);
//主体,当前状态为没有认证的状态“未认证”
Subject subject = SecurityUtils.getSubject();
//登录认证令牌(用户名密码令牌)
AuthenticationToken token = new UsernamePasswordToken(username,password);
//登录方法(认证是否通过)
//使用subject调用securityManager,安全管理器调用Realm
try {
//利用异常操作
//需要开始调用到Realm中
System.out.println("========================================");
System.out.println("1、进入认证方法");
subject.login(token);
user = (User) subject.getPrincipal();
} catch (UnknownAccountException e) {
this.addActionError(this.getText("loginError"));
return LOGIN;
}
ActionContext.getContext().getSession().put("loginUser", user);
return "home";
} else {
//验证码错误
this.addActionError(this.getText("checkCodeError"));
return LOGIN;
}
}
步骤四: 开发属于自己的realm类:
import java.util.List;
import javax.annotation.Resource;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
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 org.springframework.stereotype.Component;
import com.online.XXX.dao.IFunctionDao;
import com.online.XXX.dao.IUserDao;
import com.online.XXX.domain.Function;
import com.online.XXX.domain.User;
/**
* 自定义Realm
*
*/
@Component("XXXRealm")
public class XXXRealm extends AuthorizingRealm {
// 注入DAO
@Resource(name="userDao")
private IUserDao userDao;
@Resource(name="functionDao")
private IFunctionDao functionDao;
// 认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authenticationToken) throws AuthenticationException {
//返回空当前账号不存在
//toke强转
UsernamePasswordToken usernamePasswordToken =
(UsernamePasswordToken) authenticationToken;
String username = usernamePasswordToken.getUsername();
//根据用户名查询密码,有安全管理器负责对比查询出的数据库中的密码和页面输入的密码是否一致
User user = userDao.findUserByUsername(username);
if (user == null) {
return null;
}
String passwordFromDB = user.getPassword();
//最后的比对需要交给安全管理器
//三个参数进行初步的简单认证信息对象的包装
AuthenticationInfo info =
new SimpleAuthenticationInfo(user,
passwordFromDB, //由安全管理器进行包装运行
this.getClass().getSimpleName());
return info;
}
// 授权方法
// 执行的时期
/**
* 在访问需要控制的时候需要权限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principalCollection) {
//根据当前登录用户,查询用户的角色,根据角色对应获得的权限添加到信息对象中
//程序任何位置都可以拿到user对象
//方法一:
User user = (User) principalCollection.getPrimaryPrincipal();
//方法二:
// Subject subject = SecurityUtils.getSubject();
// User _user = (User) subject.getPrincipal();
// System.out.println("subject"+_user.getUsername());
//方法三:从context中获取XXXContext中获取
//所有的过程都是动态从数据库中取出来
//为所有的用户授予staff权限(模拟)
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
List<Function> list = null;
//根据当前登录用户,查询用户角色,返回用户权限,将权限添加到当前的权限信息中
//如果是超级管理员应该赋予全部权限的操作
if (user.getUsername().equals("admin")) {
//如果是超级管理员,授予所有权限
list = functionDao.findAll();
} else {
//普通用户,根据用户查询对应的权限
list = functionDao.findFunctionByUserId(user.getId());
}
for (Function function : list) {
//权限关键字
String code = function.getCode();
info.addStringPermission(code);
}
return info;
}
}
步骤五: shiro框架基于注解的开发:
在上面的applicationContext.xml的配置文件中已经代开了shiro的注解开发代理
<!-- 使用shiro的注解需要的配置代码 -->
<!-- 开启shiro自动代理 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor">
<!-- 指定强制使用cglib为action创建代理对象 -->
<property name="proxyTargetClass" value="true"></property>
</bean>
<aop:config proxy-target-class="true"></aop:config>
以上的配置以及代码已经实现了权限管理以及控制功能,并且实现了不同的角色登>录系统之后不同的功能列表的显示;
补充:缓存机制:使用的ehcache缓存机制,需要使用的是ehcache的jar包,以及配置一个ehcache.xml的缓存文件,如下:
<ehcache
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!-- 缓存存储的硬盘位置 -->
<diskStore path="/home/ubuntu/..."/>
<!--
1、maxElementsInMemory:最大缓冲量
2、eternal:物理内存有Java虚拟机进行定时清理
3、timeToIdleSeconds:最大空闲时间
4、timeToLiveSeconds:最大存货时间
5、maxElementsOnDisk:最大溢出大磁盘上
6、diskPersistent:服务器重启之后是否有效
7、memoryStoreEvictionPolicy:换出策略
-->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
maxElementsOnDisk="10000000"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
/>
</ehcache>
一下部分单独最为单纯的demo研究使用,一般情况下这样使用不太常见
步骤六:在Action中对应的需要权限管理的方法上@RequestMapping(“XXX”)的注解进行注解标示;
@RequiresPermissions(value="region")
public String pageQuery() throws IOException {
//业务代码略
return NONE;
}
步骤七:在XXXRealm中对于该方法进行授权与否
info.addStringPermission(code);