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

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

代码参考:crossdomain-session