跨域(二級域)session共享

這裏所說的跨域,是指跨二級域名,而且這些域名對應的應用都在同一個app上, 比如我有以下3個域名: 
www.vinceruan.info 
blog.vinceruan.info 
bbs.vinceruan.info 
我要在這三個域名直接共享cookie或者共享session,如何實現呢?在tomcat下又如何實現呢? 

首先我們來了解下cookie,顧名思義,小甜心,少食即可,多吃無益。cookie是通過瀏覽器保存在客戶端的臨時數據,一般這些數據對安全的要求不高,雖然可以通過加密存放和SSL方式提交的方式加強cookie的安全,但不代表cookie就是百分之百安全的。再者,寫入太多的cookie會造成數據管理的不可控,所以建議儘量少寫cookie,像網易、新浪這些大型互聯網公司,如果觀察一下他們的站點,你會發覺他們會在瀏覽器寫下大量的cookie,不過他們內部一般會有一套嚴格的cookie寫入和清理的管理規定。比如網易郵件事業部就有這些規定。 

cookie有兩個很重要的屬性:domain和path 
domain告訴瀏覽器當前要添加的cookie的域名歸屬,如果沒有明確指明則默認爲當前域名,比如通過訪問www.vinceruan.info添加的cookie的域名默認就是www.vinceruan.info,通過訪問blog.vinceruan.info所生成的cookie的域名就是blog.vinceruan.info. 
path告訴瀏覽器當前要添加的cookie的路徑歸屬,如果沒有明確指明則默認爲當前路徑,比如通過訪問www.vinceruan.info/java/hotspot.html添加的cookie的默認路徑就是/java/,通過blog.vinceruan.info/java/hotspot.html生成的cookie的路徑也是/java/. 

在清楚domain和path的生成規則後,我們需要知道瀏覽器在什麼時候提交什麼cookie到服務器,即瀏覽器是通過怎樣的規則篩選cookie並提交到服務器的? 
瀏覽器提交的cookie需要滿足以下兩點: 
1.當前域名或者父域名下的cookie 
而且 
2.當前路徑或父路徑下的cookie 
要滿足以上兩個條件的cookie纔會被提交.舉個例子:有4個cookie: 
cookie1:[name=value, domain=.vinceruan.info path=/] 
cookie2:[name=value, domain=blog.vinceruan.info path=/java/] 
cookie3:[name=value, domain=www.vinceruan.info path=/] 
cookie4:[name=value, domain=blog.vinceruan.info path=/] 
當我訪問blog.vinceruan.info時, 
cookie1可以被提交,因爲.vinceruan.info是blog.vinceruan.info的父域名. path路徑也一致. 
cookie2不能被提交,因爲雖然domain是保持一致的,但是path不一致,當前訪問的是/, 但是cookie2的path是/java/ 
cookie3不能被提交,因爲雖然path是一致的,但是 www.vinceruan.info不是blog.vinceruan.info的父域名. 
cookie4可以被提交,因爲domain和cookie都嚴格保持一致. 

這裏需要注意的是, 在瀏覽器看來. www.vinceruan.info不是blog.vinceruan.info的父域名,而vinceruan.info纔是blog.vinceruan.info的父域名, www.vinceruan.info也算是一個二級域名(這點如果你提交過域名到DNS服務器商的應該會知道,一般我們需要顯式提交 www.vinceruan.info和vinceruan.info, 否則www.vinceruan.info==vinceruan.info是不成立的). 
所以如果我們需要在所有二級域名下共享islogin=1的cookie,用java代碼如下: 
Cookie c = new Cookie("islogin","1"); 
c.setDomain(" .vinceruan.info");//注意是以點號開頭的. 
c.setPath=("/"); 
response.addCookie(c); 

如果要在所有的二級域名下的/java/路徑下共享silogin=1的cookie,用java代碼如下: 
Cookie c = new Cookie("islogin","1"); 
c.setDomain(" .vinceruan.info");//注意是以點號開頭的. 
c.setPath=("/java/"); 
response.addCookie(c); 

