【Shiro許可權管理】22.Shiro之記住我
阿新 • • 發佈:2019-02-10
注:該系列所有測試均在之前建立的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中未相關服務請求配置過許可權限定:
filterChainDefinitions屬性在XML中直接配置頁面許可權。
可以看到一些頁面進行了許可權限定,後面跟著的就是攔截器的配置代號,常用的身份驗證相關
的許可權攔截器代號如下:
然後現在我們需要讓使用者僅需要通過“記住我”就可以訪問Web系統的“list.jsp”頁面:
我們需要在FilterChainDefinitionMapBuilder的buildFilterChainDefinitionMap方法的Map中新增“list.jsp”
頁面的許可權限定,將其限定為“user”使用者攔截器,即使用者通過身份驗證或“記住我”都可以訪問:
然後記得之前我們在登入Controller的login服務中,在登入驗證成功之後,會設定“記住我”為true:
同時回顧一下我們的授權Realm,裡面有模擬資料庫的四個測試賬號:
list.jsp頁面程式碼:
讓後進入首頁list.jsp:
此時分別點選admin.jsp和User.jsp的超連結是一個可進入一個不可進入
那麼這個時候關閉瀏覽器,重新開啟,這個時候訪問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、首先在登入頁面選中“記住我”然後登入成功;如果是瀏覽器登入,一般會把“記住我”的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中未相關服務請求配置過許可權限定:
而在FilterChainDefinitionMapBuilder中返回了含有頁面授權資訊的Map:<!-- 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"/>
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>