Session跨域、Spring-Session共享
大约 10 分钟
Session 跨域
- 所谓 Session 跨域就是摒弃了系统(Tomcat)提供的 Session,而使用自定义的类似 Session 的机制来保存客户端数据的一种解决方案。
- 如:通过设置 cookie 的 domain 来实现 cookie 的跨域传递。在 cookie 中传递一个自定义的 session_id。这个 session_id 是客户端的唯一标记。将这个标记作为 key,将客户端需要保存的数据作为 value,在服务端进行保存(数据库保存或 NoSQL 保存)。这种机制就是 Session 的跨域解决。
- 什么跨域: 客户端请求的时候,请求的服务器,不是同一个 IP,端口,域名,主机名的时候,都称为跨域。
- 什么是域:在应用模型,一个完整的,有独立访问路径的功能集合称为一个域。如:百度称为一个应用或系统。百度下有若干的域,如:搜索引擎(www.baidu.com),百度贴吧(tie.baidu.com),百度知道(zhidao.baidu.com),百度地图(map.baidu.com)等。域信息,有时也称为多级域名。域的划分: 以 IP,端口,域名,主机名为标准,实现划分。
- localhost / 127.0.0.1
- 使用 cookie 跨域共享,是 session 跨域的一种解决方案。
- jsessionid 是和 servlet 绑定的 httpsession 的唯一标记。
- cookie 应用 - new Cookie("", "").
- request.getCookies() -> cookie[] -> 迭代找到需要使用的 cookie
- response.addCookie().
- cookie.setDomain() - 为 cookie 设定有效域范围。
- cookie.setPath() - 为 cookie 设定有效 URI 范围。
效果实现如下所示:
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
package com.yix.action;
import com.yix.common.util.CookieUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
/**
* @author wangdx
*/
@Controller
public class SsoController {
/**
* 请求控制层的方法
*
* @param request
* @param response
* @return
*/
@RequestMapping("/sso")
public String test(HttpServletRequest request, HttpServletResponse response) {
// JSESSIONID代表了系统中http的唯一标记
// custom_global_session_id。全局session的id
String cookieName = "custom_global_session_id";
String encodeString = "UTF-8";
// 获取到cookie的value值
String cookieValue = CookieUtils.getCookieValue(request, cookieName, encodeString);
// 判断cookie的value值是否为空
if (null == cookieValue || "".equals(cookieValue.trim())) {
System.out.println("无cookie,生成新的cookie数据");
// 生成一个cookie的value值
cookieValue = UUID.randomUUID().toString();
}
// 根据cookieValue访问数据存储,获取客户端数据。
// 将生产的cookie的value值设置到cookie中
// 参数0代表关闭浏览器自动删除cookie.
CookieUtils.setCookie(request, response, cookieName, cookieValue, 0, encodeString);
return "/ok.jsp";
}
}
package com.yix.common.util;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
/**
* @author wangdx
*/
public final class CookieUtils {
/**
* 得到Cookie的值, 不编码
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName) {
return getCookieValue(request, cookieName, false);
}
/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
if (isDecoder) {
retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
} else {
retValue = cookieList[i].getValue();
}
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/**
* 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue) {
setCookie(request, response, cookieName, cookieValue, -1);
}
/**
* 设置Cookie的值 在指定时间内生效,但不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage) {
setCookie(request, response, cookieName, cookieValue, cookieMaxage, false);
}
/**
* 设置Cookie的值 不设置生效时间,但编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, boolean isEncode) {
setCookie(request, response, cookieName, cookieValue, -1, isEncode);
}
/**
* 设置Cookie的值 在指定时间内生效, 编码参数
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, boolean isEncode) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
}
/**
* 设置Cookie的值 在指定时间内生效, 编码参数(指定编码)
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, String encodeString) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
}
/**
* 删除Cookie带cookie域名
*/
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) {
doSetCookie(request, response, cookieName, "", -1, false);
}
/**
* 设置Cookie的值,并使其在指定时间内生效
*
* @param request 请求,请求对象,分析域信息
* @param response 响应
* @param cookieName cookie的名称
* @param cookieValue cookie的值
* @param cookieMaxage cookie生效的最大秒数。不做设定,关闭浏览器就无效了
* @param isEncode 是否需要编码
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, boolean isEncode) {
try {
// 判断cookie的value值是否等于null
if (cookieValue == null) {
cookieValue = "";
} else if (isEncode) {
// 判断是否需要utf8编码
cookieValue = URLEncoder.encode(cookieValue, "utf-8");
}
// 创建cookie,最好做非空判断的
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0) {
// 如果cookie生效的最大秒数大于0,就设置这个值
cookie.setMaxAge(cookieMaxage);
}
if (null != request) {
// 分析解析域名
String domainName = getDomainName(request);
// 如果不等于localhost这个值,就设置一个domainName
if (!"localhost".equals(domainName)) {
// 设置域名的cookie
cookie.setDomain(domainName);
}
}
// 从根路径下的后面任意路径地址,cookie都有效
cookie.setPath("/");
// response响应写入到客户端即可
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 设置Cookie的值,并使其在指定时间内生效
*
* @param request
* @param response
* @param cookieName
* @param cookieValue
* @param cookieMaxage cookie生效的最大秒数
* @param encodeString
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, String encodeString) {
try {
if (cookieValue == null) {
cookieValue = "";
} else {
cookieValue = URLEncoder.encode(cookieValue, encodeString);
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0)
cookie.setMaxAge(cookieMaxage);
if (null != request) {
// 根据获取到的request请求,设置域名的cookie
String domainName = getDomainName(request);
if (!"localhost".equals(domainName)) {
// 设定一个域名。cookie就可以实现跨域访问了。
cookie.setDomain(domainName);
}
}
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 得到cookie的域名
*
* @param request 请求对象,包含了请求的信息
* @return
*/
private static final String getDomainName(HttpServletRequest request) {
// 定义一个变量domainName
String domainName = null;
// 获取完整的请求URL地址。请求url,转换为字符串类型
String serverName = request.getRequestURL().toString();
// 判断如果请求url地址为空或者为null
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
// 不为空或者不为null,将域名转换为小写。域名不敏感的。大小写一样
serverName = serverName.toLowerCase();
// 判断开始如果以http://开头的
if (serverName.startsWith("http://")) {
// 截取前七位字符
serverName = serverName.substring(7);
} else if (serverName.startsWith("https://")) {
// 否则如果开始以https://开始的。//截取前八位字符
serverName = serverName.substring(8);
}
// 找到/开始的位置,可以判断end的值是否为-1,如果为-1的话说明没有这个值
// 如果存在这个值,找到这个值的位置,否则返回值为-1
final int end = serverName.indexOf("/");
// .test.com www.test.com.cn/sso.test.com.cn/.test.com.cn spring.io/xxxx/xxx
// 然后截取到0开始到/的位置
if (end != -1) {
// end等于-1。说明没有/。否则end不等于-1说明有这个值
serverName = serverName.substring(0, end);
// 这是将\\.是转义为.。然后进行分割操作。
final String[] domains = serverName.split("\\.");
// 获取到长度
int len = domains.length;
// 注意,如果是两截域名,一般没有二级域名。比如spring.io/xxx/xxx,都是在spring.io后面/拼接的
// 如果是三截域名,都是以第一截为核心分割的。
// 如果是两截的就保留。三截以及多截的就
// 多截进行匹配域名规则,第一个相当于写*,然后加.拼接后面的域名地址
if (len > 3) {
// 如果是大于等于3截的,去掉第一个点之前的。留下剩下的域名
domainName = "." + domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
} else if (len <= 3 && len > 1) {
// 如果是2截和3截保留
domainName = "." + domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}
}
// 如果域名不为空并且有这个:
if (domainName != null && domainName.indexOf(":") > 0) {
// 将:转义。去掉端口port号,cookie(cookie的domainName)和端口无关。只看域名的。
String[] ary = domainName.split("\\:");
domainName = ary[0];
}
// 返回域名
System.out.println("==============================================" + domainName);
return domainName;
}
public static void main(String[] args) {
String serverName = "http://www.baidu.com/a";
String domainName = null;
// 判断如果请求url地址为空或者为null
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
// 不为空或者不为null,将域名转换为小写。域名不敏感的。大小写一样
serverName = serverName.toLowerCase();
// 判断开始如果以http://开头的
if (serverName.startsWith("http://")) {
// 截取前七位字符
serverName = serverName.substring(7);
} else if (serverName.startsWith("https://")) {
// 否则如果开始以https://开始的。//截取前八位字符
serverName = serverName.substring(8);
}
// 找到/开始的位置,可以判断end的值是否为-1,如果为-1的话说明没有这个值
final int end = serverName.indexOf("/");
// .test.com www.test.com.cn/sso.test.com.cn/.test.com.cn spring.io/xxxx/xxx
// 然后截取到0开始到/的位置
serverName = serverName.substring(0, end);
final String[] domains = serverName.split("\\.");
int len = domains.length;
if (len > 3) {
domainName = "." + domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
} else if (len <= 3 && len > 1) {
domainName = "." + domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}
if (domainName != null && domainName.indexOf(":") > 0) {
String[] ary = domainName.split("\\:");
domainName = ary[0];
}
}
}
Spring Session 共享
spring-session 技术是 spring 提供的用于处理集群会话共享的解决方案。spring-session 技术是将用户 session 数据保存到三方存储容器中,如:mysql,redis 等。 Spring-session 技术是解决同域名下的多服务器集群 session 共享问题的。不能解决跨域 session 共享问题(如果要解决跨域 sesion,就要搭建前端服务器 nginx 来解决这个题)。 使用要求:
配置一个 Spring 提供的 Filter,实现数据的拦截保存,并转换为 spring-session 需要的会话对象。必须提供一个数据库的表格信息(由 spring-session 提供,spring-session-jdbc.jar/org/springframework/session/jdbc/\*.sql,根据具体的数据库找对应的 SQL 文件,做表格的创建)。
spring-session 表:保存客户端 session 对象的表格。
spring-session-attributes 表:保存客户端 session 中的 attributes 属性数据的表格。
spring-session 框架,是结合 Servlet 技术中的 HTTPSession 完成的会话共享机制。在代码中是直接操作 HttpSession 对象的。

