Shiro是如何攔截未登錄請求的(二)

前言

在上一篇文章{% post_link Shiro是如何攔截未登錄請求的(一) %}中提到了,我們在實際的項目中采用了基于token的方式來實現(xiàn)用戶的身份鑒權,但是由于開發(fā)的時候對shiro的內部機制不太了解導致那一塊的代碼實現(xiàn)不夠完善舍肠、整潔并且還對業(yè)務造成了影響,經過了對shiro源碼的跟蹤分析之后,我們已經知道shiro是如何攔截未登錄請求的了,那么接下來我們開始來針對問題制定相應的解決方案.

解決方案

第一種方案
由于最初在app端是使用傳輸cookie的方式來實現(xiàn)身份鑒權的,跨域問題也已經解決了,為了盡量不改動已經寫好的代碼,我們可以想辦法來讓h5應用也能在跨域的情況下傳輸cookie,首先服務端在使用cors協(xié)議時需要設置響應消息頭Access-Control-Allow-Credentials的值為true即允許在ajax訪問時攜帶cookie,客戶端方面也需通過js設置withCredentials為true才能真正實現(xiàn)跨域傳輸cookie.另外為了安全,在cors標準里不允許Access-Control-Allow-Origin設置為*,而是必須指定明確的、與請求網(wǎng)頁一致的域名.cookie也依然遵循“同源策略”,只有用目標服務器域名設置的cookie才會上傳,而且使用document.cookie也無法讀取目標服務器域名下的cookie.接下來我們來看看代碼是怎么實現(xiàn)的:
1.我們原先在springboot中關于支持跨域有多種實現(xiàn)方式,我們采用最后的一種:

    @Bean
    public FilterRegistrationBean corsFilter() {
        return new FilterRegistrationBean(new Filter() {
            public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                    throws IOException, ServletException {
                HttpServletRequest request = (HttpServletRequest) req;
                HttpServletResponse response = (HttpServletResponse) res;
                String method = request.getMethod();
                String origin = request.getHeader("Origin");
                if(origin == null) {
                    origin = request.getHeader("Referer");
                }
                // this origin value could just as easily have come from a database
                response.setHeader("Access-Control-Allow-Origin", origin);            // 允許指定域訪問跨域資源
                //response.setHeader("Access-Control-Allow-Origin", "*");
                response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, OPTIONS");
                response.setHeader("Access-Control-Max-Age", "3600");
                response.setHeader("Access-Control-Allow-Credentials", "true");
                response.setHeader("Access-Control-Allow-Headers", "Accept, Origin, X-Requested-With, Content-Type,Last-Modified,device,token");
                if ("OPTIONS".equals(method)) {
                    response.setStatus(HttpStatus.OK.value());
                } else {
                    chain.doFilter(req, res);
                }
            }

            public void init(FilterConfig filterConfig) {
            }

            public void destroy() {
            }
        });
    }

2.客戶端也不再需要在請求頭中帶上token了,只要登錄之后不管調什么接口都會自動帶上cookie到后端校驗的,代碼如下:

    $.ajax({
        url:'http://localhost:8080/win/api/test/cors',
        type:'post',
         beforeSend:(xhr)=> {
           //xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
           //xhr.setRequestHeader("token", "web_session_key-5ce2ae9c-8f79-4f83-9b47-1510da4b2fb0");
           xhr.setRequestHeader("device","APP");
        },
        xhrFields:{
            withCredentials:true,
            useDefaultXhrHeader:false
        },
        corssDomain:true,
        success:function(data){
        console.log(data);
        }
    });

這樣接口就可以正常返回數(shù)據(jù)了,控制臺也不再報錯(注意request header中的cookie).

image.png

