網絡上大部分的博文是關於Apache shiro與Spring MVC的整合,以及使用教程,最近在做一個物流項目的時候使用的是Apache shiro與Spring進行整合,期間遇到了一些問題,花費了一些時間纔得到解決,所以本文首先會從介紹shiro框架到理解shiro以及使用shiro框架幾個角度進行描述如何正確的使用與理解該框架:
- 1、權限概述(正確理解認證、授權的基本概念)
- 2、常見的權限控制的方式(URL攔截的方式、方法註解的方式)
- 3、權限涉及到的數據表以及模型關係
- 4、Apache shiro框架
- 5、整合到項目中
1、權限概述(正確理解認證、授權的基本概念)
- 最初學習角色管理的相關內容的時候主要使用的RBAC權限控制管理模型,記得那會兒最先利用RBAC模型是在本科的時候完成一個人力資源管理系統,很是花費了一些時間。(不過該模型結構清晰,使用方便,便於連接查詢,shiro的表設計與RBAC的涉及實質上也是大同小異的);
具體RBAC的學習初步可以參見百度百科的有關介紹:
http://baike.baidu.com/link?url=JxUkSy4blHjN41_u5xjQPiRLTBn7M-daXD9UGpe8z_tM49z2BTbBYpe80YqDeHTnVFvN5LQFyfqzBt7aQ-AmGa
有關概念介紹
在我們開發的一個大中小型系統中提供的功能有許多,並不是所有的用戶都有權限可以進行訪問以及操作,總是需要對於有些功能進行分組,並且進行訪問限制;
認證:系統提供的用於識別用戶身份的功能,通常是在登錄功能(當前系統登錄之後,是誰?如何識別到你是誰登錄了系統);
- 授權:系統提供的賦予用戶可以操作系統某些功能能力(當前登錄系統的用戶具有哪些權限功能);
- 單點登錄(SSO):一處登錄處處使用(可以聯想我們在使用淘寶的時候,如果實在同一個終端,比如瀏覽器中登錄了一次,之後我們在使用淘寶旗下的大部分產品的時候不需要進行第二次登錄),利用Cookie實現,並且至少需要獨立出一臺服務器存儲用戶的登錄信息,也就是說用戶不是直接登錄到系統,需要進行一臺服務器的單獨認證,如果在服務器中存在有相關用戶的登錄信息,就可以不用第二次登錄;
- Remember me :記住我功能(利用的是Cookie實現);
2、常見的權限控制的方式(URL攔截的方式、方法註解的方式)
兩種方式實際上都會遵從的後臺流程圖如下所示:
而所謂的URL攔截無非就是在中間的一層使用配置文件的配置URL攔截;
而方法註解也就是在對應的業務方法上就像註解標示標示,例如:
@RequiresPermissions(value="XXX.xxx")
3、權限涉及到的數據表以及模型關係
4、Apache shiro框架
官網地址:http://shiro.apache.org
提供的進行權限控制的方式:
- 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);