原理
在网上找了很多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 } }
来源:CSDN
作者:小飞鹤
链接:https://blog.csdn.net/he90227/article/details/39005083