我們上網的時候,一定遇到過類似這樣的情況,例如使用網易郵箱時進行了登錄操作,之后再訪問網易的博客系統時,發現自動以之前的ID登錄了。這種實現在計算機中稱為 SSO (Single Sign On),即我們常說的 單點登錄 。這種在關聯網站間共享認證信息,避免需要在多個系統中重復輸入帳戶信息的行為,是SSO要解決的。
對于許多應用,可能會獨立部署等情況,所以常會采用cas的形式,來實現SSO。
我們今天要了解的,是作為在同一個Tomcat中部署的應用之間,如何實現SSO,避免重復登錄。
預備:
首先,有幾點預備知識需要先了解一下。
-
在Tomcat架構設計中,不同的Container中包含了Peipline。各個Pipeline中可以添加多種不同形式的Valve。例如我們之前提到的AccessLogValve Tomcat的AccessLogValve介紹
-
Tomcat中session的實現,最常用的是Cookie Session, 通過將名為 JSESSIONID 的cookie寫回瀏覽器,實現session。我們在前面的文章里也描述過。 深入Tomcat源碼分析Session
-
關于認證的一些內容,可以參考介紹過的Basic認證。 你可能不了解的Basic認證
環境:
有了這些準備之后,我們開始進行環境的搭建和實驗。
以Tomcat自帶的幾個應用為例,我們啟動Tomcat后,訪問這兩個應用: docs
、 examples
我們看到,默認是不需要登錄的,都可以直接訪問。
此時,在docs應用的web.xml中增加如下配置:
lt;security-constraintgt;
lt;display-namegt;SecurityConstraintlt;/display-namegt;
lt;web-resource-collectiongt;
lt;web-resource-namegt;ProtectedArealt;/web-resource-namegt;
lt;url-patterngt;/*lt;/url-patterngt;
lt;/web-resource-collectiongt;
lt;auth-constraintgt;
lt;role-namegt;tomcatlt;/role-namegt;
lt;/auth-constraintgt;
lt;/security-constraintgt;
lt;login-configgt;
lt;auth-methodgt;BASIClt;/auth-methodgt;
lt;realm-namegt;SSOTestlt;/realm-namegt;
lt;/login-configgt;
lt;security-rolegt;
lt;role-namegt;tomcatlt;/role-namegt;
lt;/security-rolegt;
此時重啟Tomcat,再次請求docs應用,發現需要驗證了。
同樣,再修改examples應用的web.xml,限制對于其直接訪問,在文件中增加如下內容: lt;url-patterngt;/*lt;/url-patterngt;
。只需要增加這個就可以了,下面是修改內容對應的位置參考。
lt;web-resource-collectiongt;
lt;web-resource-namegt;ProtectedArea-Allowmethodslt;/web-resource-namegt;
lt;url-patterngt;/jsp/security/protected/*lt;/url-patterngt;
lt;url-patterngt; /* lt;/url-patterngt;
lt;http-methodgt;DELETElt;/http-methodgt;
lt;http-methodgt;GETlt;/http-methodgt;
lt;http-methodgt;POSTlt;/http-methodgt;
lt;http-methodgt;PUTlt;/http-methodgt;
lt;/web-resource-collectiongt;
修改之后,examples也需要登錄才能訪問了。由于同樣的認證,我們對兩個應用的訪問需要重復輸入用戶名、密碼進行認證,此時,SSO的配置就顯出了必要性了。
在Tomcat的server.xml中,默認的Host,localhost中,增加以下Valve:
lt;ValveclassName=quot;org.apache.catalina.authenticator.SingleSignOnquot;/gt;
再次重啟Tomcat,這個時候SSO已經生效了,你再重新訪問上面兩個應用時,只需要對其中一個進行認證即可,是不是很容易?
原理:
在前面分析請求流程的幾篇文章中,我們介紹過從CoyoteAdapter進行service處理,再到達各個Pipeline、Valve。(Facade模式與請求處理)
而這些Valve中,對于SSO的Valve SingleSignOn
是在認證的Valve AuthenticatorBase
之前執行。
在SingleSignOn中,會先進行userPrincipal的判斷,不為空就會直接向后執行,為空時,判斷請求中是否包含SSO Cookie。
if(request.getUserPrincipal()!=null){
getNext().invoke(request,response);
return;
}
//Checkforthesinglesignoncookie
Cookiecookie=null;
Cookiecookies[]=request.getCookies();
if(cookies!=null){
for(inti=0;ilt;cookies.length;i ){
if(Constants.SINGLE_SIGN_ON_COOKIE.equals(cookies[i].getName())){
cookie=cookies[i];
break;
}
}
}
if(cookie==null){
getNext().invoke(request,response);
return;
}
對于第一個就進認證的應用,走的流程基本和配置之前一樣,區別就在于SSO配置后,會把認證的信息,添加到Cookie中。并將其存儲并和一個ssoId進行關聯。
BasicAuthenticator:
對于docs應用,使用的是Basic認證方式
principal=context.getRealm().authenticate(username,password);
if(principal!=null){
register(request,response,principal,
HttpServletRequest.BASIC_AUTH,username,password);
return(true);
}
}
FormAuthenticator
對于examples應用,使用的是Form的認證方式,如果是Form認證的應用不是第一個請求,則在請求到達時,已經進行過認證,后面的請求會直接獲取session并關聯到ssoId上。 如果是初次請求即訪問Form認證的應用,SsoId還沒值,流程基本和Basic一樣,不同的是從表單中提取用戶名和密碼信息,再進行 register 。
Principalprincipal=request.getUserPrincipal();
StringssoId=(String)request.getNote(Constants.REQ_SSOID_NOTE);
if(principal!=null){
//AssociatethesessionwithanyexistingSSOsession
if(ssoId!=null){
associate(ssoId,request.getSessionInternal(true));//注意這里,把新獲取到的sessionId關聯到ssoId中
}
returntrue;
}
這里register會把認證的信息添加,在ssoId為空時,進行Cookie的創建,
StringssoId=(String)request.getNote(Constants.REQ_SSOID_NOTE);
if(ssoId==null){
ssoId=sessionIdGenerator.generateSessionId();
Cookiecookie=newCookie(Constants.SINGLE_SIGN_ON_COOKIE,ssoId);
cookie.setMaxAge(-1);
cookie.setPath(quot;/quot;);
//Bugzilla41217
cookie.setSecure(request.isSecure());
//Bugzilla34724
StringssoDomain=sso.getCookieDomain();
if(ssoDomain!=null){
cookie.setDomain(ssoDomain);
}
//ConfigurehttpOnlyonSSOcookieusingsamerulesassessioncookies
if(request.getServletContext().getSessionCookieConfig().isHttpOnly()||
request.getContext().getUseHttpOnly()){
cookie.setHttpOnly(true);
}
response.addCookie(cookie);
//RegisterthisprincipalwithourSSOvalve
sso.register(ssoId,principal,authType,username,password);
request.setNote(Constants.REQ_SSOID_NOTE,ssoId);
Cookie不為空時,進行ssoId和session的關聯
protectedbooleanassociate(StringssoId,Sessionsession){
SingleSignOnEntrysso=cache.get(ssoId);
if(sso==null){
if(containerLog.isDebugEnabled()){
containerLog.debug(sm.getString(quot;singleSignOn.debug.associateFailquot;,
ssoId,session));
}
returnfalse;
}else{
}
sso.addSession(this,ssoId,session);
returntrue;
}
}
我們注意到這行代碼 sso.addSession(this, ssoId, session)
這里會給session添加一個listener,這個listener會在session過期銷毀時,把sso的session也移除掉
應用的SSO
在Pipeline中從SingleSignOn這個Valve開始,一直調用到AuthenticatorBase,再到達其實現類. SingleSignOn這個Valve處理請求時,判斷entry是否為空,此時由于前面的應用已經存儲過該信息,所以這里不為空,就會據此設置request中的authType和principal
SingleSignOnEntryentry=cache.get(cookie.getValue());
if(entry!=null){
request.setNote(Constants.REQ_SSOID_NOTE,cookie.getValue());
//Onlysetsecurityelementsifreauthenticationisnotrequired
if(!getRequireReauthentication()){
request.setAuthType(entry.getAuthType());
request.setUserPrincipal(entry.getPrincipal());
}
而后面的Valve中,認證時首先會判斷principal是否為空。由于前置的sso已經把這些信息填充過了,所以這里就會走這樣的邏輯:
publicvoidinvoke(Requestrequest,Responseresponse)
throwsIOException,ServletException{
//HavewegotacachedauthenticatedPrincipaltorecord?
if(cache){
Principalprincipal=request.getUserPrincipal();//這里不為空
if(principal==null){
Sessionsession=request.getSessionInternal(false);
if(session!=null){
principal=session.getPrincipal();
if(principal!=null){
request.setAuthType(session.getAuthType());
request.setUserPrincipal(principal);
}
}
}
}
總結一下:
單點登錄的實現,是在第一次進行認證的時候,將認證信息進行存儲。后續相同域的請求到達時,會先判斷是否存儲了單點登錄的認證信息,如果已經存儲過,就將其添加到新到達的request中,以此進行后續的認證,從而實現SSO.
PS. 微信公眾號里,代碼的羅列真心不好弄,通過其他的Markdown編輯器預覽效果很不錯的,粘過來就變了形了。各位如果有好的工具或辦法,歡迎留言或私信,謝謝。
相關閱讀
猜你喜歡
掃描或長按下方二維碼,即可關注!
Tags: Tomcat
文章來源:http://mp.weixin.qq.com/s?__biz=MzI3MTEwODc5Ng==