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的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--配置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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<%@ 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:

1
2
3
<%@ 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;

1
2
3
4
5
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分流,剩下的走其它后端去。
可以参考:http://www.cnblogs.com/xiaogangqq123/archive/2011/03/04/1971002.html

3.使用jvm-route

参考:https://blog.csdn.net/honghailiang888/article/details/51066411
跟ip hash类似,也并没有真正解决session共享问题。而且将特定会话附属到特定的tomcat上,当该tomcat宕机时,用户的Session也会丢失。

4.使用Redis等NoSQL

这里以Redis为例。

在用户登录成功后,将用户相关信息放入Redis,并设置过期时间;在用户退出登录时从Redis删除;如果会话超时则重新将用户数据放入Redis。

操作Redis的工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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的工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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;

代码参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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

参考:分布式系统session共享方案

代码参考:crossdomain-session