ok, 跨域共享cookie就完成了,比較容易,但是跨域共享session呢? 
首先我們需要了解session機制的原理, session都是保存在服務器端的,而http連接是無狀態的,那麼服務器是如何在多次獨立的http連接中爲你維持同一個session呢? 
這依賴於一個叫JSESSIONID的參數,這個參數可以從cookie或url獲得,你如果你打開firebug,同時你的firefox是沒有禁用cookie的,那麼你刷新幾次頁面,你會看到瀏覽器提交的cookie中有一個叫JSESSIONID的key/value,這個就是會話ID,在這裏你可以做一個試驗,我們都知道firefox和IE 的cookie是不共享的,因此你在firefox下登錄了,你用IE打開同一個網址,頁面會顯示你還處在未登錄的狀態,但是如果我在firefox下已經登錄,並且拿到JSESSIONID的值,再到IE中輸入url,並且加入JSESSION=.....的參數,你會發現,IE下也會顯示我已經登錄了。 
這說明,如果我通過網絡截除到了別人的JESSIONID,我就可以在服務器的會話還沒過期前訪問該站,如果對方還是處於登錄狀態的,我就可以修改對方資料甚至密碼,多麼可怕,當然這一切都是理論上的。 
好了,一切都顯得很順利,要在不同的二級域名共享session,只需要把JESSIONID寫進共享Cookie就行了。 

可是,真的有這麼容易麼? 
不同語言的實現手法不一樣,聽PHP方面的高手說是很容易實現的,可是java就比較難了,首先我們知道了request.getSession(true)的原理。 
我這裏tomcat爲例,因爲Java servlet api只是一套servlet接口規範,不同的java web容器實現的具體細節可能不同。 
在tomcat中,當我們調用request.getSession(true)時,如果實際上新建了一個session,那麼就會往瀏覽器寫入一個JSEESIONID的cookie.這個cookie的domain屬性是跟你當前站的域名是嚴格保持一致的,即類似於www.vinceruan.info或 blog.vinceruan.info而不是.vinceruan.info, 所以如果我們在session創建時就寫入一個共享的cookie就行了. 

