自己動手寫SSO(單點登錄)
SSO在我們的應用中非常常見,例如我們在OA系統登錄了,我們就可以直接進入采購系統,不需要再登錄了,這樣使我們非常方便。現在網上也有很多實現方法,於是乎我也想寫一個看看。我主要用到的是cookie的機制。在此,分享給大家, 同時提供源代碼下載。
進入主題:
工程說明
SSO的實現一般是會有一個SSO Server,也會叫認證中心,同時也會有被認證的系統,如OA系統、采購系統等,他們就相當於SSO Server的client。
為了更形象體現SSO,我寫的SSO是有三個工程:一個SSO Server端口為8081,一個OA系統端口為8082,一個采購系統端口為8083。如圖:
流程介紹
在整個SSO流程當,有兩個流程非常重要,第一個是用戶沒有登錄系統到登錄系統的過程;第二是用戶在一個系統當中已經登錄(例如在OA系統中登錄 了),但又想進入另一個系統(例如進入PRO系統)的過程,如果把這兩個過程搞定了,那麽SSO也就搞定了。我畫了兩幅圖來說明這兩個過程。
先看用戶沒有登錄系統到登錄系統的過程,如圖:
1:用戶通過URL訪問OA系統。
2:在OA系統中的filter發現這個URL沒有ticket(你暫且就把ticket看做是門票),此時就會跳轉到SSO Server。
3:SSO Server中的filter發現該客戶端中的cookie中沒有相應信息,也即是一個沒有登錄的用戶,那麽會跳轉到登錄頁面。
4:用戶在登錄頁面填寫相應信息,然後通過post方式提交到SSO Server中。
5:SSO Server會校驗用戶信息(我為了快,我的校驗方式就是要用戶名為:cloud,同時密碼為:cloud),同時在cookie中放username。
6:將生成ticket和username放到JVMCache中,在實際項目應該放到Memcached中,它的用處等下分析。
7,8:就是在用戶訪問OA系統的URL基礎上加上了一個ticket參數,這樣跳轉到OA系統。
(此時進入OA系統時,filter發現URL是帶ticket的,則filter會根據帶過來的ticket並通過HttpClient的形式去調用SSO Server中的TicektServlet,這樣就會返回用戶名,其實這個用戶名就是從JVMCache拿到的,同時馬上將這個ticket從JVMCache中移除,這樣保證一個ticket只會用一次,然後把返回的用戶名放到session中)
9:session中有了用戶名,說明用戶登錄成功了,則會去本應該返問的servlet。
10,11:將OA系統返回的視圖給用戶。
第二過程,用戶已經登錄成功了,但要訪問另一個系統,如圖:
1:用戶通過URL訪問PRO系統。
2:在PRO系統中的filter發現這個URL沒有ticket,此時就會跳轉到SSO Server。此時,由於用戶登錄了,所以cookie中有相應的信息(例如用戶名),此時SSO Server中的filter會生成一個ticket。
3:將生成的ticket和username放到JVMCache中。
4:就是在用戶訪問PRO系統的URL基礎上加上了一個ticket參數,這樣跳轉到PRO系統。
(此時進入PRO系統時,filter發現URL是帶ticket的,則filter會根據帶過來的ticket並通過HttpClient的形式去調用SSO Server中的TicektServlet,這樣就會返回用戶名,其實這個用戶名就是從JVMCache拿到的,同時馬上將這個ticket從JVMCache中移除,這樣保證一個ticket只會用一次,然後把返回的用戶名放到session中)
5:session中有了用戶名,說明用戶登錄成功了,則會去本應該返問的servlet。
5,7:將PRO系統返回的視圖給用戶。
關鍵代碼
先看SSO Server工程中的代碼:
pom.xml
(註意:端口為8081)
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.cloud.sso.server</groupId> <artifactId>sso-server</artifactId> <packaging>war</packaging> <version>0.0.1-SNAPSHOT</version> <name>sso-server Maven Webapp</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>sso-server</finalName> <plugins> <plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> <version>8.1.9.v20130131</version> <configuration> <stopKey>stop</stopKey> <stopPort>6000</stopPort> <webAppConfig> <contextPath>/sso</contextPath> </webAppConfig> <scanIntervalSeconds>4</scanIntervalSeconds> <connectors> <connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector"> <port>8081</port> <maxIdleTime>60000</maxIdleTime> </connector> </connectors> </configuration> </plugin> </plugins> </build> </project>
SSO Server中的filter
SSOServerFilter.java
package com.cloud.sso.server.filter; 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; import com.cloud.sso.server.JVMCache; public class SSOServerFilter implements Filter { @Override public void destroy() { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; String service = request.getParameter("service"); String ticket = request.getParameter("ticket"); Cookie[] cookies = request.getCookies(); String username = ""; if (null != cookies) { for (Cookie cookie : cookies) { if ("sso".equals(cookie.getName())) { username = cookie.getValue(); break; } } } if (null == service && null != ticket) { filterChain.doFilter(servletRequest, servletResponse); return; } if (null != username && !"".equals(username)) { long time = System.currentTimeMillis(); String timeString = username + time; JVMCache.TICKET_AND_NAME.put(timeString, username); StringBuilder url = new StringBuilder(); url.append(service); if (0 <= service.indexOf("?")) { url.append("&"); } else { url.append("?"); } url.append("ticket=").append(timeString); response.sendRedirect(url.toString()); } else { filterChain.doFilter(servletRequest, servletResponse); } } @Override public void init(FilterConfig arg0) throws ServletException { } }
兩個servlet:
LoginServlet.java
package com.cloud.sso.server.servlet; 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; import com.cloud.sso.server.JVMCache; public class LoginServlet extends HttpServlet { private static final long serialVersionUID = -3170191388656385924L; @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request, response); } @Override 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 ("cloud".equals(username) && "cloud".equals(password)) { Cookie cookie = new Cookie("sso", username); cookie.setPath("/"); response.addCookie(cookie); long time = System.currentTimeMillis(); String timeString = username + time; JVMCache.TICKET_AND_NAME.put(timeString, username); if (null != service) { StringBuilder url = new StringBuilder(); url.append(service); if (0 <= service.indexOf("?")) { url.append("&"); } else { url.append("?"); } url.append("ticket=").append(timeString); response.sendRedirect(url.toString()); } else { response.sendRedirect("/sso/index.jsp"); } } else { response.sendRedirect("/sso/index.jsp?service=" + service); } } }
TicketServlet.java
package com.cloud.sso.server.servlet; 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; import com.cloud.sso.server.JVMCache; public class TicketServlet extends HttpServlet { private static final long serialVersionUID = 5964206637772848290L; @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { super.doGet(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String ticket = request.getParameter("ticket"); String username = JVMCache.TICKET_AND_NAME.get(ticket); JVMCache.TICKET_AND_NAME.remove(ticket); PrintWriter writer = response.getWriter(); writer.write(username); } }
JVMCache.java
package com.cloud.sso.server; import java.util.HashMap; import java.util.Map; public class JVMCache { public static Map<String, String> TICKET_AND_NAME = new HashMap<String, String>(); }
web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:jsp="http://java.sun.com/xml/ns/javaee/jsp" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5"> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <filter> <filter-name>ssoServerFilter</filter-name> <filter-class>com.cloud.sso.server.filter.SSOServerFilter</filter-class> </filter> <filter-mapping> <filter-name>ssoServerFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>login</servlet-name> <servlet-class>com.cloud.sso.server.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>login</servlet-name> <url-pattern>/user/login</url-pattern> </servlet-mapping> <servlet> <servlet-name>ticket</servlet-name> <servlet-class>com.cloud.sso.server.servlet.TicketServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ticket</servlet-name> <url-pattern>/ticket</url-pattern> </servlet-mapping> </web-app>
下面是OA系統中的代碼:
pom.xml
(註意:端口為8082)
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.cloud.sso.oa</groupId> <artifactId>sso-oa</artifactId> <packaging>war</packaging> <version>0.0.1-SNAPSHOT</version> <name>sso-oa Maven Webapp</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> <dependency> <groupId>commons-httpclient</groupId> <artifactId>commons-httpclient</artifactId> <version>3.1</version> </dependency> </dependencies> <build> <finalName>sso-oa</finalName> <plugins> <plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> <version>8.1.9.v20130131</version> <configuration> <stopKey>stop</stopKey> <stopPort>6000</stopPort> <webAppConfig> <contextPath>/oa</contextPath> </webAppConfig> <scanIntervalSeconds>4</scanIntervalSeconds> <connectors> <connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector"> <port>8082</port> <maxIdleTime>60000</maxIdleTime> </connector> </connectors> </configuration> </plugin> </plugins> </build> </project>
OA系統中的filter:
SSOClientFilter.java
package com.cloud.sso.oa.filter; 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.methods.PostMethod; public class SSOClientFilter implements Filter { @Override public void destroy() { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; HttpSession session = request.getSession(); String username = (String) session.getAttribute("username"); String ticket = request.getParameter("ticket"); String url = URLEncoder.encode(request.getRequestURL().toString(), "UTF-8"); if (null == username) { if (null != ticket && !"".equals(ticket)) { PostMethod postMethod = new PostMethod("http://localhost:8081/sso/ticket"); postMethod.addParameter("ticket", ticket); HttpClient httpClient = new HttpClient(); try { httpClient.executeMethod(postMethod); username = postMethod.getResponseBodyAsString(); postMethod.releaseConnection(); } catch (Exception e) { e.printStackTrace(); } if (null != username && !"".equals(username)) { session.setAttribute("username", username); filterChain.doFilter(request, response); } else { response.sendRedirect("http://localhost:8081/sso/index.jsp?service=" + url); } } else { response.sendRedirect("http://localhost:8081/sso/index.jsp?service=" + url); } } else { filterChain.doFilter(request, response); } } @Override public void init(FilterConfig arg0) throws ServletException { } }
OAServlet.java
package com.cloud.sso.oa.servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class OAServlet extends HttpServlet { private static final long serialVersionUID = 3615122544373006252L; @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.getRequestDispatcher("/WEB-INF/jsp/welcome.jsp").forward(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doGet(request, response); } }
web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:jsp="http://java.sun.com/xml/ns/javaee/jsp" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5"> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <filter> <filter-name>ssoClientFilter</filter-name> <filter-class>com.cloud.sso.oa.filter.SSOClientFilter</filter-class> </filter> <filter-mapping> <filter-name>ssoClientFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>list</servlet-name> <servlet-class>com.cloud.sso.oa.servlet.OAServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>list</servlet-name> <url-pattern>/list</url-pattern> </servlet-mapping> </web-app>
下面是PRO系統的代碼:
pom.xml
(註意:端口為8083)
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.cloud.sso.pro</groupId> <artifactId>sso-pro</artifactId> <packaging>war</packaging> <version>0.0.1-SNAPSHOT</version> <name>sso-pro Maven Webapp</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> <dependency> <groupId>commons-httpclient</groupId> <artifactId>commons-httpclient</artifactId> <version>3.1</version> </dependency> </dependencies> <build> <finalName>sso-pro</finalName> <plugins> <plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> <version>8.1.9.v20130131</version> <configuration> <stopKey>stop</stopKey> <stopPort>6000</stopPort> <webAppConfig> <contextPath>/pro</contextPath> </webAppConfig> <scanIntervalSeconds>4</scanIntervalSeconds> <connectors> <connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector"> <port>8083</port> <maxIdleTime>60000</maxIdleTime> </connector> </connectors> </configuration> </plugin> </plugins> </build> </project>
PRO系統中的filter
SSOClientFilter.java
package com.cloud.sso.pro.filter; 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.methods.PostMethod; public class SSOClientFilter implements Filter { @Override public void destroy() { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; HttpSession session = request.getSession(); String username = (String) session.getAttribute("username"); String ticket = request.getParameter("ticket"); String url = URLEncoder.encode(request.getRequestURL().toString(), "UTF-8"); if (null == username) { if (null != ticket && !"".equals(ticket)) { PostMethod postMethod = new PostMethod("http://localhost:8081/sso/ticket"); postMethod.addParameter("ticket", ticket); HttpClient httpClient = new HttpClient(); try { httpClient.executeMethod(postMethod); username = postMethod.getResponseBodyAsString(); postMethod.releaseConnection(); } catch (Exception e) { e.printStackTrace(); } if (null != username && !"".equals(username)) { session.setAttribute("username", username); filterChain.doFilter(request, response); } else { response.sendRedirect("http://localhost:8081/sso/index.jsp?service=" + url); } } else { response.sendRedirect("http://localhost:8081/sso/index.jsp?service=" + url); } } else { filterChain.doFilter(request, response); } } @Override public void init(FilterConfig arg0) throws ServletException { } }
ProServlet.java
package com.cloud.sso.pro.servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class ProServlet extends HttpServlet { private static final long serialVersionUID = -1334093914490423930L; @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.getRequestDispatcher("/WEB-INF/jsp/welcome.jsp").forward(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { super.doPost(request, response); } }
web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:jsp="http://java.sun.com/xml/ns/javaee/jsp" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5"> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <filter> <filter-name>ssoClientFilter</filter-name> <filter-class>com.cloud.sso.pro.filter.SSOClientFilter</filter-class> </filter> <filter-mapping> <filter-name>ssoClientFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>list</servlet-name> <servlet-class>com.cloud.sso.pro.servlet.ProServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>list</servlet-name> <url-pattern>/list</url-pattern> </servlet-mapping> </web-app>
運行結果:
1:分別啟動這三個工程。
2:訪問OA系統,URL:http://localhost:8082/oa/list
3:這樣到登錄頁面,如圖:
4:用戶名為:cloud,密碼為:cloud,點擊登錄則會顯示,如圖:
5:然後去進入PRO系統,URL:http://localhost:8083/pro/list,則就不需要登錄了,直接進入,如圖:
工程源代碼供下載:
- 本文附件下載:
- sso-source.zip (62.1 KB)
自己動手寫SSO(單點登錄)