<?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"
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">
<display-name>sso-cross-domain</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
<!-- 重要:spring-session的filter拦截器 完成数据转换的拦截器,spring提供的filter实现数据的拦截保存并转换为spring-session所需的会话对象中。 -->
<filter>
<filter-name>springSessionRepositoryFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
<!-- 字符集过滤器 -->
<filter>
<filter-name>charSetFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>charSetFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 加载springmvc的配置文件 -->
<servlet>
<servlet-name>mvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>mvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:dwr="http://directwebremoting.org/schema/spring-dwr/spring-dwr-3.0.xsd"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 指定扫描的包 -->
<context:component-scan base-package="com.bie.controller" />
<!-- 为SpringMVC配置注解驱动 -->
<mvc:annotation-driven />
<!-- 为Spring基础容器开启注解配置信息 -->
<context:annotation-config />
<!-- 就是用于提供HttpSession数据持久化操作的Bean对象。
对象定义后,可以实现数据库相关操作配置,自动的实现HttpSession数据的持久化操作(CRUD)
-->
<bean class="org.springframework.session.jdbc.config.annotation.web.http.JdbcHttpSessionConfiguration" />
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url"
value="jdbc:mysql://localhost:3306/springsession?useUnicode=true&characterEncoding=UTF8"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
<property name="driverClassName"
value="com.mysql.jdbc.Driver"></property>
</bean>
<!-- 事务管理器。为JdbcHttpSessionConfiguration提供的事务管理器。 -->
<bean
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg ref="dataSource" />
</bean>
</beans>
package com.bie.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
*
* @author biehl
*
* 1、SpringSession
*
* spring-session表:保存客户端session对象的表格。
* spring-session-attributes表:保存客户端session中的attributes属性数据的表格。
* spring-session框架,是结合Servlet技术中的HTTPSession完成的会话共享机制。在代码中是直接操作HttpSession对象的。
*
*/
@Controller
public class SpringSessionController {
@RequestMapping("/springSession")
public String test(HttpServletRequest request, HttpServletResponse response) {
// 获取到attrName属性值
Object attrName = request.getSession().getAttribute("attrName");
// 如果获取到的获取到attrName属性值为空
if (null == attrName) {
// 获取到attrName属性值设置到session中
request.getSession().setAttribute("attrName", "attrValue");
}
// 后台打印消息
System.out.println("80: " + attrName);
// 返回到jsp页面
return "/ok.jsp";
}
}
demo
