1.Session跨域存在的问题
不同的域名下,Session无法共享。即设定用户在www.a.com登录,后端在Session中放入了用户的username和age,用户从www.a.com跳转到www.b.com,无法获取到Session中的用户信息。
演示:
这里使用一个nginx+2个tomcat来演示。nginx在本机,1台tomcat在本机,另外一台IP为192.168.74.135。
项目结构如下:
添加JSP和servlet的依赖:
<!--配置servlet-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<!--配置jsp jstl的支持-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
页面如下:
session.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@include file="head.jsp"%>
<html>
<head>
<title>Session跨域共享测试</title>
</head>
<body>
<form method="POST">
用户名:<input type="text" name="username" />
年龄:<input type="text" name="age" />
<input type="button" value="创建Session" id="addBtn">
</form>
<hr/>
<input type="button"value="获取Session" id="getSessionBtn">
<textarea rows="10" cols="80"></textarea>
<script src="${ctx}/js/jquery.js"></script>
<script type="text/javascript">
var flag = true;
$('#addBtn').click(function(){
if (!flag) {
alert('操作正在进行中...');
return;
}
flag = false;
var name = $('[name=username]').val();
var age = $('[name=age]').val();
if (name != '' && age != '') {
$.post('${ctx}/session/add',{username:name, age:age},function(r) {
var code = $.parseJSON(r).code;
console.log('code->' + code);
if (code == 0) {
alert('创建session成功!');
}
flag = true;
});
}
else {
alert('缺少参数!');
}
});
$('#getSessionBtn').click(function() {
if (!flag) {
alert('操作正在进行中...');
return;
}
flag = false;
$.get('${ctx}/session/get',function(r) {
$('textarea').html(r);
flag = true;
});
});
</script>
</body>
</html>
head.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:set var="ctx" value="${pageContext.request.contextPath}" />
创建Session的Servlet:
@WebServlet(name = "sessionAddServlet",urlPatterns = "/session/add")
public class SessionAddServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
String age = req.getParameter("age");
System.out.println(String.format("username->%s,age->%s",username,age));
HttpSession session = req.getSession();
session.setAttribute("username",username);
session.setAttribute("age",age);
PrintWriter printWriter = resp.getWriter();
printWriter.write("{\"code\":0}");
printWriter.flush();
printWriter.close();
}
}
获取Session的Servlet:
@WebServlet(name = "getSessionServlet",urlPatterns = "/session/get")
public class GetSessionServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession();
String username = (String) session.getAttribute("username");
String age = (String) session.getAttribute("age");
String ip = req.getRemoteHost();
String userInfo = String.format("{ip:%s,username:%s,age:%s}",ip,username,age);
System.out.println(userInfo);
PrintWriter pw = resp.getWriter();
pw.write(userInfo);
pw.flush();
pw.close();
}
}
将应用达成WAR包分别部署到本机和192.168.74.135的tomcat,并启动它们。
端口信息如下:
192.168.74.135的端口为8080,本机的端口为8081。
nginx配置如下:
upstream server_list {
server 192.168.74.135:8080;
server localhost:8081;
}
server {
listen 8088;
server_name localhost_nginx;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
#root html;
proxy_pass http://server_list;
#index index.html index.htm;
}
...后面省略
}
这里指定nginx监听端口为8088,默认是80。
测试:
1.在浏览器输入http://localhost:8088/crossdomain-session/session.jsp,这样通过nginx转发请求。
a.输入用户名,年龄,然后点击“创建Session”创建Session保存用户名和年龄。
b.点击“获取Session”,
可以看到用户名和年龄有值。
c.再次点击”获取Session”
可以看到,这次请求的IP是192.168.74.135,用户名和年龄都是空。
因为Session是本机IP创建的,所以本机IP可以获取到,而192.168.74.135则无法获取到。
2.使用IP_HASH
在upstream中增加ip_hash;
upstream server_list {
server 192.168.74.135:8080;
server localhost:8081;
ip_hash;
}
重新启动nginx,再多次点击“获取Session”,发现都是本机的请求。nginx根据IP将请求分配给了本机的tomcat,由于Session是本机的tomcat创建的,所以可以获取到。
其实可以看到,多个tomcat并没有共享Session,只是nginx根据IP分发到了固定的tomcat。
弊端:
1.nginx不是最前端的服务器。ip_hash要求nginx一定是最前端的服务器,否则nginx得不到正确ip,就不能根据ip作hash。譬如使用的是squid为最前端,那么nginx取ip时只能得到squid的服务器ip地址,用这个地址来作分流是肯定错乱的。
2.nginx的后端还有其它方式的负载均衡。假如nginx后端又有其它负载均衡,将请求又通过另外的方式分流了,那么某个客户端的请求肯定不能定位到同一台session应用服务器上。这么算起来,nginx后端只能直接指向应用服务器,或者再搭一个squid,然后指向应用服务器。最好的办法是用location作一次分流,将需要session的部分请求通过ip_hash分流,剩下的走其它后端去。
3.使用jvm-route
跟ip hash类似,也并没有真正解决session共享问题。而且将特定会话附属到特定的tomcat上,当该tomcat宕机时,用户的Session也会丢失。
4.使用Redis等NoSQL
这里以Redis为例。
在用户登录成功后,将用户相关信息放入Redis,并设置过期时间;在用户退出登录时从Redis删除;如果会话超时则重新将用户数据放入Redis。
操作Redis的工具类:
public final class RedisUtil {
//Redis服务器IP
private static String ADDR = "192.168.74.135";
//Redis的端口号
private static int PORT = 6379;
//访问密码
private static String AUTH = "system";
//可用连接实例的最大数目,默认值为8;
//如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
private static int MAX_ACTIVE = 1024;
//控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8。
private static int MAX_IDLE = 200;
//等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException;
private static int MAX_WAIT = 10000;
private static int TIMEOUT = 10000;
//在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;
private static boolean TEST_ON_BORROW = true;
private static JedisPool jedisPool = null;
/**
* 初始化Redis连接池
*/
static {
try {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(MAX_ACTIVE);
config.setMaxIdle(MAX_IDLE);
config.setMaxWaitMillis(MAX_WAIT);
config.setTestOnBorrow(TEST_ON_BORROW);
jedisPool = new JedisPool(config, ADDR, PORT, TIMEOUT, AUTH);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取Jedis实例
* @return
*/
public synchronized static Jedis getJedis() {
try {
if (jedisPool != null) {
Jedis resource = jedisPool.getResource();
return resource;
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 释放jedis资源
* @param jedis
*/
public static void returnResource(final Jedis jedis) {
if (jedis != null) {
jedisPool.returnResource(jedis);
}
}
/**
* 获取redis键值-object
*
* @param key
* @return
*/
public static String get(String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String value = jedis.get(key);
return value;
} catch (Exception e) {
System.err.println("getObject获取redis键值异常:key=" + key + " cause:" + e.getMessage());
} finally {
jedis.close();
}
return null;
}
/**
* 设置redis键值-object
* @param key
* @param value
* @return
*/
public static String set(String key, String value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.set(key,value);
} catch (Exception e) {
System.err.println("setObject设置redis键值异常:key=" + key + " value=" + value + " cause:" + e.getMessage());
return null;
} finally {
if(jedis != null)
{
jedis.close();
}
}
}
public static String set(String key, String value,int expiretime) {
String result = "";
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
result = jedis.set(key,value);
if(result.equals("OK")) {
jedis.expire(key.getBytes(), expiretime);
}
return result;
} catch (Exception e) {
System.err.println("setObject设置redis键值异常:key=" + key + " value=" + value + " cause:" + e.getMessage());
} finally {
if(jedis != null)
{
jedis.close();
}
}
return result;
}
/**
* 删除key
*/
public static Long delkey(String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.del(key.getBytes());
}catch(Exception e) {
e.printStackTrace();
return null;
}finally{
if(jedis != null)
{
jedis.close();
}
}
}
public static Boolean existsKey(String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.exists(key.getBytes());
}catch(Exception e) {
e.printStackTrace();
return null;
}finally{
if(jedis != null)
{
jedis.close();
}
}
}
public static Set<String> keys(String keyPattern) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.keys(keyPattern);
}catch(Exception e) {
e.printStackTrace();
return null;
}finally{
if(jedis != null)
{
jedis.close();
}
}
}
}
操作Cookie的工具类:
public final class CookieUtil {
public final static String getCookie(HttpServletRequest request,String cookieName) {
Cookie[] cookies = request.getCookies();
String key = null;
if (null != cookies && cookies.length > 0) {
for (Cookie cookie: cookies) {
if (cookie.getName().equals("sid")) {
key = cookie.getValue();
break;
}
}
}
return key;
}
}
创建Session的Servlet:
@WebServlet(name = "sessionAddServlet",urlPatterns = "/session/add")
public class SessionAddServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
String age = req.getParameter("age");
System.out.println(String.format("username->%s,age->%s",username,age));
String key = CookieUtil.getCookie(req, "sid");
boolean addFlag = null == key || "".equals(key);
if (null != key && !"".equals(key)) {
String sid = RedisUtil.get(key);
addFlag = null == sid || "".equals(sid);
}
if (addFlag) {
key = UUID.randomUUID().toString();
String ip = req.getRemoteHost();
System.out.println("创建Session的IP:" + ip);
String userInfo = String.format("{username:%s,age:%s}",username,age);
// 将要保存到session中的数据写入Redis,有效期30分钟
RedisUtil.set(key,userInfo,30*60*1000);
// 将Session的Key写入到用户浏览器cookie
Cookie cookie = new Cookie("sid",key);
resp.addCookie(cookie);
System.out.println("sid->" + key);
}
PrintWriter printWriter = resp.getWriter();
printWriter.write(String.format("{\"code\":0,\"msg\":\"%s\"}",addFlag ? "Session创建成功!":"Session已存在!"));
printWriter.flush();
printWriter.close();
}
}
获取Session的Servlet:
@WebServlet(name = "getSessionServlet",urlPatterns = "/session/get")
public class GetSessionServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String key = CookieUtil.getCookie(req,"sid");
String userInfo = null;
if (null != key) {
userInfo = RedisUtil.get(key);
System.out.println(userInfo);
}
userInfo = userInfo == null ? "No Session info.":userInfo;
userInfo += "\nIP:"+req.getRemoteHost();
PrintWriter pw = resp.getWriter();
pw.write(userInfo);
pw.flush();
pw.close();
}
}
注意:
1.在创建Session(放入用户数据到Redis)的地方,如果已经有了Session,不能再重复创建。这个上面已经实现;
2.写入用户数据到Redis需要有过期时间(跟Session过期时间一致);
3.用户退出登录时,需要将Redis的数据清空;
5.进阶版
上面使用Redis可以实现Session共享的功能,但是需要程序员去写这些相关代码。其实这些代码对所有需要使用Session共享的应用都是一样的,完全可以抽出来。
比如作为一个单独的依赖,其他应用使用只需要引入该依赖,并进行少量设置即可,相关Session操作跟操作HttpSession没有不同。
- 重写HttpSession,涉及到Session相关的操作全部改为操作Redis;
- 重写HttpServletRequest,因为HttpServletRequest中有创建HttpSession的方法,我们需要改为我们自定义的HttpSession的实现类;
- 定义一个Filter,过滤需要登录的受保护的资源。在doFilter方法中,我们根据ServletRequest和ServletResponse重新构造我们自定义的HttpServletRequest。并调用filterChain.doFilter方法,将ServletRequest包装为我们自己的ServletRequest;
代码参考:
public class HttpSessionImpl implements HttpSession {
private HttpServletRequest request;
private HttpServletResponse response;
private String id;
private long createTime;
private long lastAccessTime;
// session有效期30分钟
private int maxInactiveInterval = 30*60;
private Vector<String> names = new Vector<>();
private final String SESSION_KEY_PREFIX = "session";
public HttpSessionImpl(HttpServletRequest request,HttpServletResponse response) {
this.request = request;
this.response = response;
boolean isExist = false;
// 如果已经有Session就不再创建了,否则会导致每次请求产生新的Session
// 从请求头获取sid
String sid = CookieUtil.getCookie(request,"sid");
if (null != sid && !"".equals(sid)) {
// 检查Redis是否存在该Key
Set<String> keys = RedisUtil.keys(SESSION_KEY_PREFIX+":"+sid+":*");
if (null != keys && !keys.isEmpty()) {
isExist = true;
this.id = sid;
}
}
if (!isExist) {
this.id = sid != null &&!"".equals(sid) ? sid : UUID.randomUUID().toString();
this.createTime = System.currentTimeMillis();
if (null == sid || "".equals(sid)) {
Cookie cookie = new Cookie("sid",this.id);
response.addCookie(cookie);
}
}
}
@Override
public long getCreationTime() {
return createTime;
}
@Override
public String getId() {
return id;
}
@Override
public long getLastAccessedTime() {
return lastAccessTime;
}
@Override
public ServletContext getServletContext() {
return request.getServletContext();
}
@Override
public void setMaxInactiveInterval(int i) {
this.maxInactiveInterval = i;
}
@Override
public int getMaxInactiveInterval() {
return maxInactiveInterval;
}
@Override
public HttpSessionContext getSessionContext() {
return null;
}
@Override
public Object getAttribute(String s) {
this.lastAccessTime = System.currentTimeMillis();
String key = SESSION_KEY_PREFIX + ":" + id + ":" + s;
return RedisUtil.get(key);
}
@Override
public Object getValue(String s) {
return this.getAttribute(s);
}
@Override
public Enumeration<String> getAttributeNames() {
return names.elements();
}
@Override
public String[] getValueNames() {
return new String[0];
}
@Override
public void setAttribute(String s, Object o) {
String key = SESSION_KEY_PREFIX + ":" + id + ":" + s;
// 这里RedisUtil要实现保存对象,即byte[]的功能。
//RedisUtil.set(s,o,maxInactiaveInterval);
// 这里测试,假定保存的value是String类型
RedisUtil.set(key, (String) o, maxInactiveInterval);
this.lastAccessTime = System.currentTimeMillis();
this.names.add(s);
}
@Override
public void putValue(String s, Object o) {
}
@Override
public void removeAttribute(String s) {
String key = SESSION_KEY_PREFIX + ":" + id + ":" + s;
RedisUtil.delkey(key);
this.names.remove(s);
}
@Override
public void removeValue(String s) {
}
@Override
public void invalidate() {
// session过期时,从Redis删除相关数据
String key;
for (String name : names) {
key = SESSION_KEY_PREFIX + ":" + id + ":" + name;
RedisUtil.delkey(key);
}
}
@Override
public boolean isNew() {
return false;
}
}
public class TommyHttpServletRequest extends HttpServletRequestWrapper {
private HttpServletRequest request;
private HttpServletResponse response;
private HttpSessionImpl session;
public TommyHttpServletRequest(HttpServletRequest request,HttpServletResponse response) {
super(request);
this.request = request;
this.response = response;
}
@Override
public HttpSession getSession() {
return this.getSession(true);
}
@Override
public HttpSession getSession(boolean create) {
if (create && this.session == null) {
this.session = new HttpSessionImpl(request,response);
}
return session;
}
}
@WebFilter(filterName = "sessionFilter",urlPatterns = "/*")
public class SessionFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
TommyHttpServletRequest request = new TommyHttpServletRequest((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
filterChain.doFilter(request,servletResponse);
}
@Override
public void destroy() {
}
}
可以将上面的当初抽取出来作为一个依赖,其他应用引入该依赖,并配置Redis和Filter即可。
6.使用Spring Session
代码参考:crossdomain-session