可是這還是有困難: 
1.如果我們在訪問www.vinceruan.info新建了一個session的同時, 我寫入一個JSESSIONID=****的、domain是.vinceruan.info的cookie, 理論上blog.vinceruan.info 也可以訪問到在www.vinceruan.info這邊創建的session了,但是很遺憾,如果我在訪問www.vinceruan.info前先訪問了blog.vinceruan.info, 同時創建了屬於blog.vinceruan.info的session,這是再去訪問www.vinceruan.info站點,同時建立了另一個session,並且建立了共享的JSESSIONID,這時再回去訪問blog.vinceruan.info時,你會看到瀏覽器提交了兩個JSESSIONID的cookie,類似於: 
JSESSIONID=EXAD2AD5FA4D1A5D4 path =/ domain=blog.vinceruan.info 
JSESSIONID=254SD2S5S1D2S5DS5 path = / domain=.vinceruan.info 
可以看出,後者纔是我手工創建的,而前者是tomcat創建的。很遺憾,我手工創建的不生效,因爲tomcat在綁定session時,採取嚴格匹配更加優先的原則,blog.vinceruan.info比.vinceruan.info更加匹配。 
所以問題已經轉化爲無論是那個應用,我要想辦法在第一個創建session的應用中同時寫入共享的JSESSIONID,即是說,我在寫JSESSIONID到某個域下時,不允許這個域及其子域存在JSESSIONID不同值的情況。怎麼解決呢?或許可以通過以下代碼解決: 
public Session getSession(HttpServletRequest request, HttpServletResponse response){ 
      HttpSession session = request.getSession(false); 
       if (session==null){ 
             session = request.getSession(true); 
             String session_id = session.getId(); 
             Cookie c = new Cookie("JSESSIONID",session_id); 
             c.setDomain(".vinceruan.info"); 
             c.setPath("/"); 
             response.addCookie(); 
        } 

然後在所有應用中需要用到session的地方都調用這個方法,好像真的可以解決了,但是真的可以麼? 
還是有問題: 
1.如果我已經往response裏面輸出body內容了,而這時調用這個方法請求session,且session是第一次被創建時會有問題。 
2.對於我調用的第三方代碼,比如spring,它們裏面調用session的方式不是我能控制的。 

好的,這個方法已經明顯不行了。 
還有一個HttpSessionListener,  它裏面還有一個sessionCreated(SessionEvent event)的回調方法,無論是在什麼代碼中新建session,這個回調接口是一定會被調用的,但是,我拿不到HttpServletResponse句柄.所以這個方法還是不行. 

我沒有用SSO,是因爲我這個應用的需求很簡單,就是共享登錄狀態,而且就一個應用,只不過對應到不同的域名而已,所以沒有必要用那麼重量級的東西。 

最後的修改是爲tomcat增加一個Valve,同時註冊到server.xml中去,這種方式也不太好,因爲跟tomcat耦合了,但是也是目前可以想到的比較好的一個方法了。 
首先新建一個類: 
public class SessionCrossDomainValve extends ValveBase { 

public SessionCrossDomainValve() { 
super(); 
info = "com.jinfuzi.SessionCrossDomainValve"; 


@Override 
public void invoke(Request request, Response response) throws IOException, ServletException { 
request.getSession(true); 
// replace any Tomcat-generated session cookies with our own 
Cookie[] cookies = response.getCookies(); 
if (cookies != null) { 
for (int i = 0; i < cookies.length; i++) { 
Cookie cookie = cookies; 
containerLog.debug("CrossSubdomainSessionValve: Cookie name is " 
+ cookie.getName()); 
if (Globals.SESSION_COOKIE_NAME.equals(cookie.getName())) { 
replaceCookie(request, response, cookie); 



// process the next valve 
getNext().invoke(request, response); 


   @SuppressWarnings("unchecked")   
      protected void replaceCookie(Request request, Response response, Cookie cookie) {   
          //copy the existing session cookie, but use a different domain   
          Cookie newCookie = new Cookie(cookie.getName(), cookie.getValue());   
          if (cookie.getPath() != null)   
              newCookie.setPath(cookie.getPath());   
          newCookie.setDomain(getCookieDomain(request));   
          newCookie.setMaxAge(cookie.getMaxAge());   
          newCookie.setVersion(cookie.getVersion());   
          if (cookie.getComment() != null)   
              newCookie.setComment(cookie.getComment());   
          newCookie.setSecure(cookie.getSecure());   
     
          //if the response has already been committed, our replacement strategy will have no effect   
          if (response.isCommitted())   
              containerLog.error("CrossSubdomainSessionValve: response was already committed!");   
     
          //find the Set-Cookie header for the existing cookie and replace its value with new cookie   
          MimeHeaders headers = response.getCoyoteResponse().getMimeHeaders();   
          for (int i = 0, size = headers.size(); i < size; i++)   
          {   
              if (headers.getName(i).equals("Set-Cookie"))   
              {   
                  MessageBytes value = headers.getValue(i);   
                  if (value.indexOf(cookie.getName()) >= 0)   
                  {   
                      StringBuffer buffer = new StringBuffer();   
                      ServerCookie.appendCookieValue(buffer, newCookie.getVersion(), newCookie   
                              .getName(), newCookie.getValue(),  newCookie.getPath(), newCookie 
                              .getDomain(), newCookie.getComment(), newCookie.getMaxAge(), newCookie   
                              .getSecure());  //如果是tomcat6.020,這裏需要多加一個true. 
                      containerLog.debug("CrossSubdomainSessionValve: old Set-Cookie value: " + value.toString());   
                      containerLog.debug("CrossSubdomainSessionValve: new Set-Cookie value: " + buffer);   
                      value.setString(buffer.toString());   
                  }   
              }   
          }   
      } 
    
    protected String getCookieDomain(Request request) { 
String cookieDomain = request.getServerName(); 
String[] parts = cookieDomain.split("\\."); 
if (parts.length >= 2) 
cookieDomain = parts[parts.length - 2] + "." 
+ parts[parts.length - 1]; 
return "." + cookieDomain; 
    } 

public String toString() { 
return ("CrossSubdomainSessionValve[container=" + container.getName() + ']'); 
}   

將這個類打包成jar包,放進{catalina_home}/lib下,並在server.xml中註冊: 
<Valve className="SessionCrossDomainValve"/>
發佈了24 篇原創文章 · 獲贊 23 · 訪問量 29萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章