原理解釋
shiro對cookie做了什么?
其實你設置了這個rememberMe之后shiro還是有做一點事情的,它會生成一個cookie值叫 rememberMe 並保存在你的瀏覽器里面,而且這個參數會隨着你調用 subject.logout() 會被自動清除。這個參數的值是一串很長的Base64加密過的字符串,大概長這樣
名稱: rememberMe 內容: 6gYvaCGZaDXt1c0xwriXj/Uvz6g8OMT3VSaAK4WL0Fvqvkcm0nf3CfTwkWWTT4EjeSS/EoQjRfCPv4WKUXezQDvoNwVgFMtsLIeYMAfTd17ey5BrZQMxW+xU1lBSDoEM1yOy/i11ENh6eXjmYeQFv0yGbhchGdJWzk5W3MxJjv2SljlW4dkGxOSsol3mucoShzmcQ4VqiDjTcbVfZ7mxSHF/0M1JnXRphi8meDaIm9IwM4Hilgjmai+yzdVHFVDDHv/vsU/fZmjb+2tJnBiZ+jrDhl2Elt4qBDKxUKT05cDtXaUZWYQmP1bet2EqTfE8eiofa1+FO3iSTJmEocRLDLPWKSJ26bUWA8wUl/QdpH07Ymq1W0ho8EIdFhOsELxM66oMcj7a/8LVzypJXAXZdMFaNe8cBSN2dXpv4PwiktCs3J9P9vP4XrmYees5x27UmXNqYFk86xQhRjFdJsw5A9ctDKXzPYvJmWFouo3qT5hugX0uxWALCfWg8MHJnG9w7QgVKM8oy3Xy4Ut8lSvYlA==
這串字符串其實是對你登陸后的 Principal 進行了序列化后再Base64的結果。Principal 是 shiro 的一個概念,表示一個唯一的字符串能表示你這個用戶的,如果你按照最簡單的用戶名密碼登陸的方式,並且使用的是 SimpleAuthenticationInfo 對象,那么這個 Principal 其實就是一個字符串,就是你的用戶名 username
所以這串東西解密出來就是你的username
shiro覺得rememberMe不安全
shiro覺得不能把rememberMe等同於已經登陸了,這樣不安全。所以shiro 覺得就算 rememberMe = true 也不能算是 authc 的而是 user 級別的。
我們一般設置路徑攔截是這樣設置的
/** = authc
這樣就保證了所有路徑都需要登陸才能訪問。就算你是 rememberMe=true也不能訪問,官方說你如果設置成攔截級別為user就能訪問,比如
/** = user
這樣就可以訪問了,但是官方建議不敏感的部分用user,敏感的部分還是要讓用戶再登陸一次,就像你上淘寶網就算不登陸,只要上一次有登陸過,你依然可以直接看我的淘寶那個頁面,但是點擊 我的寶貝的時候就又要讓你登陸了。
但是!我們的確有很多時候是 需要記住用戶就相當於用戶登錄了!
設置成user這個方案還有一個問題,就是我們實際項目中在登陸后有做了很多設置用戶上下文的工作,比如設置session等,如果我們只是設置攔截級別為user,那么再次進入的時候雖然可以訪問,但是session是空的,我們的頁面必然異常頻出。
解決方案
前提條件
采用這個解決方案的前提是,你必須自己先實現一個realm,不過這個我相信大家都會實現的,畢竟默認的不是jdbcRealm ,真正的項目都是要查數據庫才能確定用戶是否登錄的。那么我就假定大家的項目中都有那么一個負責驗證登錄的 JdbcRealm, 並且是采用用戶名密碼認證的,在 doGetAuthenticationInfo 方法里面是采用如下的方法來做認證
... info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName());
這個前提條件保證你的principal是username,相信大部分人根據教程做shiro的時候都采用了這種方式
STEP1 復寫 FormAuthenticationFilter 的 isAccessAllowed 方法
做一個新類繼承FormAuthenticationFilter ,並復寫 isAccessAllowed 方法
package com.yqr.jxc.shiro; import javax.annotation.Resource; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; import com.yqr.jxc.service.global.GlobalUserService; public class RememberAuthenticationFilter extends FormAuthenticationFilter { @Resource(name="globalUserService") private GlobalUserService globalUserService; /** * 這個方法決定了是否能讓用戶登錄 */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { Subject subject = getSubject(request, response); //如果 isAuthenticated 為 false 證明不是登錄過的,同時 isRememberd 為true 證明是沒登陸直接通過記住我功能進來的 if(!subject.isAuthenticated() && subject.isRemembered()){ //獲取session看看是不是空的 Session session = subject.getSession(true); //隨便拿session的一個屬性來看session當前是否是空的,我用userId,你們的項目可以自行發揮 if(session.getAttribute("userId") == null){ //如果是空的才初始化,否則每次都要初始化,項目得慢死 //這邊根據前面的前提假設,拿到的是username String username = subject.getPrincipal().toString(); //在這個方法里面做初始化用戶上下文的事情,比如通過查詢數據庫來設置session值,你們自己發揮 globalUserService.initUserContext(username, subject); } } //這個方法本來只返回 subject.isAuthenticated() 現在我們加上 subject.isRemembered() 讓它同時也兼容remember這種情況 return subject.isAuthenticated() || subject.isRemembered(); } }
STEP2 設置使用這個新的 AuthenticationFilter (認證過濾器)
如果你用的是spring那么
<!-- 整合了rememberMe功能的filter --> <bean id="rememberAuthFilter" class="com.yqr.jxc.shiro.RememberAuthenticationFilter" ></bean> <!--將之前的 /** = authc 替換成 rememberAuthFilter ... /** = rememberAuthFilter ...
如果你用的是 ini 文件,那么
rememberAuthFilter=com.yqr.jxc.shiro.RememberAuthenticationFilter #將之前的 /** = authc 替換成 rememberAuthFilter ... /** = rememberAuthFilter
然后重啟項目我們來測試一下,先登錄一次系統,然后直接關掉瀏覽器,然后打開瀏覽器直接輸入系統某個頁面的地址,發現可以直接進去了,session什么的也設置好了
看起來很美?但是!
忙活了半天,最后我還是決定在我的系統中撤下了這個功能。為什么呢?因為這個功能有個致命的安全缺陷就是隨便誰把這個cookie值拿到別的瀏覽器都可以登錄。就算你用再牛逼的加密,或者是這個cookie值根據瀏覽器的各個別的屬性來達到僅供這個瀏覽器使用,但是對於黑客來說,只要你是通過表單把東西發送出去,這整個表單都是可以偽造的。就算是增加了過期時間,在這段時間之內還是有被偽造的風險,我目前沒有想到什么好的解決方案。
唯一能想到的就是對於使用場景的選擇,在嚴格的業務系統中不能使用記住我這個功能,在非嚴格的系統中,比如不敏感的系統,像看看流量看看微博之類的,還是可以使用以上的方式來解決rememberMe的問題的。
所以,請謹慎選擇是否要將 rememberMe 功能范圍擴大化!