第二種方案
從上一篇文章中我們知道shiro是在其默認的會話管理器DefaultWebSessionManager中獲取請求攜帶過來的cookie的,我們可以通過繼承這個類來擴展其中相關的代碼來實現(xiàn)我們的需求,之前在項目中我們已經擴展過這個類了,當時是為了重寫其中定時驗證session有效性的部分以便在session失效時做一些數(shù)據(jù)清理工作,下面貼出的是shiro從cookie中獲取sessionid的主要源代碼:

    @Override
    public Serializable getSessionId(SessionKey key) {
        Serializable id = super.getSessionId(key);
        if (id == null && WebUtils.isWeb(key)) {
            ServletRequest request = WebUtils.getRequest(key);
            ServletResponse response = WebUtils.getResponse(key);
            id = getSessionId(request, response);
        }
        return id;
    }

    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        return getReferencedSessionId(request, response);
    }
    private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
        String id = getSessionIdCookieValue(request, response);
        .......
        return id;
    }
    private String getSessionIdCookieValue(ServletRequest request, ServletResponse response) {
        .......
        //getSessionIdCookie().readValue()操作的是cookie對象.
        return getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response));
    }

那么我們只要在擴展類中覆寫這些方法,通過在請求頭傳輸過來的device標識便可以區(qū)分出不同的調用端來源,即pc端后臺依然采用shiro原有的認證方式,而app端或者h5應用則可以使用基于token的身份認證方式,達到兩者共存的目的.下面來看看我們自定義的CustomerWebSessionManager類,其繼承了shiro的DefaultWebSessionManager類.

public class CustomerWebSessionManager extends DefaultWebSessionManager {

    private static final Logger logger = LoggerFactory.getLogger(CustomerWebSessionManager.class);

    private static final String AUTH_TOKEN = "token";

    public CustomerWebSessionManager() {
        super();
    }
    @Override
    public void validateSessions() {
        if (logger.isInfoEnabled()) {
            logger.info("Validating all active sessions...");
        }
        ......
    }

其中定義的類靜態(tài)變量AUTH_TOKEN為請求頭中需要攜帶的會話id的名稱,validateSessions方法是我們重寫的用來實現(xiàn)當session失效時做數(shù)據(jù)清理的.由于DefaultWebSessionManager中的大部分方法為私有的方法,無法為其子類所繼承,所以只好重寫其中所有用到的protected方法,代碼如下:

