Java實現的SSO單點登入
原理
在網上找了很多SSO 框架,不是太複雜就是侵入式的,比如CAS,josso,後為想還是自己寫一個吧,反正不難。以下記錄一下,希望對大家有用.
1:產生背景
想像一下,一家企業從無資訊化系統開始著手實現自己公司的資訊化,假如這家公司有自己的IT團隊,第一個系統公司一般都會先上OA系統,他的基本結構如下:
系統上線後執行很正常,公司從企業資訊化中嚐到甜頭,想接著開發第二個系統:採購系統.公司IT人員注意到採購系統可以利用OA系統中的公共程式碼:使用者、組織架構、應用許可權。
公司為了節約成本和加快開發速度會採用重用OA系統中的公共程式碼. 這裡有兩種複用情況供他們選擇
(1): 把所有公共表結構和公共程式碼複製一份,作為採購系統的開發基線.
(2): 把公共表結構和公共程式碼從OA系統中抽離出來,作為一個獨立的系統,叫他統一檢視,對外提供RESTFul介面或者Web Service介面供其它系統呼叫.形成如下結構:
兩種方案比較:
第一種方案簡單明瞭,對原有OA系統衝擊小或者無衝擊.但是他會導致採購系統中有一份和OA系統中一樣的一份使用者資料,兩邊都可以對使用者新增和刪除、修改,這樣會導致兩邊使用者不一致,有一種做法就是採購系統中把使用者相關的操作移除,系統初始化時從OA系統中匯入使用者資料,然後每天晚上去增量同步OA中更新過的使用者資料.這樣採購系統應該也可以執行良好.
第二種方案是一種比第一種方案更好的方案,因為他提供統一地方管理所有應用公共的資源,對企業以後做應用門戶打下良好的基礎,但是此時對OA系統的衝擊比較大,主要衝擊在:
(1): 以有呼叫戶公共資料的地方是本地呼叫,要全部都修改成遠端API呼叫.
(2): 以前可以關聯公共表查詢的地方由於表不在本地資料將都導致要修改.
正是對由OA系統的衝擊很大,OA系統又是一個執行良好的系統,導致管理層不願使用第二種方案,而更願意使用第一種方案,隨著系統的越來越多,將導致一些問題,如單點登陸不能很好實現。這裡還有一種方案就是把統一檢視直接做到OA中,這樣對OA系統沒有上面兩點衝突,OA系統只要提供相應的API就可以了,但是統一檢視有可能會做得不是很全面,它可能只完成了使用者、組織架構部份,但對權根管理不能做到統一管理,這一部份還是會交給各應用自己去實現。
有一些企業有可能一開始就採用第二種方案實現自己的資訊化架構,但一般都比較少,都是後期進行慢慢修改向第二種方案進行改造。
2:SSO實現基礎
2.1:SSO 三個組成部份
從第一部份產生背景分析來看,好像還沒有引入SSO(單點登陸)。什麼是SSO呢?比如上面所說的OA系統、採購系統,他們兩個系統都有自己的登陸介面,使用者A要使用OA系統,在瀏覽器中輸入OA系統的使用者名稱和密碼,就可以使用OA系統了,但是如果使用者A想使用採購系統,那他要必須做同樣的操作,在背景分析進行了使用者資訊整合後,使用者密碼只有一個,因為使用者修改密碼只能在一處修改其它地方使用,如果不共享使用者資訊,那麼使用者有可能還有兩個密碼,這個是SSO不允許的。 單點登陸是指使用者只要在一處輸入使用者名稱和密碼,進行了登陸再訪問其它系統就無須再次登陸此係統就可以使用此係統。那這個”一處”到底是OA系統的登陸介面還是採購系統的登陸介面呢?因為選擇那一個對另一個都不公平^^,所以都不選,這樣就催生了一個第三方,只負責使用者登陸的系統,這個系統我們叫他SSO Server有時也叫認證中心,OA系統和採購系統就成了SSO Server的Clent, 我們叫他SSO Client.
綜上所述SSO 要實現,要由三個重要部份組成
(1): 一個統一的使用者檢視,通常只要使用者表就可以了,不包括組織架構,功能選單,許可權,角色,角色許可權分配,使用者角色分配。但有時使用者會掛組織架構下,所以使用者檢視可能還包括組織架構。使用者檢視是統一檢視的一部份
(2):SSO Server, 統一的登陸介面,只由此係統統一訪問使用者檢視進行驗證.
(3):SSO Client, 需要使用 SSO Server進行驗證的應用
2.2:SSO 技術基礎
要實現Java SSO 平臺必須掌握如下技術
(1): 同源策略
(2): Cookie和Session實現原理
(3): 通過應用程式呼叫Http API 技術,如:Http Client
(4): Java Web
(5): 加密碼技術(對稱加密碼/非對稱加密碼)
(6): Memcached
3:SSO實現
3.1:總體流程圖
3.2:使用者退出
使用者點OA系統的退出,按鈕將退出整個SSO,如果此時使用者正在使用採購系統,當前Session使用者是可以使用的,但是隻要此session失效,下次必須登陸採購系統。
實現流程
3.3:安全
(1): SSO 全部URL介面使用HTTPS,客戶端整合方便,可以使用無證書呼叫
(2): SSO Server中 sso cookie應使用對稱加密碼
http://liulang203.iteye.com/blog/1028257
(3): SSO Client 中對ticket應使用非對稱加密碼
http://snowolf.iteye.com/blog/381767
(4): 可以使用數字信封
3.4:基本程式碼
還沒有完全寫完,安全部份和退出部份自己去實現一個,如果有需要告訴我一下,我再去更新一下。如果沒有需要,先這樣了。再初的想法基本程式碼:
Cookie實現的單點登入
首先,單點登入分為“服務端”和“客戶端”。服務端就是單點登入伺服器,而客戶端通常是“函式庫”或者“外掛”。需要使用單點登入的應用程式,需要把客戶端外掛安裝到自己的系統中,或者將客戶端函式庫包括在程式碼中。單點登入的客戶端通常替換了原來應用程式的認證部分的程式碼。
某個應用程式首先要發起第1次認證。大部分情況下,應用程式中嵌入的客戶端會把應用程式原來的登入畫面遮蔽掉,而直接轉到單點登入伺服器的登入頁面。
使用者在單點登入伺服器的登入頁面中,輸入使用者名稱和密碼。
然後單點登入伺服器會對使用者名稱和密碼進行認證。認證本身並不是單點登入伺服器的功能,因此,通常會引入某種認證機制。認證機制可以有很多種,例如自己寫一個認證程式,或者使用一些標準的認證方法,例如LDAP或者資料庫等等。在大多數情況下,會使用LDAP進行認證。這是因為LDAP在處理使用者登入方面,有很多獨特的優勢,這在本文的後面還會比較詳細地進行介紹。
認證通過之後,單點登入伺服器會和應用程式進行一個比較複雜的互動,這通常是某種授權機制。CAS使用的是所謂的Ticket。具體這點後面還會介紹。
授權完成後,CAS把頁面重定向,回到Web應用。Web應用此時就完成了成功的登入(當然這也是單點登入的客戶端,根據返回的Ticket資訊進行判斷成功的)。
然後單點登入伺服器會在客戶端建立一個Cookie。注意,是在使用者的客戶端,而不是服務端建立一個Cookie。這個Cookie是一個加密的Cookie,其中儲存了使用者登入的資訊。
如果使用者此時希望進入其他Web應用程式,則安裝在這些應用程式中的單點登入客戶端,首先仍然會重定向到CAS伺服器。不過此時CAS伺服器不再要求使用者輸入使用者名稱和密碼,而是首先自動尋找Cookie,根據Cookie中儲存的資訊,進行登入。登入之後,CAS重定向回到使用者的應用程式。
這樣,就不再需要使用者繼續輸入使用者名稱和密碼,從而實現了單點登入。
注意,這種單點登入體系中,並沒有通過http進行密碼的傳遞(但是有使用者名稱的傳遞),因此是十分安全的。
CAS被設計為一個獨立的Web應用,目前是通過若干個Java servlets來實現的。CAS必須執行在支援SSL的web伺服器至上。應用程式可以通過三個URL路徑來使用CAS,分別是登入URL(login URL),校驗URL(validation URL)和登出URL(logout URL)。
應用程式一開始,通常跳過原來的登陸介面,而直接轉向CAS自帶的登入介面。當然也可以在應用程式的主介面上增加一個登入之類的按鈕,來完成跳轉工作。
如果使用者喜歡的話,也可以手工直接進入CAS的登入介面,先進行登入,在啟動其他的應用程式。不過這種模式主要用於測試環境。
CAS的登入介面處理所謂的“主體認證”。它要求使用者輸入使用者名稱和密碼,就像普通的登入介面一樣。
主體認證時,CAS獲取使用者名稱和密碼,然後通過某種認證機制進行認證。通常認證機制是LDAP。
為了進行以後的單點登入,CAS向瀏覽器送回一個所謂的“記憶體cookie”。這種cookie並不是真的儲存在記憶體中,而只是瀏覽器一關閉,cookie就自動過期。這個cookie稱為“ticket-granting cookie”,用來表明使用者已經成功地登入。
認證成功後,CAS伺服器建立一個很長的、隨機生成的字串,稱為“Ticket”。隨後,CAS將這個ticket和成功登入的使用者,以及服務聯絡在一起。這個ticket是一次性使用的一種憑證,它只對登入成功的使用者及其服務使用一次。使用過以後立刻失效。
主體認證完成後,CAS將使用者的瀏覽器重定向,回到原來的應用。CAS客戶端,在從應用轉向CAS的時候,同時也會記錄原始的URL,因此CAS知道誰在呼叫自己。CAS重定向的時候,將ticket作為一個引數傳遞回去。
例如原始應用的網址是http://www.itil.com/,在這個網址上,一開始有如下語句,轉向CAS伺服器的單點登入頁面https://secure.oa.com/cas/login?service=http://www.itil.com/auth.aspx。
CAS完成主體認證後,會使用下面URL進行重定向http://www.itil.com/authenticate.aspx?ticket= ST-2-7FahVdQ0rYdQxHFBIkKgfYCrcoSHRTsFZ2w-20。
收到ticket之後,應用程式需要驗證ticket。這是通過將ticket 傳遞給一個校驗URL來實現的。校驗URL也是CAS伺服器提供的。
CAS通過校驗路徑獲得了ticket之後,通過內部的資料庫對其進行判斷。如果判斷是有效性,則返回一個NetID給應用程式。
隨後CAS將ticket作廢,並且在客戶端留下一個cookie。
以後其他應用程式就使用這個cookie進行認證(當然通過CAS的客戶端),而不再需要輸入使用者名稱和密碼。
SSO Service:
package com.zhengmenbb; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class LoginServlet */ public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; /** * @see HttpServlet#HttpServlet() */ public LoginServlet() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); String service = request.getParameter("service"); if (username.equals(password)) { //需要加密 Cookie cookie = new Cookie("sso", username); cookie.setPath("/"); response.addCookie(cookie); response.sendRedirect(service); } else { response.sendRedirect("/index.jsp?service=" + service); } } }
Sso client:
package com.zhengmenbb; import java.io.IOException; import java.net.URLEncoder; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * Servlet Filter implementation class CheckSession */ public class CheckSession implements Filter { /** * Default constructor. */ public CheckSession() { // TODO Auto-generated constructor stub } /** * @see Filter#destroy() */ public void destroy() { // TODO Auto-generated method stub } /** * @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain) */ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // TODO Auto-generated method stub // place your code here HttpServletRequest httpServletRequest = (HttpServletRequest)request; HttpServletResponse httpServletResponse = (HttpServletResponse)response; HttpSession session = httpServletRequest.getSession(); String username = (String)session.getAttribute("username"); String url = URLEncoder.encode(httpServletRequest.getRequestURL().toString()); if (username == null) { Cookie [] cookies = httpServletRequest.getCookies(); if (cookies != null) { for(Cookie cookie : cookies) { if (cookie.getName().equals("sso")) { username = cookie.getValue(); break; } } } if (username != null && !username.equals("")) { //use the username login in session.setAttribute("username", username); chain.doFilter(request, response); } else { httpServletResponse.sendRedirect("http://127.0.0.1:8080/sso/index.jsp?service=" + url); } return; } else { chain.doFilter(request, response); } // pass the request along the filter chain } /** * @see Filter#init(FilterConfig) */ public void init(FilterConfig fConfig) throws ServletException { // TODO Auto-generated method stub } }
Sso service:
LoginServlet:
package com.zhengmenbb; import java.io.IOException; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class LoginServlet */ public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; public static Map<String, String> tickets = new HashMap<String, String>(); /** * @see HttpServlet#HttpServlet() */ public LoginServlet() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); String service = request.getParameter("service"); if (username.equals(password)) { //需要加密 Cookie cookie = new Cookie("sso", username); cookie.setPath("/"); response.addCookie(cookie); System.out.println("service" + service); long time = System.currentTimeMillis(); tickets.put(""+time, username); if (service.indexOf("?")>=0) { service = service+ "&ticket=" + time; } else { service = service+ "?ticket=" + time; } response.sendRedirect(service); } else { response.sendRedirect("/index.jsp?service=" + service); } } }
SsoFilter.java
package com.zhengmenbb; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet Filter implementation class SsoFilter */ public class SsoFilter implements Filter { /** * Default constructor. */ public SsoFilter() { // TODO Auto-generated constructor stub } /** * @see Filter#destroy() */ public void destroy() { // TODO Auto-generated method stub } /** * @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain) */ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // TODO Auto-generated method stub // place your code here // pass the request along the filter chain HttpServletRequest httpServletRequest = (HttpServletRequest)request; HttpServletResponse httpServletResponse = (HttpServletResponse)response; String username = ""; Cookie [] cookies = httpServletRequest.getCookies(); if (cookies != null) { for(Cookie cookie : cookies) { if (cookie.getName().equals("sso")) { username = cookie.getValue(); break; } } } String service = request.getParameter("service"); if (username!=null && !username.equals("")) { long time = System.currentTimeMillis(); LoginServlet.tickets.put(""+time, username); if (service.indexOf("?")>=0) { service = service+ "&ticket=" + time; } else { service = service+ "?ticket=" + time; } httpServletResponse.sendRedirect(service); } else { chain.doFilter(request, response); } } /** * @see Filter#init(FilterConfig) */ public void init(FilterConfig fConfig) throws ServletException { // TODO Auto-generated method stub } }
TicketServlet.java:
package com.zhengmenbb; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class TicketServlet */ public class TicketServlet extends HttpServlet { private static final long serialVersionUID = 1L; /** * @see HttpServlet#HttpServlet() */ public TicketServlet() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String ticket = request.getParameter("ticket"); String username = LoginServlet.tickets.get(ticket); LoginServlet.tickets.remove(ticket); PrintWriter out = response.getWriter(); out.write(username); } }
Sso Client:
CheckSession.java
package com.zhengmenbb; import java.io.IOException; import java.net.URLEncoder; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.NameValuePair; import org.apache.commons.httpclient.methods.PostMethod; /** * Servlet Filter implementation class CheckSession */ public class CheckSession implements Filter { /** * Default constructor. */ public CheckSession() { // TODO Auto-generated constructor stub } /** * @see Filter#destroy() */ public void destroy() { // TODO Auto-generated method stub } /** * @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain) */ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // TODO Auto-generated method stub // place your code here HttpServletRequest httpServletRequest = (HttpServletRequest)request; HttpServletResponse httpServletResponse = (HttpServletResponse)response; HttpSession session = httpServletRequest.getSession(); String username = (String)session.getAttribute("username"); String url = URLEncoder.encode(httpServletRequest.getRequestURL().toString()); if (username == null) { String ticket = request.getParameter("ticket"); if (ticket!=null && !ticket.equals("")) { //回撥取得username PostMethod postMethod = new PostMethod("http://127.0.0.1:8080/sso/TicketServlet"); NameValuePair pair = new NameValuePair("ticket", ticket); postMethod.addParameter(pair); HttpClient httpClient = new HttpClient(); httpClient.executeMethod(postMethod); username = postMethod.getResponseBodyAsString(); postMethod.releaseConnection(); System.out.println("username==" + username); if (username!=null && !username.equals("")) { //進行本地驗證 session.setAttribute("username", username); chain.doFilter(request, response); } else { httpServletResponse.sendRedirect("http://127.0.0.1:8080/sso/index.jsp?service=" + url); } } else { httpServletResponse.sendRedirect("http://127.0.0.1:8080/sso/index.jsp?service=" + url); } return; } else { chain.doFilter(request, response); } // pass the request along the filter chain } /** * @see Filter#init(FilterConfig) */ public void init(FilterConfig fConfig) throws ServletException { // TODO Auto-generated method stub } }