单点登录介绍
SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的机制。它是目前比较流行的企业业务整合的解决方案之一。
实现机制
当用户第一次访问应用系统1的时候,因为还没有登录,会被引导到认证系统中进行登录;根据用户提供的登录信息,认证系统进行身份校验,如果通过校验,应该返回给用户一个认证的凭据--token;用户再访问别的应用的时候就会将这个token带上,作为自己认证的凭据,应用系统接受到请求之后会把token送到认证系统进行校验,检查token的合法性。如果通过校验,用户就可以在不用再次登录的情况下访问应用系统2和应用系统3了 。
Token(令牌)
token的意思是“令牌”,是服务端生成的一串字符串,作为客户端进行请求的一个标识。
当用户第一次登录后,服务器生成一个token并将此token返回给客户端,客户端收到token后把它存储起来,可以放在cookie或者Local Storage(本地存储)里。 以后客户端只需带上这个token前来请求数据即可,无需再次带上用户名和密码。
简单token的组成;uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token的前几位以哈希算法压缩成的一定长度的十六进制字符串。为防止token泄露)。
设计token的值可以有以下方式
- 用设备mac地址作为token
- 用sessionid作为token
同域SSO原理分析
实际上,HTTP协议是无状态的,单个系统的会话由服务端Session进行维持,Session保持会话的原理是通过Cookie把sessionId写入浏览器,每次访问都会自动携带全部Cookie,在服务端读取其中的sessionId进行验证实现会话保持。同域下单点登录其实就是手写token代替sessionId进行会话认证。
token的生成
服务端生成token后,将token与user对象存储在Map结构中,token为Key,user对象为value,response.addCookie()生成新的Cookie,名为token,值为token的值。
token过期移除
将服务端的token从Map中移除,再删除浏览器端的名为token的Cookie。
同域SSO案例
流程图如下:
项目架构图如下:
本例演示的是同域SSO
test-sso-auth的访问地址是:http://check.x.com:8080/test-sso-auth
test-sso1的访问地址是:http://demo1.x.com:8080/test-sso1
test-sso2的访问地址是:http://demo2.x.com:8080/test-sso2
为了测试方便,需要在hosts文件中,修改IP与域名的映射关系,如下:
1 127.0.0.1 demo1.x.com 2 127.0.0.1 demo2.x.com 3 127.0.0.1 check.x.com
test-sso-auth认证中心
1、新建一个Maven Web项目作为认证中心,test-sso-auth,参考:【Maven】Eclipse 使用Maven创建Java Web项目
2、新建一个登录Servlet,处理登录请求,LoginServlet.java
1 package com.test.sso.servlet; 2 3 import java.io.IOException; 4 import javax.servlet.ServletException; 5 import javax.servlet.http.Cookie; 6 import javax.servlet.http.HttpServlet; 7 import javax.servlet.http.HttpServletRequest; 8 import javax.servlet.http.HttpServletResponse; 9 10 import com.test.sso.util.SSOUtil; 11 12 /** 13 * Servlet implementation class LoginServlet 14 */ 15 public class LoginServlet extends HttpServlet { 16 private static final long serialVersionUID = 1L; 17 18 /** 19 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) 20 */ 21 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 22 // TODO Auto-generated method stub 23 String username = request.getParameter("username"); 24 String password = request.getParameter("password"); 25 String redirectUrl = request.getParameter("redirectUrl"); 26 if(SSOUtil.doLogin(username, password)) { 27 Cookie cookie = new Cookie("sso", "123456"); 28 // 设置到父域中,所有子域可以使用cookie 29 cookie.setDomain("x.com"); 30 // cookie设置的域的最顶层 31 cookie.setPath("/"); 32 response.addCookie(cookie); 33 response.sendRedirect(redirectUrl); 34 }else { 35 36 //这句话的意思,是让浏览器用utf8来解析返回的数据 37 response.setHeader("Content-type", "text/html;charset=UTF-8"); 38 //这句话的意思,是告诉servlet用UTF-8转码,而不是用默认的ISO8859 39 response.setCharacterEncoding("UTF-8"); 40 response.getWriter().print("登录失败"); 41 } 42 } 43 44 /** 45 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) 46 */ 47 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 48 // TODO Auto-generated method stub 49 doGet(request, response); 50 } 51 52 }
3、新建一个验证Token的Servlet,验证Token的有效性,CheckServlet.java
1 package com.test.sso.servlet; 2 3 import java.io.IOException; 4 import javax.servlet.ServletException; 5 import javax.servlet.http.Cookie; 6 import javax.servlet.http.HttpServlet; 7 import javax.servlet.http.HttpServletRequest; 8 import javax.servlet.http.HttpServletResponse; 9 10 import com.test.sso.util.SSOUtil; 11 12 /** 13 * Servlet implementation class CheckServlet 14 */ 15 public class CheckServlet extends HttpServlet { 16 private static final long serialVersionUID = 1L; 17 18 /** 19 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) 20 */ 21 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 22 23 String username = request.getParameter("username"); 24 String password = request.getParameter("password"); 25 boolean checkLogin = SSOUtil.checkLogin(username, password); 26 27 //这句话的意思,是让浏览器用utf8来解析返回的数据 28 response.setHeader("Content-type", "text/html;charset=UTF-8"); 29 //这句话的意思,是告诉servlet用UTF-8转码,而不是用默认的ISO8859 30 response.setCharacterEncoding("UTF-8"); 31 response.getWriter().print(checkLogin); 32 33 } 34 35 /** 36 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) 37 */ 38 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 39 // TODO Auto-generated method stub 40 doGet(request, response); 41 } 42 43 }
4、编辑一个Util,处理登录,验证token的逻辑,SSOUtil.java
1 package com.test.sso.util; 2 3 import javax.servlet.http.Cookie; 4 import javax.servlet.http.HttpServletRequest; 5 6 public class SSOUtil { 7 8 public static String USERNAME = "admin"; 9 public static String PASSWORD = "123456"; 10 11 public static boolean doLogin(String username, String password) { 12 if (USERNAME.equals(username) && PASSWORD.equals(password)) { 13 return true; 14 } 15 return false; 16 } 17 18 public static boolean checkLogin(String username, String password) { 19 20 if ("sso".equals(username) && "123456".equals(password)) { 21 return true; 22 } 23 return false; 24 } 25 }
5、编辑web.xml文件,注册相应的servlet
1 <?xml version="1.0" encoding="UTF-8"?> 2 <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xmlns="http://java.sun.com/xml/ns/javaee" 4 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 5 id="WebApp_ID" version="3.0"> 6 <display-name>/test-sso-auth</display-name> 7 <servlet> 8 <servlet-name>LoginServlet</servlet-name> 9 <display-name>LoginServlet</display-name> 10 <description></description> 11 <servlet-class>com.test.sso.servlet.LoginServlet</servlet-class> 12 </servlet> 13 <servlet> 14 <servlet-name>CheckServlet</servlet-name> 15 <display-name>CheckServlet</display-name> 16 <description></description> 17 <servlet-class>com.test.sso.servlet.CheckServlet</servlet-class> 18 </servlet> 19 20 <servlet-mapping> 21 <servlet-name>LoginServlet</servlet-name> 22 <url-pattern>/login</url-pattern> 23 </servlet-mapping> 24 <servlet-mapping> 25 <servlet-name>CheckServlet</servlet-name> 26 <url-pattern>/check</url-pattern> 27 </servlet-mapping> 28 <welcome-file-list> 29 <welcome-file>index.jsp</welcome-file> 30 </welcome-file-list> 31 </web-app>
6、编辑统一登录界面,login.jsp
1 <%@ page language="java" contentType="text/html; charset=UTF-8" 2 pageEncoding="UTF-8"%> 3 <html> 4 <body> 5 <h2>统一登录界面</h2> 6 <form action="login" method="POST"> 7 <span>用户名:</span><input type="text" name="username" /> 8 <span>密码:</span><input type="password" name="password" /> 9 <input type="hidden" value="${param.redirectUrl }" name="redirectUrl"/> 10 <input type="submit"/> 11 </form> 12 </body> 13 </html>
test-sso1子系统
1、新建一个Maven Web项目作为子系统,test-sso1,参考:【Maven】Eclipse 使用Maven创建Java Web项目
2、新建一个Servlet,逻辑为,当用户请求时,验证用户是否登录(可以用拦截器做),
已登录情况,直接返回用户请求的界面
未登录情况,跳转到统一登录界面进行登录
1 package com.test.sso.servlet; 2 3 import java.io.IOException; 4 import java.net.URLEncoder; 5 6 import javax.servlet.ServletException; 7 import javax.servlet.http.Cookie; 8 import javax.servlet.http.HttpServlet; 9 import javax.servlet.http.HttpServletRequest; 10 import javax.servlet.http.HttpServletResponse; 11 12 import com.test.sso.util.HttpConnectionUtils; 13 14 /** 15 * Servlet implementation class HomeServlet 16 */ 17 public class Home1Servlet extends HttpServlet { 18 private static final long serialVersionUID = 1L; 19 20 /** 21 * Default constructor. 22 */ 23 public Home1Servlet() { 24 // TODO Auto-generated constructor stub 25 } 26 27 /** 28 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) 29 */ 30 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 31 // TODO Auto-generated method stub 32 // response.getWriter().append("Served at: ").append(request.getContextPath()); 33 // String username = request.getParameter("username"); 34 // String password = request.getParameter("password"); 35 boolean checkLogin = checkLogin(request); 36 if(checkLogin) { 37 // 登录-跳转到主页 38 request.getRequestDispatcher("home1.jsp").forward(request, response); 39 }else { 40 // 未登录-跳转的登录界面 41 String redirectUrl = "http://demo1.x.com:8080/test-sso1/home1"; 42 43 // request.getSession().setAttribute("redirectUrl", redirectUrl); 44 // request.getRequestDispatcher("login.jsp").forward(request, response); 45 46 response.sendRedirect("http://check.x.com:8080/test-sso-auth/login.jsp?redirectUrl=" + URLEncoder.encode(redirectUrl, "UTF-8")); 47 } 48 } 49 50 private boolean checkLogin(HttpServletRequest request) { 51 Cookie[] cookies = request.getCookies(); 52 if(cookies != null) { 53 for (Cookie cookie : cookies) { 54 55 String method = HttpConnectionUtils.getMethod("http://check.x.com:8080/test-sso-auth/check?" + "username=" +cookie.getName() + "&password=" + cookie.getValue()); 56 if("true".equals(method)) { 57 return true; 58 } 59 } 60 } 61 return false; 62 } 63 64 /** 65 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) 66 */ 67 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 68 // TODO Auto-generated method stub 69 doGet(request, response); 70 } 71 72 }
其中代码中使用的http工具类HttpConnectionUtils,参考【JAVA】通过URLConnection/HttpURLConnection发送HTTP请求的方法(一)
3、编辑web.xml文件,注册servlet
1 <?xml version="1.0" encoding="UTF-8"?> 2 <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xmlns="http://java.sun.com/xml/ns/javaee" 4 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 5 id="WebApp_ID" version="3.0"> 6 <display-name>/test-sso1</display-name> 7 <servlet> 8 <servlet-name>Home1Servlet</servlet-name> 9 <display-name>Home1Servlet</display-name> 10 <description></description> 11 <servlet-class>com.test.sso.servlet.Home1Servlet</servlet-class> 12 </servlet> 13 14 <servlet-mapping> 15 <servlet-name>Home1Servlet</servlet-name> 16 <url-pattern>/home1</url-pattern> 17 </servlet-mapping> 18 <welcome-file-list> 19 <welcome-file>index.jsp</welcome-file> 20 </welcome-file-list> 21 </web-app>
4、编辑用户请求界面home1.jsp
1 <%@ page language="java" contentType="text/html; charset=UTF-8" 2 pageEncoding="UTF-8"%> 3 <!DOCTYPE html> 4 <html> 5 <head> 6 <meta charset="UTF-8"> 7 <title>Insert title here</title> 8 </head> 9 <body> 10 <h2>Home 1 JSP</h2> 11 </body> 12 </html>
test-sso2子系统
搭建方式,参考上面的test-sso1子系统
项目演示
将test-sso-auth、test-sso1 和 test-sso2 三个项目发布到tomcat中,并启动tomcat
流程:
1、使用http://demo1.x.com:8080/test-sso1/home1,请求界面
2、未登录,跳转到统一登录界面(http://check.x.com:8080/test-sso-auth/login.jsp),输出用户名/密码:admin/123456
3、登录完成,跳转到http://demo1.x.com:8080/test-sso1/home1界面
4、打开http://demo2.x.com:8080/test-sso2/home2,查看是否完成登录
跨域SSO案例
本例演示的是跨域SSO
test-sso-auth的访问地址是:http://check.x.com:8080/test-sso-auth
test-sso1的访问地址是:http://demo1.a.com:8080/test-sso1
test-sso2的访问地址是:http://demo2.b.com:8080/test-sso2
为了测试方便,需要在hosts文件中,修改IP与域名的映射关系,如下:
1 127.0.0.1 check.x.com 2 127.0.0.1 demo1.a.com 3 127.0.0.1 demo2.b.com
为了方便,本例在上面同域SSO案例的代码上进行修改
test-sso-auth认证中心
修改了LoginServlet.java,登录成功时,回调前没有写入Cookie,而是吧Cookie的值在url中传回给子系统,让子系统做逻辑处理
1 package com.test.sso.servlet; 2 3 import java.io.IOException; 4 import javax.servlet.ServletException; 5 import javax.servlet.http.Cookie; 6 import javax.servlet.http.HttpServlet; 7 import javax.servlet.http.HttpServletRequest; 8 import javax.servlet.http.HttpServletResponse; 9 10 import com.test.sso.util.SSOUtil; 11 12 /** 13 * Servlet implementation class LoginServlet 14 */ 15 public class LoginServlet extends HttpServlet { 16 private static final long serialVersionUID = 1L; 17 18 /** 19 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) 20 */ 21 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 22 // TODO Auto-generated method stub 23 String username = request.getParameter("username"); 24 String password = request.getParameter("password"); 25 String redirectUrl = request.getParameter("redirectUrl"); 26 if(SSOUtil.doLogin(username, password)) { 27 // Cookie cookie = new Cookie("sso", "123456"); 28 // // 设置到父域中,所有子域可以使用cookie 29 // cookie.setDomain("x.com"); 30 // // cookie设置的域的最顶层 31 // cookie.setPath("/"); 32 // response.addCookie(cookie); 33 // response.sendRedirect(redirectUrl); 34 35 response.sendRedirect(redirectUrl + "?cname=sso&cval=123456"); 36 }else { 37 38 //这句话的意思,是让浏览器用utf8来解析返回的数据 39 response.setHeader("Content-type", "text/html;charset=UTF-8"); 40 //这句话的意思,是告诉servlet用UTF-8转码,而不是用默认的ISO8859 41 response.setCharacterEncoding("UTF-8"); 42 response.getWriter().print("登录失败"); 43 } 44 } 45 46 /** 47 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) 48 */ 49 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 50 // TODO Auto-generated method stub 51 doGet(request, response); 52 } 53 54 }
test-sso1子系统
1、修改业务的Servlet,Home1Servlet.java
修改内容:a、接收认证中心在url传过来的Cookie的值,生成Cookie并保持,且传到界面
b、界面上,利用HTML的script标签跨域写Cookie,增加子系统b的Cookie
1 package com.test.sso.servlet; 2 3 import java.io.IOException; 4 import java.net.URLEncoder; 5 6 import javax.servlet.ServletException; 7 import javax.servlet.http.Cookie; 8 import javax.servlet.http.HttpServlet; 9 import javax.servlet.http.HttpServletRequest; 10 import javax.servlet.http.HttpServletResponse; 11 12 import com.test.sso.util.HttpConnectionUtils; 13 14 /** 15 * Servlet implementation class HomeServlet 16 */ 17 public class Home1Servlet extends HttpServlet { 18 private static final long serialVersionUID = 1L; 19 20 /** 21 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) 22 */ 23 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 24 25 // 认证中心回调,验证 26 boolean checkSSO = checkSSO(request, response); 27 boolean checkLogin = checkLogin(request); 28 29 if(checkLogin || checkSSO) { 30 // 登录-跳转到主页 31 request.getRequestDispatcher("home1.jsp").forward(request, response); 32 }else { 33 // 未登录-跳转的登录界面 34 String redirectUrl = "http://demo1.a.com:8080/test-sso1/home1"; 35 36 // request.getSession().setAttribute("redirectUrl", redirectUrl); 37 // request.getRequestDispatcher("login.jsp").forward(request, response); 38 response.sendRedirect("http://check.x.com:8080/test-sso-auth/login.jsp?redirectUrl=" + URLEncoder.encode(redirectUrl, "UTF-8")); 39 } 40 } 41 42 private boolean checkLogin(HttpServletRequest request) { 43 Cookie[] cookies = request.getCookies(); 44 if(cookies != null) { 45 for (Cookie cookie : cookies) { 46 47 String method = HttpConnectionUtils.getMethod("http://check.x.com:8080/test-sso-auth/check?" + "username=" +cookie.getName() + "&password=" + cookie.getValue()); 48 if("true".equals(method)) { 49 request.getSession().setAttribute("canme", cookie.getName()); 50 request.getSession().setAttribute("cval", cookie.getValue()); 51 return true; 52 } 53 } 54 } 55 return false; 56 } 57 58 private boolean checkSSO(HttpServletRequest request, HttpServletResponse response) { 59 String cname = request.getParameter("cname"); 60 String cval = request.getParameter("cval"); 61 if(cname != null && cname.length() > 0 && cval != null && cval.length() > 0) { 62 // 验证 63 String method = HttpConnectionUtils.getMethod("http://check.x.com:8080/test-sso-auth/check?" + "username=" +cname + "&password=" + cval); 64 if("true".equals(method)) { 65 request.getSession().setAttribute("canme", cname); 66 request.getSession().setAttribute("cval", cval); 67 68 Cookie cookie = new Cookie(cname, cval); 69 // cookie设置的域的最顶层 70 cookie.setPath("/"); 71 response.addCookie(cookie); 72 return true; 73 } 74 } 75 return false; 76 } 77 78 /** 79 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) 80 */ 81 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 82 // TODO Auto-generated method stub 83 doGet(request, response); 84 } 85 86 }
2、界面上,利用HTML的script标签跨域写Cookie,增加子系统b的Cookie,服务端增加对应的Servlet服务,AddCookieServlet.java
1 package com.test.sso.servlet; 2 3 import java.io.IOException; 4 import javax.servlet.ServletException; 5 import javax.servlet.http.Cookie; 6 import javax.servlet.http.HttpServlet; 7 import javax.servlet.http.HttpServletRequest; 8 import javax.servlet.http.HttpServletResponse; 9 10 /** 11 * Servlet implementation class AddTokenServlet 12 */ 13 public class AddCookieServlet extends HttpServlet { 14 private static final long serialVersionUID = 1L; 15 16 /** 17 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) 18 */ 19 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 20 String cname = request.getParameter("cname"); 21 String cval = request.getParameter("cval"); 22 Cookie cookie = new Cookie(cname, cval); 23 // cookie设置的域的最顶层 24 cookie.setPath("/"); 25 response.addCookie(cookie); 26 } 27 28 /** 29 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) 30 */ 31 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 32 // TODO Auto-generated method stub 33 doGet(request, response); 34 } 35 36 }
3、修改home1.jsp界面
1 <%@ page language="java" contentType="text/html; charset=UTF-8" 2 pageEncoding="UTF-8"%> 3 <!DOCTYPE html> 4 <html> 5 <head> 6 <meta charset="UTF-8"> 7 <title>Insert title here</title> 8 </head> 9 <body> 10 <h2>Home 1 JSP</h2> 11 </body> 12 13 <script type="text/javascript" src="http://demo2.b.com/addCookie?cname=${params.cname }&cval=${params.cval }"></script> 14 15 </html>
4、在web.xml中注册新增的Servlet
1 <?xml version="1.0" encoding="UTF-8"?> 2 <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xmlns="http://java.sun.com/xml/ns/javaee" 4 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 5 id="WebApp_ID" version="3.0"> 6 <display-name>/test-sso1</display-name> 7 <servlet> 8 <servlet-name>Home1Servlet</servlet-name> 9 <display-name>Home1Servlet</display-name> 10 <description></description> 11 <servlet-class>com.test.sso.servlet.Home1Servlet</servlet-class> 12 </servlet> 13 <servlet> 14 <description> 15 </description> 16 <display-name>AddCookieServlet</display-name> 17 <servlet-name>AddCookieServlet</servlet-name> 18 <servlet-class>com.test.sso.servlet.AddCookieServlet</servlet-class> 19 </servlet> 20 21 <servlet-mapping> 22 <servlet-name>Home1Servlet</servlet-name> 23 <url-pattern>/home1</url-pattern> 24 </servlet-mapping> 25 <servlet-mapping> 26 <servlet-name>AddCookieServlet</servlet-name> 27 <url-pattern>/addCookie</url-pattern> 28 </servlet-mapping> 29 <welcome-file-list> 30 <welcome-file>index.jsp</welcome-file> 31 </welcome-file-list> 32 </web-app>
test-sso2子系统
搭建方式,参考上面的test-sso1子系统
项目演示
将test-sso-auth、test-sso1 和 test-sso2 三个项目发布到tomcat中,并启动tomcat
流程:
1、使用http://demo1.a.com:8080/test-sso1/home1,请求界面
2、未登录,跳转到统一登录界面(http://check.x.com:8080/test-sso-auth/login.jsp),输出用户名/密码:admin/123456
3、登录完成,跳转到http://demo1.a.com:8080/test-sso1/home1界面
4、打开http://demo2.b.com:8080/test-sso2/home2,查看是否完成登录