sso单点登录的入门(Session跨域、Spring-Session共享)
1、单点登录,就是多系统,单一位置登录,实现多系统同时登录的一种技术。单点登录一般是用于互相授信的系统,实现单一位置登录,全系统有效的。
注意:区分与三方登录(第三方登录) ,三方登录:某系统,使用其他系统的用户,实现本系统登录的方式。如,在王者荣耀中使用微信或者QQ登录。解决信息孤岛和用户不对等的实现方案。
2、单点登录方案选择:
2.1、方案一、Session跨域(熟悉即可)。
1 所谓Session跨域就是摒弃了系统(Tomcat)提供的Session,而使用自定义的类似Session的机制来保存客户端数据的一种解决方案。 2 如:通过设置cookie的domain来实现cookie的跨域传递。在cookie中传递一个自定义的session_id。这个session_id是客户端的唯一标记。将这个标记作为key,将客户端需要保存的数据作为value,在服务端进行保存(数据库保存或NoSQL保存)。这种机制就是Session的跨域解决。 3 什么跨域: 客户端请求的时候,请求的服务器,不是同一个IP,端口,域名,主机名的时候,都称为跨域。 4 什么是域:在应用模型,一个完整的,有独立访问路径的功能集合称为一个域。如:百度称为一个应用或系统。百度下有若干的域,如:搜索引擎(www.baidu.com),百度贴吧(tie.baidu.com),百度知道(zhidao.baidu.com),百度地图(map.baidu.com)等。域信息,有时也称为多级域名。域的划分: 以IP,端口,域名,主机名为标准,实现划分。 5 localhost / 127.0.0.1 6 7 使用cookie跨域共享,是session跨域的一种解决方案。 8 jsessionid是和servlet绑定的http session的唯一标记。 9 10 cookie应用 - new Cookie("", ""). 11 request.getCookies() -> cookie[] -> 迭代找到需要使用的cookie 12 response.addCookie(). 13 cookie.setDomain() - 为cookie设定有效域范围。 14 cookie.setPath() - 为cookie设定有效URI范围。
代码实现如下所示:
1 package com.bie.controller; 2 3 import java.util.UUID; 4 5 import javax.servlet.http.HttpServletRequest; 6 import javax.servlet.http.HttpServletResponse; 7 8 import org.springframework.stereotype.Controller; 9 import org.springframework.web.bind.annotation.RequestMapping; 10 11 import com.bie.utils.CookieUtils; 12 13 /** 14 * 15 * @author biehl 16 * 17 * 1、单点登录实现方案一,Session跨域实现 18 */ 19 @Controller 20 public class SsoController { 21 22 /** 23 * 请求控制层的方法 24 * 25 * @param request 26 * @param response 27 * @return 28 */ 29 @RequestMapping("/sso") 30 public String test(HttpServletRequest request, HttpServletResponse response) { 31 // JSESSIONID代表了系统中http的唯一标记 32 33 // custom_global_session_id。全局session的id 34 String cookieName = "custom_global_session_id"; 35 String encodeString = "UTF-8"; 36 37 // 获取到cookie的value值 38 String cookieValue = CookieUtils.getCookieValue(request, cookieName, encodeString); 39 40 // 判断cookie的value值是否为空 41 if (null == cookieValue || "".equals(cookieValue.trim())) { 42 System.out.println("无cookie,生成新的cookie数据"); 43 // 生成一个cookie的value值 44 cookieValue = UUID.randomUUID().toString(); 45 } 46 47 // 根据cookieValue访问数据存储,获取客户端数据。 48 // 将生产的cookie的value值设置到cookie中 49 // 参数0代表关闭浏览器自动删除cookie. 50 CookieUtils.setCookie(request, response, cookieName, cookieValue, 0, encodeString); 51 52 return "/ok.jsp"; 53 } 54 55 }
解析域名和设置cookie的方法工具类:
1 package com.bie.utils; 2 3 import java.io.UnsupportedEncodingException; 4 import java.net.URLDecoder; 5 import java.net.URLEncoder; 6 7 import javax.servlet.http.Cookie; 8 import javax.servlet.http.HttpServletRequest; 9 import javax.servlet.http.HttpServletResponse; 10 11 /** 12 * 13 * Cookie 工具类 14 * 15 */ 16 public final class CookieUtils { 17 18 /** 19 * 得到Cookie的值, 不编码 20 * 21 * @param request 22 * @param cookieName 23 * @return 24 */ 25 public static String getCookieValue(HttpServletRequest request, String cookieName) { 26 return getCookieValue(request, cookieName, false); 27 } 28 29 /** 30 * 得到Cookie的值, 31 * 32 * @param request 33 * @param cookieName 34 * @return 35 */ 36 public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) { 37 Cookie[] cookieList = request.getCookies(); 38 if (cookieList == null || cookieName == null) { 39 return null; 40 } 41 String retValue = null; 42 try { 43 for (int i = 0; i < cookieList.length; i++) { 44 if (cookieList[i].getName().equals(cookieName)) { 45 if (isDecoder) { 46 retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8"); 47 } else { 48 retValue = cookieList[i].getValue(); 49 } 50 break; 51 } 52 } 53 } catch (UnsupportedEncodingException e) { 54 e.printStackTrace(); 55 } 56 return retValue; 57 } 58 59 /** 60 * 得到Cookie的值, 61 * 62 * @param request 63 * @param cookieName 64 * @return 65 */ 66 public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) { 67 Cookie[] cookieList = request.getCookies(); 68 if (cookieList == null || cookieName == null) { 69 return null; 70 } 71 String retValue = null; 72 try { 73 for (int i = 0; i < cookieList.length; i++) { 74 if (cookieList[i].getName().equals(cookieName)) { 75 retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString); 76 break; 77 } 78 } 79 } catch (UnsupportedEncodingException e) { 80 e.printStackTrace(); 81 } 82 return retValue; 83 } 84 85 /** 86 * 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码 87 */ 88 public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, 89 String cookieValue) { 90 setCookie(request, response, cookieName, cookieValue, -1); 91 } 92 93 /** 94 * 设置Cookie的值 在指定时间内生效,但不编码 95 */ 96 public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, 97 String cookieValue, int cookieMaxage) { 98 setCookie(request, response, cookieName, cookieValue, cookieMaxage, false); 99 } 100 101 /** 102 * 设置Cookie的值 不设置生效时间,但编码 103 */ 104 public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, 105 String cookieValue, boolean isEncode) { 106 setCookie(request, response, cookieName, cookieValue, -1, isEncode); 107 } 108 109 /** 110 * 设置Cookie的值 在指定时间内生效, 编码参数 111 */ 112 public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, 113 String cookieValue, int cookieMaxage, boolean isEncode) { 114 doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode); 115 } 116 117 /** 118 * 设置Cookie的值 在指定时间内生效, 编码参数(指定编码) 119 */ 120 public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, 121 String cookieValue, int cookieMaxage, String encodeString) { 122 doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString); 123 } 124 125 /** 126 * 删除Cookie带cookie域名 127 */ 128 public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) { 129 doSetCookie(request, response, cookieName, "", -1, false); 130 } 131 132 /** 133 * 设置Cookie的值,并使其在指定时间内生效 134 * 135 * @param request 136 * 请求,请求对象,分析域信息 137 * @param response 138 * 响应 139 * @param cookieName 140 * cookie的名称 141 * @param cookieValue 142 * cookie的值 143 * @param cookieMaxage 144 * cookie生效的最大秒数。不做设定,关闭浏览器就无效了 145 * @param isEncode 146 * 是否需要编码 147 */ 148 private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, 149 String cookieValue, int cookieMaxage, boolean isEncode) { 150 try { 151 // 判断cookie的value值是否等于null 152 if (cookieValue == null) { 153 cookieValue = ""; 154 } else if (isEncode) { 155 // 判断是否需要utf8编码 156 cookieValue = URLEncoder.encode(cookieValue, "utf-8"); 157 } 158 // 创建cookie,最好做非空判断的 159 Cookie cookie = new Cookie(cookieName, cookieValue); 160 if (cookieMaxage > 0) { 161 // 如果cookie生效的最大秒数大于0,就设置这个值 162 cookie.setMaxAge(cookieMaxage); 163 } 164 if (null != request) { 165 // 分析解析域名 166 String domainName = getDomainName(request); 167 // 如果不等于localhost这个值,就设置一个domainName 168 if (!"localhost".equals(domainName)) { 169 // 设置域名的cookie 170 cookie.setDomain(domainName); 171 } 172 } 173 // 从根路径下的后面任意路径地址,cookie都有效 174 cookie.setPath("/"); 175 // response响应写入到客户端即可 176 response.addCookie(cookie); 177 } catch (Exception e) { 178 e.printStackTrace(); 179 } 180 } 181 182 /** 183 * 设置Cookie的值,并使其在指定时间内生效 184 * 185 * @param request 186 * @param response 187 * @param cookieName 188 * @param cookieValue 189 * @param cookieMaxage 190 * cookie生效的最大秒数 191 * @param encodeString 192 */ 193 private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, 194 String cookieValue, int cookieMaxage, String encodeString) { 195 try { 196 if (cookieValue == null) { 197 cookieValue = ""; 198 } else { 199 cookieValue = URLEncoder.encode(cookieValue, encodeString); 200 } 201 Cookie cookie = new Cookie(cookieName, cookieValue); 202 if (cookieMaxage > 0) 203 cookie.setMaxAge(cookieMaxage); 204 if (null != request) { 205 // 根据获取到的request请求,设置域名的cookie 206 String domainName = getDomainName(request); 207 if (!"localhost".equals(domainName)) { 208 // 设定一个域名。cookie就可以实现跨域访问了。 209 cookie.setDomain(domainName); 210 } 211 } 212 cookie.setPath("/"); 213 response.addCookie(cookie); 214 } catch (Exception e) { 215 e.printStackTrace(); 216 } 217 } 218 219 /** 220 * 得到cookie的域名 221 * 222 * @param request 223 * 请求对象,包含了请求的信息 224 * @return 225 */ 226 private static final String getDomainName(HttpServletRequest request) { 227 // 定义一个变量domainName 228 String domainName = null; 229 230 // 获取完整的请求URL地址。请求url,转换为字符串类型 231 String serverName = request.getRequestURL().toString(); 232 // 判断如果请求url地址为空或者为null 233 if (serverName == null || serverName.equals("")) { 234 domainName = ""; 235 } else { 236 // 不为空或者不为null,将域名转换为小写。域名不敏感的。大小写一样 237 serverName = serverName.toLowerCase(); 238 // 判断开始如果以http://开头的 239 if (serverName.startsWith("http://")) { 240 // 截取前七位字符 241 serverName = serverName.substring(7); 242 } else if (serverName.startsWith("https://")) { 243 // 否则如果开始以https://开始的。//截取前八位字符 244 serverName = serverName.substring(8); 245 } 246 // 找到/开始的位置,可以判断end的值是否为-1,如果为-1的话说明没有这个值 247 // 如果存在这个值,找到这个值的位置,否则返回值为-1 248 final int end = serverName.indexOf("/"); 249 // .test.com www.test.com.cn/sso.test.com.cn/.test.com.cn spring.io/xxxx/xxx 250 // 然后截取到0开始到/的位置 251 if (end != -1) { 252 // end等于-1。说明没有/。否则end不等于-1说明有这个值 253 serverName = serverName.substring(0, end); 254 // 这是将\\.是转义为.。然后进行分割操作。 255 final String[] domains = serverName.split("\\."); 256 // 获取到长度 257 int len = domains.length; 258 // 注意,如果是两截域名,一般没有二级域名。比如spring.io/xxx/xxx,都是在spring.io后面/拼接的 259 // 如果是三截域名,都是以第一截为核心分割的。 260 // 如果是两截的就保留。三截以及多截的就 261 // 多截进行匹配域名规则,第一个相当于写*,然后加.拼接后面的域名地址 262 if (len > 3) { 263 // 如果是大于等于3截的,去掉第一个点之前的。留下剩下的域名 264 domainName = "." + domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1]; 265 } else if (len <= 3 && len > 1) { 266 // 如果是2截和3截保留 267 domainName = "." + domains[len - 2] + "." + domains[len - 1]; 268 } else { 269 domainName = serverName; 270 } 271 } 272 } 273 // 如果域名不为空并且有这个: 274 if (domainName != null && domainName.indexOf(":") > 0) { 275 // 将:转义。去掉端口port号,cookie(cookie的domainName)和端口无关。只看域名的。 276 String[] ary = domainName.split("\\:"); 277 domainName = ary[0]; 278 } 279 // 返回域名 280 System.out.println("==============================================" + domainName); 281 return domainName; 282 } 283 284 public static void main(String[] args) { 285 String serverName = "http://www.baidu.com/a"; 286 String domainName = null; 287 // 判断如果请求url地址为空或者为null 288 if (serverName == null || serverName.equals("")) { 289 domainName = ""; 290 } else { 291 // 不为空或者不为null,将域名转换为小写。域名不敏感的。大小写一样 292 serverName = serverName.toLowerCase(); 293 // 判断开始如果以http://开头的 294 if (serverName.startsWith("http://")) { 295 // 截取前七位字符 296 serverName = serverName.substring(7); 297 } else if (serverName.startsWith("https://")) { 298 // 否则如果开始以https://开始的。//截取前八位字符 299 serverName = serverName.substring(8); 300 } 301 // 找到/开始的位置,可以判断end的值是否为-1,如果为-1的话说明没有这个值 302 final int end = serverName.indexOf("/"); 303 // .test.com www.test.com.cn/sso.test.com.cn/.test.com.cn spring.io/xxxx/xxx 304 // 然后截取到0开始到/的位置 305 serverName = serverName.substring(0, end); 306 final String[] domains = serverName.split("\\."); 307 int len = domains.length; 308 if (len > 3) { 309 domainName = "." + domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1]; 310 } else if (len <= 3 && len > 1) { 311 domainName = "." + domains[len - 2] + "." + domains[len - 1]; 312 } else { 313 domainName = serverName; 314 } 315 } 316 317 if (domainName != null && domainName.indexOf(":") > 0) { 318 String[] ary = domainName.split("\\:"); 319 domainName = ary[0]; 320 } 321 } 322 323 }
效果实现如下所示:
C:\Windows\System32\drivers\etc\host配置文件配置一下地址映射。
# sso test
192.168.0.102 www.test.com
192.168.0.102 sso.test.com
192.168.0.102 cart.test.com
域名访问,实现session跨域的效果:
2.2、Spring Session共享( 了解即可)。
1 spring-session技术是spring提供的用于处理集群会话共享的解决方案。spring-session技术是将用户session数据保存到三方存储容器中,如:mysql,redis等。 2 Spring-session技术是解决同域名下的多服务器集群session共享问题的。不能解决跨域session共享问题(如果要解决跨域sesion,就要搭建前端服务器nginx来解决这个问题)。 3 使用要求:
配置一个Spring提供的Filter,实现数据的拦截保存,并转换为spring-session需要的会话对象。必须提供一个数据库的表格信息(由spring-session提供,找spring-session-jdbc.jar/org/springframework/session/jdbc/*.sql,根据具体的数据库找对应的SQL文件,做表格的创建)。 4 spring-session表:保存客户端session对象的表格。 5 spring-session-attributes表:保存客户端session中的attributes属性数据的表格。 6 spring-session框架,是结合Servlet技术中的HTTPSession完成的会话共享机制。在代码中是直接操作HttpSession对象的。
Spring Session共享、图示如下所示:
基于Spring-session的代码实现如下所示:
首先在配置文件中配置spring-session的filter拦截器。
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 5 http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 6 id="WebApp_ID" version="2.5"> 7 8 <display-name>sso-cross-domain</display-name> 9 10 <welcome-file-list> 11 <welcome-file>index.html</welcome-file> 12 </welcome-file-list> 13 14 <!-- 重要:spring-session的filter拦截器 完成数据转换的拦截器,spring提供的filter实现数据的拦截保存并转换为spring-session所需的会话对象中。 --> 15 <filter> 16 <filter-name>springSessionRepositoryFilter</filter-name> 17 <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> 18 </filter> 19 <filter-mapping> 20 <filter-name>springSessionRepositoryFilter</filter-name> 21 <url-pattern>/*</url-pattern> 22 <dispatcher>REQUEST</dispatcher> 23 <dispatcher>ERROR</dispatcher> 24 </filter-mapping> 25 26 <!-- 字符集过滤器 --> 27 <filter> 28 <filter-name>charSetFilter</filter-name> 29 <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> 30 <init-param> 31 <param-name>encoding</param-name> 32 <param-value>UTF-8</param-value> 33 </init-param> 34 </filter> 35 <filter-mapping> 36 <filter-name>charSetFilter</filter-name> 37 <url-pattern>/*</url-pattern> 38 </filter-mapping> 39 40 <!-- 加载springmvc的配置文件 --> 41 <servlet> 42 <servlet-name>mvc</servlet-name> 43 <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> 44 <init-param> 45 <param-name>contextConfigLocation</param-name> 46 <param-value>classpath:applicationContext-mvc.xml</param-value> 47 </init-param> 48 <load-on-startup>1</load-on-startup> 49 </servlet> 50 <servlet-mapping> 51 <servlet-name>mvc</servlet-name> 52 <url-pattern>/</url-pattern> 53 </servlet-mapping> 54 55 56 </web-app>
然后配置提供数据库的表格信息:
mysql文件路径在这里,找到以后运行一下sql即可。/org/springframework/session/jdbc/schema-mysql.sql。
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:mvc="http://www.springframework.org/schema/mvc" 5 xmlns:context="http://www.springframework.org/schema/context" 6 xmlns:dwr="http://directwebremoting.org/schema/spring-dwr/spring-dwr-3.0.xsd" 7 xsi:schemaLocation="http://www.springframework.org/schema/beans 8 http://www.springframework.org/schema/beans/spring-beans.xsd 9 http://www.springframework.org/schema/mvc 10 http://www.springframework.org/schema/mvc/spring-mvc.xsd 11 http://www.springframework.org/schema/context 12 http://www.springframework.org/schema/context/spring-context.xsd"> 13 14 <!-- 指定扫描的包 --> 15 <context:component-scan base-package="com.bie.controller" /> 16 17 <!-- 为SpringMVC配置注解驱动 --> 18 <mvc:annotation-driven /> 19 20 <!-- 为Spring基础容器开启注解配置信息 --> 21 <context:annotation-config /> 22 <!-- 就是用于提供HttpSession数据持久化操作的Bean对象。 23 对象定义后,可以实现数据库相关操作配置,自动的实现HttpSession数据的持久化操作(CRUD) 24 --> 25 <bean class="org.springframework.session.jdbc.config.annotation.web.http.JdbcHttpSessionConfiguration" /> 26 27 28 <bean id="dataSource" 29 class="org.springframework.jdbc.datasource.DriverManagerDataSource"> 30 <property name="url" 31 value="jdbc:mysql://localhost:3306/springsession?useUnicode=true&characterEncoding=UTF8"></property> 32 <property name="username" value="root"></property> 33 <property name="password" value="123456"></property> 34 <property name="driverClassName" 35 value="com.mysql.jdbc.Driver"></property> 36 </bean> 37 38 <!-- 事务管理器。为JdbcHttpSessionConfiguration提供的事务管理器。 --> 39 <bean 40 class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> 41 <constructor-arg ref="dataSource" /> 42 </bean> 43 44 </beans>
简单的控制层代码;
1 package com.bie.controller; 2 3 import javax.servlet.http.HttpServletRequest; 4 import javax.servlet.http.HttpServletResponse; 5 6 import org.springframework.stereotype.Controller; 7 import org.springframework.web.bind.annotation.RequestMapping; 8 9 /** 10 * 11 * @author biehl 12 * 13 * 1、SpringSession 14 * 15 * spring-session表:保存客户端session对象的表格。 16 * spring-session-attributes表:保存客户端session中的attributes属性数据的表格。 17 * spring-session框架,是结合Servlet技术中的HTTPSession完成的会话共享机制。在代码中是直接操作HttpSession对象的。 18 * 19 */ 20 @Controller 21 public class SpringSessionController { 22 23 @RequestMapping("/springSession") 24 public String test(HttpServletRequest request, HttpServletResponse response) { 25 // 获取到attrName属性值 26 Object attrName = request.getSession().getAttribute("attrName"); 27 // 如果获取到的获取到attrName属性值为空 28 if (null == attrName) { 29 // 获取到attrName属性值设置到session中 30 request.getSession().setAttribute("attrName", "attrValue"); 31 } 32 // 后台打印消息 33 System.out.println("80: " + attrName); 34 // 返回到jsp页面 35 return "/ok.jsp"; 36 } 37 38 }
页面效果如下所示:
然后发现,请求数据已经自动入库了,时间到期后自动删除。