    /**
     * 重寫父類獲取sessionID的方法,若請求為APP或者H5則從請求頭中取出token,若為PC端后臺則從cookie中獲取
     *
     * @param request
     * @param response
     * @return
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        if (!(request instanceof HttpServletRequest)) {
            logger.debug("Current request is not an HttpServletRequest - cannot get session ID.  Returning null.");
            return null;
        }
        HttpServletRequest httpRequest = WebUtils.toHttp(request);
        if (StringHelpUtils.isNotBlank(httpRequest.getHeader("device"))
                && (httpRequest.getHeader("device").equals("APP") || httpRequest
                .getHeader("device").equals("H5"))) {
            //從header中獲取token
            String token = httpRequest.getHeader(AUTH_TOKEN);
            // 每次讀取之后都把當前的token放入response中
            HttpServletResponse httpResponse = WebUtils.toHttp(response);
            if (StringHelpUtils.isNotEmpty(token)) {
                httpResponse.setHeader(AUTH_TOKEN, token);
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            }
            //sessionIdUrlRewritingEnabled的配置為false,不會在url的后面帶上sessionID
            request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
            return token;
        }
        return getReferencedSessionId(request, response);
    }
    /**
     * shiro默認從cookie中獲取sessionId
     *
     * @param request
     * @param response
     * @return
     */
    private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
        String id = getSessionIdCookieValue(request, response);
        if (id != null) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                    ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
        } else {
            //not in a cookie, or cookie is disabled - try the request URI as a fallback (i.e. due to URL rewriting):
            //try the URI path segment parameters first:
            id = getUriPathSegmentParamValue(request, ShiroHttpSession.DEFAULT_SESSION_ID_NAME);
            if (id == null) {
                //not a URI path segment parameter, try the query parameters:
                String name = getSessionIdName();
                id = request.getParameter(name);
                if (id == null) {
                    //try lowercase:
                    id = request.getParameter(name.toLowerCase());
                }
            }
            if (id != null) {
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                        ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
            }
        }
        if (id != null) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            //automatically mark it valid here.  If it is invalid, the
            //onUnknownSession method below will be invoked and we'll remove the attribute at that time.
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
        }
        // always set rewrite flag - SHIRO-361
        request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
        return id;
    }

    //copy from DefaultWebSessionManager
    private String getSessionIdCookieValue(ServletRequest request, ServletResponse response) {
        if (!isSessionIdCookieEnabled()) {
            logger.debug("Session ID cookie is disabled - session id will not be acquired from a request cookie.");
            return null;
        }
        if (!(request instanceof HttpServletRequest)) {
            logger.debug("Current request is not an HttpServletRequest - cannot get session ID cookie.  Returning null.");
            return null;
        }
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        return getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response));
    }

    //since 1.2.2 copy from DefaultWebSessionManager
    private String getUriPathSegmentParamValue(ServletRequest servletRequest, String paramName) {
        if (!(servletRequest instanceof HttpServletRequest)) {
            return null;
        }
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String uri = request.getRequestURI();
        if (uri == null) {
            return null;
        }
        int queryStartIndex = uri.indexOf('?');
        if (queryStartIndex >= 0) { //get rid of the query string
            uri = uri.substring(0, queryStartIndex);
        }
        int index = uri.indexOf(';'); //now check for path segment parameters:
        if (index < 0) {
            //no path segment params - return:
            return null;
        }
        //there are path segment params, let's get the last one that may exist:
        final String TOKEN = paramName + "=";
        uri = uri.substring(index + 1); //uri now contains only the path segment params
        //we only care about the last JSESSIONID param:
        index = uri.lastIndexOf(TOKEN);
        if (index < 0) {
            //no segment param:
            return null;
        }
        uri = uri.substring(index + TOKEN.length());
        index = uri.indexOf(';'); //strip off any remaining segment params:
        if (index >= 0) {
            uri = uri.substring(0, index);
        }
        return uri; //what remains is the value
    }

    //since 1.2.1 copy from DefaultWebSessionManager
    private String getSessionIdName() {
        String name = this.getSessionIdCookie() != null ? this.getSessionIdCookie().getName() : null;
        if (name == null) {
            name = ShiroHttpSession.DEFAULT_SESSION_ID_NAME;
        }
        return name;
    }

當shiro取不到sessionid時,會調用DelegatingSubject類中的getSession(true)方法創(chuàng)建一個新的session.

    public Session getSession(boolean create) {
        if (log.isTraceEnabled()) {
            log.trace("attempting to get session; create = " + create +
                    "; session is null = " + (this.session == null) +
                    "; session has id = " + (this.session != null && session.getId() != null));
        }

        if (this.session == null && create) {

            //added in 1.2:
            if (!isSessionCreationEnabled()) {
                String msg = "Session creation has been disabled for the current subject.  This exception indicates " +
                        "that there is either a programming error (using a session when it should never be " +
                        "used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created " +
                        "for the current Subject.  See the " + DisabledSessionException.class.getName() + " JavaDoc " +
                        "for more.";
                throw new DisabledSessionException(msg);
            }

            log.trace("Starting session for host {}", getHost());
            SessionContext sessionContext = createSessionContext();
            Session session = this.securityManager.start(sessionContext);
            this.session = decorate(session);
        }
        return this.session;
    }

