shiro實現無狀態的會話,帶源碼分析


轉載請在頁首明顯處注明作者與出處

朱小傑        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中。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM