1. 程式人生 > >Shiro在請求頭中獲取sessionId以及rememberMe資訊

Shiro在請求頭中獲取sessionId以及rememberMe資訊

本文介紹的內容需要對Shiro有一定了解,學習Shiro可檢視跟開濤我學Shiro

解決問題步驟
  • 重寫 DefaultWebSessionManager 命名為 DefaultHeaderSessionManager
  • 重寫 CookieRememberMeManager 命名為 HeaderRememberMeManager
  • 重寫 ShiroFilterFactoryBean,修改其中的預設Filters;
  • 修改配置檔案,指定為重寫的類。

重寫DefaultWebSessionManager

DefaultWebSessionManager:用於Web環境的實現,可以替代ServletContainerSessionManager,自己維護著會話,直接廢棄了Servlet容器的會話管理。
DefaultWebSessionManager預設實現中,是通過Cookie確定sessionId,重寫時,只需要把獲取sessionid的方式變更為在request header中獲取即可。
新建 DefaultHeaderSessionManager

類並 extends DefaultSessionManager 以及 implements WebSessionManager

//省略 import 資訊
/**
 * @author Created by yangyang on 2018/1/18.
 * e-mail :[email protected] ; tel :18580128658 ;QQ :296604153
 */
public class DefaultHeaderSessionManager extends DefaultSessionManager implements WebSessionManager {

}

request header中,我使用x-auth-token進行sessionid標識,接下來直接展示當前類的詳細實現

//省略 import 資訊
public class DefaultHeaderSessionManager extends DefaultSessionManager implements WebSessionManager {

    // slf4j  logback
    private static final Logger log = LoggerFactory.getLogger(DefaultHeaderSessionManager.class);

    private final String X_AUTH_TOKEN = "x-auth-token";

