1. 程式人生 > >【Shiro許可權管理】22.Shiro之記住我

【Shiro許可權管理】22.Shiro之記住我

注:該系列所有測試均在之前建立的Shiro3的Web工程的基礎上。
在我們登入一些網站的時候,在登入輸入框的下側一般都會有一個“記住我”的勾選框,選擇之後,我們
下次進入網站的時候就會自動進行登入操作,無需我們再次輸入密碼。而在訪問一些敏感資訊的時候,還是需要
進行登入操作的。

有關“記住我”的實現原理如下:
1、首先在登入頁面選中“記住我”然後登入成功;如果是瀏覽器登入,一般會把“記住我”的Cookie寫到客戶端並儲存
下來。
2、關閉瀏覽器再重新開啟,會發現瀏覽器還是記住你的。
3、訪問一般的網頁伺服器端還是知道你是誰,且能正常訪問。
4、但是比如我們訪問淘寶時,如果要檢視我的訂單或進行支付時,此時還是需要再進行身份認證的,
以確保當前使用者還是你。

Shiro出了提供了登入時的“認證”操作,同時也提供了“記住我”的操作實現,那麼兩個有何區別呢?
(1)subject.isAuthenticated()

表示使用者進行了身份驗證登入的,即使用Subject.login進行了登入;
(2)subject.isRemembered()
表示使用者是通過“記住我”登入的,此時可能並不是真正的你(如其他人使用你的電腦,或者你的cookie被竊取)
在訪問的;
兩者二選一,即subject.isAuthenticated()==true,則subject.isRemembered()==false;反之一樣。

相關建議:
(1)訪問一般網頁
如個人在主頁之類的,我們使用user攔截器即可,user攔截器只要使用者登入 (isRemembered()||isAuthenticated())
過即可訪問成功。
(2)訪問特殊網頁

如提交訂單頁面,我們使用authc攔截器即可,authc攔截器會判斷使用者是否是通過
Subject.login(isAuthenticated()==true)登入的,如果是才放行,否則會跳轉到登入頁面叫你重新登入。

下面演示一下使用Shiro實現“記住我”的效果。記得在之前的Web測試工程中,我們在Spring的application.xml
配置檔案中的shiroFilter中未相關服務請求配置過許可權限定:
<!-- 6. 配置 ShiroFilter. 
6.1 id 必須和 web.xml 檔案中配置的 DelegatingFilterProxy 的 <filter-name> 一致.
   若不一致, 則會丟擲: NoSuchBeanDefinitionException. 因為 Shiro 會來 IOC 容器中查詢和 <filter-name> 名字對應的 filter bean.
-->     
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login.jsp"/>
    <property name="successUrl" value="/list.jsp"/>
    <property name="unauthorizedUrl" value="/index.jsp"/>

    <property name ="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property>
</bean>

<bean id="filterChainDefinitionMap"
    factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"/>

<!-- 配置一個bean,該bean實際上是一個Map,通過例項工廠方法的方式 -->
<bean id="filterChainDefinitionMapBuilder"
    	class="com.test.shiro.factory.FilterChainDefinitionMapBuilder"/>
而在FilterChainDefinitionMapBuilder中返回了含有頁面授權資訊的Map:
package com.test.shiro.factory;
import java.util.LinkedHashMap;
public class FilterChainDefinitionMapBuilder {
    public LinkedHashMap<String,String> buildFilterChainDefinitionMap(){
    	LinkedHashMap<String,String> map = new LinkedHashMap<>();
    	/*配置哪些頁面需要受保護. 
    	以及訪問這些頁面需要的許可權. 
    	1). anon 可以被匿名訪問
    	2). authc 必須認證(即登入)後才可能訪問的頁面. 
    	3). logout 登出
    	4). roles 角色過濾器*/
    	map.put("/login.jsp","anon");
    	map.put("/userAuth/login","anon");
    	map.put("/userAuth/logout","logout");
    	map.put("/User.jsp","authc,roles[user]");//需要認證並且有user角色
    	map.put("/admin.jsp","authc,roles[admin]");//需要認證並且有admin角色
    	map.put("/**","authc");
    	return map;
    }
}
注:這裡使用filterChainDefinitionMapBuilder例項工廠從Java類中獲取頁面許可權限定的Map,當然也可以使用
filterChainDefinitions屬性在XML中直接配置頁面許可權。

