shiro權限驗證框架
1.什麼是Shiro?
Shiro 是一個用 Java 語言實現的框架,通過一個簡單易用的 API 提供身份驗證和授權。使用 Shiro,您就能夠爲您的應用程序提供安全性而又無需從頭編寫所有代碼。
2.爲什麼要用Shiro?
shiro在大多數的企業級系統中,我們一般都是採用角色關聯資源,然後對用戶指定一些角色,這樣用戶就擁有了一些url,菜單等資源,登陸後頁面即可相應的一些功能,但是這種情況下存在這安全問題,因爲用戶頁面只是沒有顯示相應的功能菜單,若某個用戶知道url地址,直接去訪問,系統是沒法控制該用戶操作的,於是便迎來了權限驗證框架這一說。
3.Shiro能夠有哪些方式控制權限呢?
同樣shiro有兩種配置方式,xml和註解,當然xml相對而言不靈活,只能指定經過認證授權後可以訪問哪些頁面以及不能訪問哪些頁面,對於註解就靈活一些了,可以適應於各種情景,
方法上加上
@RequiresAuthentication (驗證用戶是否登錄)
@RequiresUser 驗證用戶是否被記憶,user有兩種含義:一種是成功登錄的(subject.isAuthenticated() 結果爲true);另外一種是被記憶的(subject.isRemembered()結果爲true)。
@RequiresGuest 驗證是否是一個guest的請求
@RequiresRoles 如果subject中有aRoleName角色纔可以訪問方法someMethod。如果沒有這個權限則會拋出異常AuthorizationException。
@RequiresPermissions 要求subject中必須同時含有file:read和write:aFile.txt的權限才能執行方法someMethod()。否則拋出異常AuthorizationException。
@RequiresUser 驗證用戶是否被記憶,user有兩種含義:一種是成功登錄的(subject.isAuthenticated() 結果爲true);另外一種是被記憶的(subject.isRemembered()結果爲true)。
@RequiresGuest 驗證是否是一個guest的請求
@RequiresRoles 如果subject中有aRoleName角色纔可以訪問方法someMethod。如果沒有這個權限則會拋出異常AuthorizationException。
@RequiresPermissions 要求subject中必須同時含有file:read和write:aFile.txt的權限才能執行方法someMethod()。否則拋出異常AuthorizationException。
4.說說怎樣使用Shiro?
先添加 spring-shiro.xml 配置
具體如下:
在applicationContext中引入該文件
<import resource="classpath*:/spring-shiro.xml" />
在springMVC中加入:
<!-- 開啓Shiro的註解,實現對Controller的方法級權限檢查(如@RequiresRoles,@RequiresPermissions),需
藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證 -->
<!-- 需要在 sprimg-MVC的配置文件中 -->
上述這樣就配置完畢了
具體如下:
<?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-3.2.xsd"
default-lazy-init="true">
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/shiro/turnlogin.do" />
<property name="successUrl" value="/shiro/success.do" />
<property name="unauthorizedUrl" value="/shiro/unauth.do" />
<property name="filterChainDefinitions">
<value>
/shiro/success = authc <!-- authc 表示需要認證才能訪問的頁面 -->
/shiro/success = authc, perms[/home] <!-- perms 表示需要該權限才能訪問的頁面 -->
</value>
</property>
</bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myShiroRealm"></property>
</bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
<bean id="myShiroRealm" class="com.hfmx.util.shiro.MyShiroRealm">
<!-- businessManager 用來實現用戶名密碼的查詢 -->
<!-- <property name="shiroService" ref="shiroService" /> -->
</bean>
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<!--登錄-->
<prop key="org.apache.shiro.authz.UnauthenticatedException">
redirect:/shiro/turnlogin.do
</prop>
<!--授權-->
<prop key="org.apache.shiro.authz.UnauthorizedException">
redirect:/shiro/turnlogin.do
</prop>
</props>
</property>
<property name="defaultErrorView" value="s/403" />
</bean>
</beans>
在applicationContext中引入該文件
<import resource="classpath*:/spring-shiro.xml" />
在springMVC中加入:
<!-- 開啓Shiro的註解,實現對Controller的方法級權限檢查(如@RequiresRoles,@RequiresPermissions),需
藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證 -->
<!-- 需要在 sprimg-MVC的配置文件中 -->
<bean id="controllerAdvisorAutoProxyCreator"
class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-
on="lifecycleBeanPostProcessor"/>
<beanid="controllerAuthorizationAttributeSourceAdvisor" class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
上述這樣就配置完畢了
我們要新建一個類MyShiroRealm ,繼承AuthorizingRealm
重寫doGetAuthorizationInfo() //獲取授權信息
以及doGetAuthenticationInfo()//獲取認證信息
這兩個方法
因爲公司系統 已經使用了一定時間了,用戶登錄這塊已經很完善就仍然使用原來的,所以這裏的doGetAuthenticationInfo方法 可以不做太多的處理,這裏也把應有的流程貼出來
@Override
public AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken at) throws AuthenticationException {
//1. 把AuthenticationToken 轉化爲 UsernamePasswordToken
UsernamePasswordToken token = (UsernamePasswordToken) at;
//2. 從UsernamePasswordToken 中獲取 username
String username = token.getUsername();
char[] password = token.getPassword();
//3. 調用數據庫的方法,從數據庫中查詢 username 對應的用戶記錄
//4. 若用戶不存在,則可以拋出 UnknownAccountException
//5. 根據用戶信息的情況,決定是否需要拋出其他的AuthenticationException 異常
//6. 根據用戶的情況,來構建 AuthenticationInfo 對象並返回
//不帶 鹽值的 加密 -- 可能存在密碼相同時 加密後的密文也是相同的
return new SimpleAuthenticationInfo(username, new String(password), getName());
//帶鹽值加密 -- 最安全
//ByteSource credentialsSalt = ByteSource.Util.bytes(user.getUsername());
//return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), credentialsSalt, getName());
}
@Override
public AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection pc) {
//1. 從PrincipalCollection 中獲取登錄用戶的信息
Object principal = pc.getPrimaryPrincipal();
//2. 利用登錄用戶的信息來獲取當前用戶的角色或權限(可能需要查詢數據庫)
//3. 創建SimpleAuthorizationInfo,並設置roles屬性
//4. 返回SimpleAuthorizationInfo對象
//通過從shiro session值中獲取信息
List<String> urllist = (List<String>) SecurityUtils.getSubject().getSession().getAttribute("url");
if(null!=urllist&&urllist.size()!=0){
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
for (String url : urllist) {
if(null!=url&&(!(url.trim()).equals(""))){
info.addStringPermission(url);
}
}
return info;
}
/*// 根據自己系統規則的需要編寫獲取授權信息,這裏只獲取了用戶對應角色的資源url信息
String username = (String) pc.fromRealm(getName()).iterator().next();
//指定url訪問 通過查找數據庫的方式進行
if (username != null) {
ArrayList<String> urls = shiroService.getUrlByName(username);
if(null!=urls&&urls.size()!=0){
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
for (String url : urls) {
if(null!=url&&(!(url.trim()).equals(""))){
info.addStringPermission(url);
}
}
return info;
}
}*/
return null;
}
其實註解驗證的方式原理很簡單,通過doGetAuthorizationInfo(PrincipalCollection pc)方法參數pc可以獲得到用戶的登錄名,通過登錄名來查找該用戶的url資源,然後將這些url資源 添加至SimpleAuthorizationInfo中(通過info.addStringPermission(url)方法),然後在controller層的方法上加上@RequiresPermissions ("url地址")
註解方式,注意這裏的url地址需要與真實的RequestMapping映射地址一致,用戶請求了該地址後系統就會自動匹配剛剛添加的SimpleAuthorizationInfo中是否含有該url地址,如果有則通過,反之拋出異常。
那麼PrincipalCollection pc 的參數是誰傳過來的呢?其實在登錄時成功後,就需要進行設置了,
if(!currentUser.isAuthenticated()){
UsernamePasswordToken token = new UsernamePasswordToken(_userName, password);
token.setRememberMe(true);
try{
currentUser.login(token); //添加用戶信息
}catch(AuthenticationException ac){
System.out.println("登錄異常:" + ac.getMessage());
this.writeJson(response, new AjaxMsg(false, "登錄異常:" + ac.getMessage()));
}
}
UsernamePasswordToken token = new UsernamePasswordToken(_userName, password);
token.setRememberMe(true);
try{
currentUser.login(token); //添加用戶信息
}catch(AuthenticationException ac){
System.out.println("登錄異常:" + ac.getMessage());
this.writeJson(response, new AjaxMsg(false, "登錄異常:" + ac.getMessage()));
}
}
5. 最後說一下,這幾天解決的問題,
doGetAuthorizationInfo()是獲取授權的方法,通常我們是通過用戶名查詢數據庫的方式獲取所有資源,然後包裝成SimpleAuthorizationInfo對象進行返回,顯然這種方式不太可取,每次認證都要查詢數據庫,肯定是不行的,這裏shiro提供了緩存技術可以查閱一下,由於該系統是支持集羣的,所以這種方式也不適合,而該系統中session交由redis進行管理,所以有個想法,把這些資源放進session,也就是redis,這樣獲取的時候能夠保證高效。最有趣的事來了,
先前測試案例中我是將 資源放到Httpsession中,然後在doGetAuthorizationInfo()方法中獲取,而這裏獲取的是org.apache.shiro.session.Session,需要通過轉化才能成爲httpsession,但是效果達到了,後來到正式使用的時候,發現獲取不到了。
找啊找.........突然發現可以直接把資源放到org.apache.shiro.session.Session中,這樣豈不是不用轉化了,於是登陸成功後 執行
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
session.setAttribute("url", list);
將url資源添加至org.apache.shiro.session.Session中,
然後在doGetAuthorizationInfo()是獲取授權的方法 直接 拿出來進行包裝,
//通過從shiro session值中獲取信息
List<String> urllist = (List<String>) SecurityUtils.getSubject().getSession().getAttribute("url");
if(null!=urllist&&urllist.size()!=0){
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
for (String url : urllist) {
if(null!=url&&(!(url.trim()).equals(""))){
info.addStringPermission(url);
}
}
return info;
}
問題解決了,先就記錄到這兒吧,其實還有進一步的優化。
補充:!!!針對上述 稱述有問題的地方特別指出
上述說的shiro是部署在集羣系統中的,那麼在登錄時候直接將url資源放置到org.apache.shiro.session.Session中是不合理的,因爲集羣下,shiro的session不依懶於任何容器,也不會自動像HttpSession一樣通過spring-session自動放置到redis中管理,於是集羣下這種方式就行不通了,那麼該怎麼辦呢?
其實 最初的時候已經 做了,那就是登陸成功後 直接
request.getSession().setAttribute("url", list); 將url資源也直接放置到httpsession中,這樣一切都變得簡單了,session週期也不用自己去管理了,
然後在doGetAuthorizationInfo()方法中獲取剛剛放進去的資源,也就是HttpSession的一個Attribute爲‘’url‘’的值
List<String> urllist = (List<String>)request.getSession().getAttribute("url");
即可獲得HttpSession
於是也就支持了集羣和非集羣下的使用!
總結一下:大致有三種方式 權限驗證時獲取 資源值
//1. 通過從shiro session 直接獲取登錄時存放的url資源 (非集羣 下 使用)
/* List<String> urllist = (List<String>) SecurityUtils.getSubject().getSession().getAttribute("url");
if(null!=urllist&&urllist.size()!=0){
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
for (String url : urllist) {
if(null!=url&&(!(url.trim()).equals(""))){
info.addStringPermission(url);
}
}
return info;
}*/
//2. 通過 request的session獲取登錄時存放的url資源 (集羣 非集羣 下 通用--效率高)
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
List<String> urllist = (List<String>)request.getSession().getAttribute("url");
if(null!=urllist&&urllist.size()!=0){
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
for (String url : urllist) {
if(null!=url&&(!(url.trim()).equals(""))){
info.addStringPermission(url);
System.err.println("添加資源"+url);
}
}
return info;
}
//3. 根據自己系統規則的需要編寫獲取授權信息,這裏只獲取了用戶對應角色的資源url信息 (集羣 非集羣 下 通用--效率低下)
/*String username = (String) pc.fromRealm(getName()).iterator().next();
//指定url訪問 通過查找數據庫的方式進行
if (username != null) {
ArrayList<String> urls = shiroService.getUrlByName(username);
if(null!=urls&&urls.size()!=0){
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
for (String url : urls) {
if(null!=url&&(!(url.trim()).equals(""))){
info.addStringPermission(url);
}
}
return info;
}
}*/
到此 最完美的解決方法也就是 (第二種)資源還是 放在 HttpSession 中 最合理!
--謝謝您的閱讀,不足地方請指出!