    // 請求頭中獲取 sessionId 並把sessionId 放入 response 中
private String getSessionIdHeaderValue(ServletRequest request, ServletResponse response) { if (!(request instanceof HttpServletRequest)) { log.debug("Current request is not an HttpServletRequest - cannot get session ID cookie. Returning null."); return null; } else { HttpServletRequest httpRequest = (HttpServletRequest) request; // 在request 中 讀取 x-auth-token 資訊 作為 sessionId String sessionId = httpRequest.getHeader(this.X_AUTH_TOKEN); // 每次讀取之後 都把當前的 sessionId 放入 response 中 HttpServletResponse httpResponse = (HttpServletResponse) response; if (StringUtils.isNotEmpty(sessionId)) { httpResponse.setHeader(this.X_AUTH_TOKEN, sessionId); log.info("Current session ID is {}", sessionId); } return sessionId; } } //獲取sessionid private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) { String id = this.getSessionIdHeaderValue(request, response); //DefaultWebSessionManager 中程式碼 直接copy過來 if (id != null) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header"); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); } //不會把sessionid放在URL後 request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, Boolean.FALSE); return id; } // 移除sessionid 並設定為 deleteMe 標識 private void removeSessionIdHeader(HttpServletRequest request, HttpServletResponse response) { response.setHeader(this.X_AUTH_TOKEN, "deleteMe"); } /** * 把sessionId 放入 response header 中 * onStart時呼叫 * 沒有sessionid時 會產生sessionid 並放入 response header中 */ private void storeSessionId(Serializable currentId, HttpServletRequest ignored, HttpServletResponse response) { if (currentId == null) { String msg = "sessionId cannot be null when persisting for subsequent requests."; throw new IllegalArgumentException(msg); } else { String idString = currentId.toString(); response.setHeader(this.X_AUTH_TOKEN, idString); log.info("Set session ID header for session with id {}", idString); log.trace("Set session ID header for session with id {}", idString); } } // 建立session protected Session createExposedSession(Session session, SessionContext context) { if (!WebUtils.isWeb(context)) { return super.createExposedSession(session, context); } else { ServletRequest request = WebUtils.getRequest(context); ServletResponse response = WebUtils.getResponse(context); SessionKey key = new WebSessionKey(session.getId(), request, response); return new DelegatingSession(this, key); } } protected Session createExposedSession(Session session, SessionKey key) { if (!WebUtils.isWeb(key)) { return super.createExposedSession(session, key); } else { ServletRequest request = WebUtils.getRequest(key); ServletResponse response = WebUtils.getResponse(key); SessionKey sessionKey = new WebSessionKey(session.getId(), request, response); return new DelegatingSession(this, sessionKey); } } protected void onStart(Session session, SessionContext context) { super.onStart(session, context); if (!WebUtils.isHttp(context)) { log.debug("SessionContext argument is not HTTP compatible or does not have an HTTP request/response pair. No session ID cookie will be set."); } else { HttpServletRequest request = WebUtils.getHttpRequest(context); HttpServletResponse response = WebUtils.getHttpResponse(context); Serializable sessionId = session.getId(); this.storeSessionId(sessionId, request, response); request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW, Boolean.TRUE); } } //獲取sessionid 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 = this.getSessionId(request, response); } return id; } protected Serializable getSessionId(ServletRequest request, ServletResponse response) { return this.getReferencedSessionId(request, response); } protected void onExpiration(Session s, ExpiredSessionException ese, SessionKey key) { super.onExpiration(s, ese, key); this.onInvalidation(key); } protected void onInvalidation(Session session, InvalidSessionException ise, SessionKey key) { super.onInvalidation(session, ise, key); this.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)) { log.debug("Referenced session was invalid. Removing session ID header."); this.removeSessionIdHeader(WebUtils.getHttpRequest(key), WebUtils.getHttpResponse(key)); } else { log.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."); } } 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); log.debug("Session has been stopped (subject logout or explicit stop). Removing session ID cookie."); this.removeSessionIdHeader(request, response); } else { log.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."); } } public boolean isServletContainerSessions() { return false; }

重寫CookieRememberMeManger

預設情況下,Shiro會把rememberMe資訊放入set-cookie中,儲存在瀏覽器上。這裡,重寫Cookie方式,把rememberMe資訊放入response header中。
建立 HeaderRememberMeManager 類並extends AbstractRememberMeManager ,程式碼如下


//省略 import 資訊
public class HeaderRememberMeManager extends AbstractRememberMeManager {

    private static final transient Logger log = LoggerFactory.getLogger(HeaderRememberMeManager.class);

    // header 中 固定使用的 key
    public static final String DEFAULT_REMEMBER_ME_HEADER_NAME = "remember-me";


    protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
        if (!WebUtils.isHttp(subject)) {
            if (log.isDebugEnabled()) {
                String msg = "Subject argument is not an HTTP-aware instance.  This is required to obtain a servlet request and response in order to set the rememberMe cookie. Returning immediately and ignoring rememberMe operation.";
                log.debug(msg);
            }

        } else {
            HttpServletResponse response = WebUtils.getHttpResponse(subject);
            String base64 = Base64.encodeToString(serialized);
            // 設定 rememberMe 資訊到 response header 中
            response.setHeader(DEFAULT_REMEMBER_ME_HEADER_NAME, base64);
        }
    }

    private boolean isIdentityRemoved(WebSubjectContext subjectContext) {
        ServletRequest request = subjectContext.resolveServletRequest();
        if (request == null) {
            return false;
        } else {
            Boolean removed = (Boolean) request.getAttribute(ShiroHttpServletRequest.IDENTITY_REMOVED_KEY);
            return removed != null && removed;
        }
    }

    protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
        if (!WebUtils.isHttp(subjectContext)) {
            if (log.isDebugEnabled()) {
                String msg = "SubjectContext argument is not an HTTP-aware instance.  This is required to obtain a servlet request and response in order to retrieve the rememberMe cookie. Returning immediately and ignoring rememberMe operation.";
                log.debug(msg);
            }

            return null;
        } else {
            WebSubjectContext wsc = (WebSubjectContext) subjectContext;
            if (this.isIdentityRemoved(wsc)) {
                return null;
            } else {
                HttpServletRequest request = WebUtils.getHttpRequest(wsc);
                // 在request header 中獲取 rememberMe資訊
                String base64 = request.getHeader(DEFAULT_REMEMBER_ME_HEADER_NAME);
                if ("deleteMe".equals(base64)) {
                    return null;
                } else if (base64 != null) {
                    base64 = this.ensurePadding(base64);
                    if (log.isTraceEnabled()) {
                        log.trace("Acquired Base64 encoded identity [" + base64 + "]");
                    }

                    byte[] decoded = Base64.decode(base64);
                    if (log.isTraceEnabled()) {
                        log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
                    }

                    return decoded;
                } else {
                    return null;
                }
            }
        }
    }

    private String ensurePadding(String base64) {
        int length = base64.length();
        if (length % 4 != 0) {
            StringBuilder sb = new StringBuilder(base64);

            for (int i = 0; i < length % 4; ++i) {
                sb.append('=');
            }

            base64 = sb.toString();
        }

        return base64;
    }

    protected void forgetIdentity(Subject subject) {
        if (WebUtils.isHttp(subject)) {
            HttpServletRequest request = WebUtils.getHttpRequest(subject);
            HttpServletResponse response = WebUtils.getHttpResponse(subject);
            this.forgetIdentity(request, response);
        }

    }

    public void forgetIdentity(SubjectContext subjectContext) {
        if (WebUtils.isHttp(subjectContext)) {
            HttpServletRequest request = WebUtils.getHttpRequest(subjectContext);
            HttpServletResponse response = WebUtils.getHttpResponse(subjectContext);
            this.forgetIdentity(request, response);
        }
    }

    private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) {
    //設定刪除標示
        response.setHeader(DEFAULT_REMEMBER_ME_HEADER_NAME, "deleteMe");
    }

}

重寫ShiroFilterFactoryBean