可以看到一些頁面進行了許可權限定,後面跟著的就是攔截器的配置代號,常用的身份驗證相關
的許可權攔截器代號如下:


然後現在我們需要讓使用者僅需要通過“記住我”就可以訪問Web系統的“list.jsp”頁面:


我們需要在FilterChainDefinitionMapBuilder的buildFilterChainDefinitionMap方法的Map中新增“list.jsp”
頁面的許可權限定,將其限定為“user”使用者攔截器,即使用者通過身份驗證或“記住我”都可以訪問:
package com.test.shiro.factory;
import java.util.LinkedHashMap;
public class FilterChainDefinitionMapBuilder {
    public LinkedHashMap<String,String> buildFilterChainDefinitionMap(){
    	LinkedHashMap<String,String> map = new LinkedHashMap<>();
    	/*配置哪些頁面需要受保護. 
    	以及訪問這些頁面需要的許可權. 
    	1). anon 可以被匿名訪問
    	2). authc 必須認證(即登入)後才可能訪問的頁面. 
    	3). logout 登出
    	4). roles 角色過濾器*/
    	map.put("/login.jsp","anon");
    	map.put("/userAuth/login","anon");
    	map.put("/userAuth/logout","logout");
    	map.put("/User.jsp","authc,roles[user]");//需要認證並且有user角色
    	map.put("/admin.jsp","authc,roles[admin]");//需要認證並且有admin角色
    	
    	map.put("/list.jsp","user");//認證過或“記住我”都可訪問list.jsp
    	
    	map.put("/**","authc");
    	return map;
    }
}

然後記得之前我們在登入Controller的login服務中,在登入驗證成功之後,會設定“記住我”為true:
@RequestMapping("login")
public String login(String username,String password){
	//獲取當前的Subject
    Subject currentUser = SecurityUtils.getSubject();
    //測試當前使用者是否已經被認證(即是否已經登入)
    if (!currentUser.isAuthenticated()) {
    	//將使用者名稱與密碼封裝為UsernamePasswordToken物件
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        token.setRememberMe(true);//記錄使用者
        try {
            currentUser.login(token);//呼叫Subject的login方法執行登入
        } catch (AuthenticationException e) {//所有認證時異常的父類
            System.out.println("登入失敗:"+e.getMessage());
        } 
    }
	return "redirect:/list.jsp";
}

同時回顧一下我們的授權Realm,裡面有模擬資料庫的四個測試賬號:
package com.test.shiro.realms;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import com.test.shiro.po.User;
public class ShiroRealm extends AuthorizingRealm{
	
	private static Map<String,User> userMap = new HashMap<String,User>();
	static{
		//使用Map模擬資料庫獲取User表資訊
		userMap.put("administrator", new User("administrator","5703a57069fce1f17882d283132229e0",false));//密碼明文:aaa123
		userMap.put("jack", new User("jack","43e66616f8730a08e4bf1663301327b1",false));//密碼明文:aaa123
		userMap.put("tom", new User("tom","3abee8ced79e15b9b7ddd43b95f02f95",false));//密碼明文:bbb321
		userMap.put("jean", new User("jean","1a287acb0d87baded1e79f4b4c0d4f3e",true));//密碼明文:ccc213
	}

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(
			AuthenticationToken token) throws AuthenticationException {
		System.out.println("[ShiroRealm]");
		//1.把AuthenticationToken轉換為UsernamePasswordToken
		UsernamePasswordToken userToken = (UsernamePasswordToken) token;
		
		//2.從UsernamePasswordToken中獲取username
		String username = userToken.getUsername();
		
		//3.呼叫資料庫的方法,從資料庫中查詢Username對應的使用者記錄
		System.out.println("從資料看中獲取UserName為"+username+"所對應的資訊。");
		//Map模擬資料庫取資料
		User u = userMap.get(username);
		
		//4.若使用者不行存在,可以丟擲UnknownAccountException
		if(u==null){
			throw new UnknownAccountException("使用者不存在");
		}
		
		//5.若使用者被鎖定,可以丟擲LockedAccountException
		if(u.isLocked()){
			throw new LockedAccountException("使用者被鎖定");
		}
		
		//7.根據使用者的情況,來構建AuthenticationInfo物件,通常使用的實現類為SimpleAuthenticationInfo
		//以下資訊是從資料庫中獲取的
		//1)principal:認證的實體資訊,可以是username,也可以是資料庫表對應的使用者的實體物件
		Object principal = u.getUsername();
		//2)credentials:密碼
		Object credentials = u.getPassword();
		//3)realmName:當前realm物件的name,呼叫父類的getName()方法即可
		String realmName = getName();
		//4)credentialsSalt鹽值
		ByteSource credentialsSalt = ByteSource.Util.bytes(principal);//使用賬號作為鹽值
		
		SimpleAuthenticationInfo info = null; //new SimpleAuthenticationInfo(principal,credentials,realmName);
		info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
		return info;
	}