上面的第22行this.securityManager.start最終調用的是DefaultWebSessionManager中的onStart方法,所以我們要重寫這個方法,將產生的sessionid放到response header中.另外當session失效或銷毀時的相關方法也需重新實現(xiàn),具體代碼如下:

    //存儲會話id到response header中
    private void storeSessionId(Serializable currentId, HttpServletRequest request, HttpServletResponse response) {
        if (currentId == null) {
            String msg = "sessionId cannot be null when persisting for subsequent requests.";
            throw new IllegalArgumentException(msg);
        }
        String idString = currentId.toString();
        if (StringHelpUtils.isNotBlank(request.getHeader("device"))
                && (request.getHeader("device").equals("APP") || request
                .getHeader("device").equals("H5"))) {
            response.setHeader(AUTH_TOKEN, idString);
        } else {
            Cookie template = getSessionIdCookie();
            Cookie cookie = new SimpleCookie(template);
            cookie.setValue(idString);
            cookie.saveTo(request, response);
        }
        logger.trace("Set session ID cookie for session with id {}", idString);
    }

    //設置deleteMe到response header中
    private void removeSessionIdCookie(HttpServletRequest request, HttpServletResponse response) {
        if (StringHelpUtils.isNotBlank(request.getHeader("device"))
                && (request.getHeader("device").equals("APP") || request
                .getHeader("device").equals("H5"))) {
            response.setHeader(AUTH_TOKEN, Cookie.DELETED_COOKIE_VALUE);
        } else {
            getSessionIdCookie().removeFrom(request, response);
        }
    }

    /**
     * 會話創(chuàng)建
     * Stores the Session's ID, usually as a Cookie, to associate with future requests.
     *
     * @param session the session that was just {@link #createSession created}.
     */
    @Override
    protected void onStart(Session session, SessionContext context) {
        super.onStart(session, context);
        if (!WebUtils.isHttp(context)) {
            logger.debug("SessionContext argument is not HTTP compatible or does not have an HTTP request/response " +
                    "pair. No session ID cookie will be set.");
            return;
        }
        HttpServletRequest request = WebUtils.getHttpRequest(context);
        HttpServletResponse response = WebUtils.getHttpResponse(context);
        if (isSessionIdCookieEnabled()) {
            Serializable sessionId = session.getId();
            storeSessionId(sessionId, request, response);
        } else {
            logger.debug("Session ID cookie is disabled.  No cookie has been set for new session with id {}", session.getId());
        }
        request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE);
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
    }
    //會話失效
    @Override
    protected void onExpiration(Session s, ExpiredSessionException ese, SessionKey key) {
        super.onExpiration(s, ese, key);
        onInvalidation(key);
    }

    @Override
    protected void onInvalidation(Session session, InvalidSessionException ise, SessionKey key) {
        super.onInvalidation(session, ise, key);
        onInvalidation(key);
    }

    private void onInvalidation(SessionKey key) {
        ServletRequest request = WebUtils.getRequest(key);
        if (request != null) {
            request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID);
        }
        if (WebUtils.isHttp(key)) {
            logger.debug("Referenced session was invalid.  Removing session ID cookie.");
            removeSessionIdCookie(WebUtils.getHttpRequest(key), WebUtils.getHttpResponse(key));
        } else {
            logger.debug("SessionKey argument is not HTTP compatible or does not have an HTTP request/response " +
                    "pair. Session ID cookie will not be removed due to invalidated session.");
        }
    }
    //會話銷毀
    @Override
    protected void onStop(Session session, SessionKey key) {
        super.onStop(session, key);
        if (WebUtils.isHttp(key)) {
            HttpServletRequest request = WebUtils.getHttpRequest(key);
            HttpServletResponse response = WebUtils.getHttpResponse(key);
            logger.debug("Session has been stopped (subject logout or explicit stop).  Removing session ID cookie.");
            removeSessionIdCookie(request, response);
        } else {
            logger.debug("SessionKey argument is not HTTP compatible or does not have an HTTP request/response " +
                    "pair. Session ID cookie will not be removed due to stopped session.");
        }
    }