預設Fileter鏈中,user名稱的過濾器在為登陸狀態下會返回到登入介面,這裡修改一下,為登陸狀態直接放回Json字串,不用跳轉至登入頁面。
如果使用了authc過濾 需要對重寫 FormAuthenticationFilter ,為了適配App客戶端,這裡不推薦使用authc,可以在必須重新驗證使用者登陸資訊時(使用rememberMe資訊登陸無效)預先請求一下服務端或者通過記錄的x-auth-token有效期進行判斷。
新建 MyUserFilter 類 extends UserFilter

public class MyUserFilter extends org.apache.shiro.web.filter.authc.UserFilter {

    // isAccessAllowed return false 執行
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        // 這裡也可以不用儲存 儲存當前request 可在登陸後重新請求當前 request
        this.saveRequest(request);
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        httpResponse.setContentType("application/json;charset=utf-8");
        httpResponse.getWriter().write("{\"code\":-1,\"message\":\"no.login\"}");
        return false;
    }

}

新建 MyDefaultFilter enum

// 程式碼 為修改部分 只需要更改 user 執行為新建的 Filter
// 具體程式碼 可檢視 org.apache.shiro.web.filter.mgt.DefaultFilter
public enum MyDefaultFilter {

    user(MyUserFilter.class);

    private final Class<? extends Filter> filterClass;

    private MyDefaultFilter(Class<? extends Filter> filterClass) {
        this.filterClass = filterClass;
    }
}

新建 MyDefaultFilterChainManager 類 extends DefaultFilterChainManager

public class MyDefaultFilterChainManager extends DefaultFilterChainManager {

    protected void addDefaultFilters(boolean init) {

        //使用我們建立的 DefaultFilter
        MyDefaultFilter[] var2 = MyDefaultFilter.values();
        int var3 = var2.length;

        for (int var4 = 0; var4 < var3; ++var4) {
            MyDefaultFilter defaultFilter = var2[var4];
            super.addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
        }

    }

}

新建 MyShiroFilterFactoryBean 類 extends ShiroFilterFactoryBean

public class MyShiroFilterFactoryBean extends ShiroFilterFactoryBean {

    protected FilterChainManager createFilterChainManager() {
        // 只要修改這裡 使用我們建立的 DefaultFilterChainManager
        MyDefaultFilterChainManager manager = new MyDefaultFilterChainManager();

        //省略程式碼  請在 ShiroFilterFactoryBean 中 copy
    }
    // 預設的private方法 需在 ShiroFilterFactoryBean 中 copy
}

修改配置資訊,指向重寫類

這裡只展示和上述有關的配置資訊,採用的註解@Bean 方式

    @Bean(name = "rememberMeManager")
    public HeaderRememberMeManager rememberMeManager() {
        HeaderRememberMeManager headerRememberMeManager = new HeaderRememberMeManager();
        // base64Encoded 自行生成一個 用於rememberMe加密
        headerRememberMeManager.setCipherKey(base64Encoded);
        return headerRememberMeManager;
    }

    @Bean
    public DefaultHeaderSessionManager defaultWebSessionManager(SessionDAO sessionDAO) {
        DefaultHeaderSessionManager defaultHeaderSessionManager = new DefaultHeaderSessionManager();
        // 設立不使用 調取器驗證 session 是否過期 作者使用了 redis ,這裡根據SessionDAO實際情況設定
        defaultHeaderSessionManager.setSessionValidationSchedulerEnabled(false);
        defaultHeaderSessionManager.setSessionDAO(sessionDAO);
        return defaultHeaderSessionManager;
    }

    @Bean(name = "shiroFilter")
    public MyShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
        // 使用自行建立的 FactoryBean
        MyShiroFilterFactoryBean shiroFilterFactoryBean = new MyShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setFilterChainDefinitions("/api/v1/login = anon\n" +
                "/ = anon\n" +
                "/api/v1/website/article/** = anon\n" +
                "/api/v1/** = cors,user\n");
        return shiroFilterFactoryBean;
    }
@Bean(name = "securityManager")
    public DefaultWebSecurityManager defaultWebSecurityManager(                                                               DefaultHeaderSessionManager sessionManager,
                                                               RememberMeManager rememberMeManager) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //其他配置這裡未列出
        //DefaultHeaderSessionManager 重寫的 sessionManager
        defaultWebSecurityManager.setSessionManager(sessionManager);
        // rememberMeManager 重寫的 rememberMeManager
        defaultWebSecurityManager.setRememberMeManager(rememberMeManager);
        SecurityUtils.setSecurityManager(defaultWebSecurityManager);
        return defaultWebSecurityManager;
    }

完成以上配置資訊就可以在 把sessionid或者rememberMe寫入response header中和在 request header中讀取,session超時自動銷燬時間前端需和服務端保持一致,rememberMe有效時間由前端自行控制。

結束

本文介紹了一些專案中使用shiro的技巧,如果有錯誤或者有更好的方式,希望能與筆者聯絡,筆者QQ:296604153。