轉載請在頁首明顯處注明作者與出處
朱小傑 http://www.cnblogs.com/zhuxiaojie/p/7809767.html
一:說明
在網上都找不到相關的信息,還是翻了大半天shiro的源碼才找到答案。親試絕對可行,帶源碼分析
很多時候,開發的項目不僅僅是一個基於瀏覽器的項目,還可能是基於app的項目,基於小程序的項目,而這些項目都是無狀態的。而普通web項目中,一個web項目的會話是由session保持的,而session又是由瀏覽器攜帶的cookie來驗證身份的,可以這么說,一個會話就是依賴於cookie,但是app與小程序是沒有cookie維持的。
一般的作法會在header中帶有一個token,或者是在參數中,后台根據這個token來進行校驗這個用戶的身份,但是這個時候,servlet中的session就無法保存,我們在這個時候,就要實現自己的會話創建,普通的作法就是重寫session與request的接口,然后在過濾器在把它替換成自己的request,所以得到的session也是自己的session,然后根據token來創建和維護會話。
但在shiro中會怎么做呢?
二:shiro介紹
shiro是一個權限驗證框架,它比spring security的功能要少一些,但是我卻更喜歡shiro,因為spring security封裝的太死了,如果要重寫一些功能,特別的麻煩,而shiro中使用了大量的策略模式,使得開發人員可以很好的替換成自己的策略,靈活性更加強,可以定義自己的過濾器來實現自己需要的一些功能。
shiro中的權限操作是委托給securityManager的,而securityManager管理session又是委托給sessionManager的,在開發web項目中,我們一般會使用
org.apache.shiro.web.mgt.DefaultWebSecurityManager
來創建securityManager,我們看一下這個DefaultWebSecurityManager默認是使用的哪個session管理器,它的構造方法如下
public DefaultWebSecurityManager() { super(); ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator()); this.sessionMode = HTTP_SESSION_MODE; setSubjectFactory(new DefaultWebSubjectFactory()); setRememberMeManager(new CookieRememberMeManager()); setSessionManager(new ServletContainerSessionManager());//這里可以看到是使用的servlet的默認管理器 }
可以看到,如果構造一個DefaultWebSecurityManager,它使用的是
org.apache.shiro.web.session.mgt.ServletContainerSessionManager
它是依賴於瀏覽器的cookie來維護session的,那肯定不能實現無狀態的會話。
不過shiro還提供了另一個基於web的session管理器,它就是
org.apache.shiro.web.session.mgt.DefaultWebSessionManager
如果我們想實現自己的一套session管理器,都會選擇去繼承它來重寫
小提示:筆者1.4.0的版本,當前是最新版本,無法直接在security中設置sessionManager的時候,直接new一個DefaultWebSessionManager,如下:
@Bean public SecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setSessionManager(new DefaultWebSessionManager()); securityManager.setRealm(new WebRealm()); return securityManager; }
如果直接設置為DefaultWebSessionManager,那么在有http請求的時候會報錯,提示找不到SecurityManager,解決辦法是寫一個類來繼承它,哪怕繼承后什么都不做,都可以解決這個問題
三:重寫shiro的sessionManager
上面說到我們要重寫DefaultWebSessionManager,那我們要怎么重寫呢?
import org.apache.shiro.session.mgt.SessionKey; import org.apache.shiro.web.servlet.ShiroHttpServletRequest; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.apache.shiro.web.util.WebUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.Serializable; import java.util.UUID; /** * @author zxj<br> * 時間 2017/11/8 15:55 * 說明 ... */ public class StatelessSessionManager extends DefaultWebSessionManager { /** * 這個是服務端要返回給客戶端, */ public final static String TOKEN_NAME = "TOKEN"; /** * 這個是客戶端請求給服務端帶的header */ public final static String HEADER_TOKEN_NAME = "token"; public final static Logger LOG = LoggerFactory.getLogger(StatelessSessionManager.class); @Override public Serializable getSessionId(SessionKey key) { Serializable sessionId = key.getSessionId(); if(sessionId == null){ HttpServletRequest request = WebUtils.getHttpRequest(key); HttpServletResponse response = WebUtils.getHttpResponse(key); sessionId = this.getSessionId(request,response); } HttpServletRequest request = WebUtils.getHttpRequest(key); request.setAttribute(TOKEN_NAME,sessionId.toString()); return sessionId; } @Override protected Serializable getSessionId(ServletRequest servletRequest, ServletResponse servletResponse) { HttpServletRequest request = (HttpServletRequest) servletRequest; String token = request.getHeader(HEADER_TOKEN_NAME); if(token == null){ token = UUID.randomUUID().toString(); } //這段代碼還沒有去查看其作用,但是這是其父類中所擁有的代碼,重寫完后我復制了過來...開始 request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled()); //這段代碼還沒有去查看其作用,但是這是其父類中所擁有的代碼,重寫完后我復制了過來...結束 return token; } }
三:源碼分析
上面就是完整的重寫的代碼,我們一個一個方法來看
3.1:第一個方法
public Serializable getSessionId(SessionKey key)
這個方法的覆蓋和它的父類其實沒有太大的區別,邏輯上面都是通過一個sessionKey來獲取一個sessionId,但是重寫的部分多了一個把獲取到的token設置到request的部分,這是因為app調用登陸接口的時候,是沒有token的,登陸成功后,產生了token,我們把它放到request中,返回結果給客戶端的時候,把它從request中取出來,並且傳遞給客戶端,客戶端每次帶着這個token過來,就相當於是瀏覽器的cookie的作用,也就能維護會話了
這里不得不說一下sessionId和sessionKey的區別了,本人也是因為這個東西坑了好久,從字面上面看,sessionKey是一個對象,而sessionId是一個serializable對象,實際上從我們返回的token可以知道,它就是一個String。
sessionKey是在sessionStore中,對應存儲的key值,而sessionId則就是請求帶來的token,或者是瀏覽器請求的cookie中的jsessionid。
我們要想象一個,他們有什么關系呢?我們通過sessionId,應該得到sessionKey,然后通過sessionKey,能在sessionStore中找到session,那我們就把sessionId與sessionKey相等吧,這樣就不用找對應關系了,因為sessionId就等於sessionKey的話,那我們也不需要保存他們之間的對應關系了,而其實DefaultWebSessionManager也是這樣做的,因數sessionKey這個對象里面就有一個sessionId。
但是有一個值得注意的是,這個方法會被調用多次,用戶登陸成功以后,會話保持成功后,怎么調用,傳入的sessionKey都是一樣的,但是我們把鏡頭拉到用戶登陸的那一次請求中,就會發現一些不同的地方了。
我們可以看到,第一次調用時,sessionKey里面的sessionId是空的,按照我們的邏輯,我們會調用第二個方法,取得header中的token,然后返回sessionId為token。
斷點繼續,第二次調用的時候,也會傳入一個sessionKey,但是這個sessionKey里面的sessionId值卻已經有了,它是一個uuid,但是sessionKey里面的sessionId,與第一次返回的sessionId不一致,或者說和我們的token不一致,這是為什么呢?
因為當得到sessionId時,session管理器會嘗試到sessionStore中通過這個sessionKey去獲取一個session,但是可以肯定的是,這個session肯定是得不到的,因為還沒有代碼給它創建,所以當檢測到獲取到的session為null的時候,會調用sessionStore的createSession方法,這個時候,它會生成一個隨機的sessionId,然后根據這個新生成的sessionId,創建一個session,然后會把這個sessionId設置到sessionKey里面,替換掉之前的sessionId,所以我們在這個方法后面的幾次調用就就會發現第一次不一樣,sessionId也和第一次返回的sessionId不一樣,因為它創建session的時候生成了一個新的sessionId,這個時候我們要怎么辦呢?
我們就修改客戶端的token,讓它與最新生成的sessionId一致就行了,所以之前說的,這里面有一個把token設置到request中的代碼,就是在返回給客戶端的時候,通知給客戶端最新的token,而不是繼續沿用之前的token,因為這個token在sessionStore中是沒法取出一個session的。
還有一個要注意的地方,我們從request取出新的token返回給客戶端的時候,要在認證完成之后,因為只有當認證完成之后,才會創建session,才會得到最新的token並返回給客戶端,不然返回的是老的token。
代碼如下:
@RequestMapping("/") public void login(@RequestParam("code")String code, HttpServletRequest request){ Map<String,Object> data = new HashMap<>(); if(SecurityUtils.getSubject().isAuthenticated()){
//這里代碼着已經登陸成功,所以自然不用再次認證,直接從rquest中取出就行了, data.put(StatelessSessionManager.HEADER_TOKEN_NAME,getServerToken()); data.put(BIND,ShiroKit.getUser().getTel() != null); response(data); } LOG.info("授權碼為:" + code); AuthorizationService authorizationService = authorizationFactory.getAuthorizationService(Constant.clientType); UserDetail authorization = authorizationService.authorization(code); Oauth2UserDetail userDetail = (Oauth2UserDetail) authorization; loginService.login(userDetail); User user = userService.saveUser(userDetail,Constant.clientType.toString()); ShiroKit.getSession().setAttribute(ShiroKit.USER_DETAIL_KEY,userDetail); ShiroKit.getSession().setAttribute(ShiroKit.USER_KEY,user); data.put(BIND,user.getTel() != null);
//這里的代碼,必須放到login之執行,因為login后,才會創建session,才會得到最新的token咯 data.put(StatelessSessionManager.HEADER_TOKEN_NAME,getServerToken()); response(data); }
我們把token返回給客戶端,然后客戶端每次請求時,帶上這個token,我們就維持這個會話了
3.2:第二個方法
方法簽名如下
protected Serializable getSessionId(ServletRequest servletRequest, ServletResponse servletResponse)
第二個方法相對簡單,因為僅僅是獲取token而已,可以從header獲取,參數中獲取,cookie中獲取,當然用戶第一次請求的時候,肯定是沒有token的,只有登陸成功后才會得到token,所以當token為null的時候,我們生成了一個uuid,但是這個uuid並不會成為后面的token,這個在上面有講到,因為會被后面生成session時生成的sessionId給替換掉。
而至少那一堆設置數據到request中的代碼,我也沒去看具體做什么用的,因為它的父類中,執行這個方法的時候,有這些代碼的設置,復制過來,怕出什么問題。
四:完整配置代碼
完整的配置代碼如下:
import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.LinkedHashMap; import java.util.Map; /** * @author zxj<br> * 時間 2017/11/8 15:40 * 說明 ... */ @Configuration public class ShiroConfiguration { @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){ return new LifecycleBeanPostProcessor(); } /** * 此處注入一個realm * @param realm * @return */ @Bean public SecurityManager securityManager(Realm realm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setSessionManager(new StatelessSessionManager()); securityManager.setRealm(realm); return securityManager; } @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager); Map<String,String> map = new LinkedHashMap<>(); map.put("/public/**","anon"); map.put("/login/**","anon"); map.put("/**","user"); bean.setFilterChainDefinitionMap(map); return bean; } }
其實完整的配置代碼都已經不重要了,重要的就是sessionManager,上面紅色部分說明了怎么把我們自己寫的sessionManager設置到securityManager中。