最后再在springboot中做如下配置:

    @Bean
    public CustomerWebSessionManager sessionManager() {
        CustomerWebSessionManager sessionManager = new CustomerWebSessionManager();
        //會話驗證器調度時間
        sessionManager.setSessionValidationInterval(1800000);
        //定時檢查失效的session
        sessionManager.setSessionValidationSchedulerEnabled(true);
        //是否在會話過期后會調用SessionDAO的delete方法刪除會話 默認true
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionDAO(redisSessionDAO());
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        sessionManager.setSessionIdCookie(wapsession());
        sessionManager.setSessionIdCookieEnabled(true);
        return sessionManager;
    }

我們來看看在postman中的接口訪問測試結果:


image.png

而在h5應用中已無需再支持跨域傳輸cookie了,但需重新在請求頭中傳輸token,js代碼稍微做如下修改:

    $.ajax({
        url:'http://localhost:8080/win/api/test/cors',
        type:'post',
         beforeSend:(xhr)=> {
           //xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
           xhr.setRequestHeader("token", "web_session_key-26653f18-1d81-4bd3-a039-301870788abb");
           xhr.setRequestHeader("device","APP");
        },
        xhrFields:{
            //withCredentials:true,
            //useDefaultXhrHeader:false
        },
        //corssDomain:true,
        success:function(data){
        console.log(data);
        }
    });

通過瀏覽器可以看到已經成功訪問接口了,控制臺也沒有報錯,結果如下圖(注意其中request header和response header中的token)


image.png

好了,到此我們已經完成了對shiro支持token身份認證的全部改造了.

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末窘面,一起剝皮案震驚了整個濱河市翠语,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌财边,老刑警劉巖肌括,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異酣难,居然都是意外死亡谍夭,警方通過查閱死者的電腦和手機黑滴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來紧索,“玉大人袁辈,你說我怎么就攤上這事≈槠” “怎么了吵瞻?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長甘磨。 經常有香客問我橡羞,道長,這世上最難降的妖魔是什么济舆? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任卿泽,我火速辦了婚禮,結果婚禮上滋觉,老公的妹妹穿的比我還像新娘签夭。我一直安慰自己,他們只是感情好椎侠,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布第租。 她就那樣靜靜地躺著,像睡著了一般我纪。 火紅的嫁衣襯著肌膚如雪慎宾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天浅悉,我揣著相機與錄音趟据,去河邊找鬼。 笑死术健,一個胖子當著我的面吹牛汹碱,可吹牛的內容都是我干的。 我是一名探鬼主播荞估,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼咳促,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了勘伺?” 一聲冷哼從身側響起跪腹,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎娇昙,沒想到半個月后尺迂,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體笤妙,經...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡冒掌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年噪裕,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片股毫。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡膳音,死狀恐怖,靈堂內的尸體忽然破棺而出铃诬,到底是詐尸還是另有隱情祭陷,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布趣席,位于F島的核電站兵志,受9級特大地震影響,放射性物質發(fā)生泄漏宣肚。R本人自食惡果不足惜想罕,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望霉涨。 院中可真熱鬧按价,春花似錦、人聲如沸笙瑟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽往枷。三九已至框产,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間错洁,已是汗流浹背茅信。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留墓臭,地道東北人蘸鲸。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像窿锉,于是被迫代替她去往敵國和親酌摇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內容

  • 問題描述 之前在公司搭項目平臺的時候權限框架采用的是shiro,由于系統(tǒng)主要面向的是APP端的用戶,PC端僅僅是公...
    Briseis閱讀 34,032評論 4 28
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理嗡载,服務發(fā)現(xiàn)窑多,斷路器,智...
    卡卡羅2017閱讀 134,665評論 18 139
  • 多久沒畫了洼滚,說好的堅持呢埂息!這就是我從來都停步不前的原因,沒有持之以恒的耐心
    日落荷棲閱讀 307評論 3 2
  • 終于下班了,端午節(jié)的前一天千康,我依然奮斗前線中享幽,有句話說的好,只有閑出來的病拾弃,很喜歡這種忙碌的生活值桩,明天端午節(jié)了,祝...
    楊美玲閱讀 153評論 0 0