	//給Shiro的授權驗證提供授權資訊
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(
			PrincipalCollection principals) {
		//1.從principals中獲取登入使用者的資訊
		Object principal = principals.getPrimaryPrincipal();
		
		//2.利用登入使用者的資訊獲取當前使用者的角色(有資料庫的話,從資料庫中查詢)
		Set<String> roles = new HashSet<String>();//放置使用者角色的set集合(不重複)
		roles.add("user");//放置所有使用者都有的普通使用者角色
		if("administrator".equals(principal)){
			roles.add("admin");//當賬號為administrator時,新增admin角色
		}
		
		//3.建立SimpleAuthorizationInfo,並設定其roles屬性
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
		
		//4.返回SimpleAuthorizationInfo物件
		return info;
	}
}

list.jsp頁面程式碼:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>首頁</title>
  </head>
  <body>
     登入成功!歡迎<shiro:principal/>訪問首頁O(∩_∩)O
   <a href="userAuth/logout">登出</a>

   <br/><br/>
   <a href="admin.jsp">Admin Page</a>
   
   <br/><br/>
   <a href="User.jsp">User Page</a>
   
  </body>
</html>
此時我們啟動Web系統,然後登入一個普通使用者“jack”(doGetAuthorizationInfo會自動分配“user”許可權):

讓後進入首頁list.jsp:

此時分別點選admin.jsp和User.jsp的超連結是一個可進入一個不可進入

(因為普通使用者只有user角色,沒有admin角色):



那麼這個時候關閉瀏覽器,重新開啟,這個時候訪問admin.jsp/User.jsp都訪問不了,會直接跳回登入頁:

而直接去訪問list.jsp,發現依然可以:


這就說明list.jsp/admin.jsp/user.jsp頁面訪問許可權過濾是有效的,特別是list.jsp,不管是登入認證,還是“記住我”都可以。

實際開發時會在登入頁面放置一個checkBox複選框,讓使用者勾選是否“記住我”,以此來判斷是否在後臺
呼叫“token.setRememberMe(true);”方法。

最後,我們可以給RememberMe設定一個生效時長(一個月/一年等),如何設定呢?
我們在登入時,會使用securityManager配置的實現類DefaultWebSecurityManager,在其中含有rememberMeManager物件
,其中含有一個cookie物件,在cookie物件中有一個名為maxAge的引數,代表了“記住我”的預設最大生效時間:

可以看到預設為31536000秒。
所以我們可以通過設定securityManager的rememberManager的cookie物件的maxAge引數,來設定rememberMe的
生效時間:
<!--1. 配置 SecurityManager-->     
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="cacheManager" ref="cacheManager"/>
    <property name="authenticator" ref="authenticator"/>
    <property name="realms">
        <list>
            <ref bean="shiroRealm"/>
            <ref bean="secordRealm"/>
        </list>
    </property>
    <!-- 設定rememeberMe的時常為30分鐘(1800秒) -->
    <property name="rememberMeManager.cookie.maxAge" value="1800"></property>
</bean>