该方案的实现场景是:现有一个tomcat分布式集群,由nginx来进行负载均衡,需要不同tomcat之间session共享。即用户登录请求经由nginx转发给了一个tomcat后,要求所有的tomcat都有该用户登录的token、session,下次请求转发到其他的tomcat时,仍然认为该用户是登陆状态。
为了解决这个session共享的问题,我使用了cookie来解决。用cookie来存储用户的sessionId,登陆后在服务器的redis缓存中也生成一个由sessionId、User对象组成的键值对,redis缓存每个tomcat均可以读取到。当下次再进行其他请求的时候,就会从cookie里读取出登录的信息和User对象,并且将sessionId和redis缓存中的进行比较,如果相同,就认为已经登录。
这里还要考虑session的有效期,一般是30 min。redis可以设置值的有效期,很好地解决了这个问题。可以在存入时将值的有效期设为30 min。
具体实现需要配置nginx,进行vitual host的配置。这里我的域名暂时还没有备案完成,就修改本地的host文件模拟一下。
在win10的hosts文件中添加:
127.0.0.1 www.mmall.com
将www.mmall.com
这个域名指向本地,用来测试。
首先是nginx的vhost里的文件。
upstream www.mmall.com{
server www.mmall.com:8080 weight=1;
server www.mmall.com:9080 weight=1;
}
server{
listen 80;
autoindex on;
server_name mmall.com www.mmall.com;
access_log g:access.log combined;
index index.html index.htm index.jsp index.php;
location / {
proxy_pass http://www.mmall.com;
add_header Access-Control-Allow-Origin *;
}
}
这里给两个tomcat分配了权重,使得访问本地的时候会负载均衡,请求会分发到不同的tomcat。
这里是Controller中的实现。这里面JsonUtil工具类中的obj2String(),作用是将某个对象转换为json字符串。RedisUtil也是封装的工具类。
/**
* 用户登录
* @param username
* @param password
* @param session
* @return
*/
@RequestMapping(value = "login.do", method = RequestMethod.POST)
@ResponseBody
public ServerResponse<User> login(String username, String password, HttpSession session, HttpServletResponse httpServletResponse){
ServerResponse<User> response = iUserService.login(username,password);
if( response.isSuccess() ){ //若登录成功
//向浏览器cookie写入sessionId
CookieUtil.writeLoginToken(httpServletResponse, session.getId());
//向redis存储sessionId和user对象的json字符串,且设置了有效期
RedisPoolUtil.setEx(session.getId(), JsonUtil.obj2String(response.getData()), Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
}
return response;
}
/**
* 用户登出
* @param httpServletRequest
* @param httpServletResponse
* @return
*/
@RequestMapping(value = "logout.do", method = RequestMethod.POST)
@ResponseBody
public ServerResponse<String> logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse){
String loginToken = CookieUtil.readLoginToken(httpServletRequest);
CookieUtil.delLoginToken(httpServletRequest, httpServletResponse);//删除cookie
RedisPoolUtil.del(loginToken);//删除redis中存储的记录
return ServerResponse.createBySuccess();
}
自定义的CookieUtil工具类,用来对cookie进行读取和写入。注意domain的设置,在tomcat8.5之后开头不要加".",否则会报错。
/**
* Created by makersy on 2019
*/
@Slf4j
public class CookieUtil {
//tomcat8.5之后domain开头不要加 .
private final static String COOKIE_DOMAIN = "mmall.com";
private final static String COOKIE_NAME = "mmall_login_token";
/**
* 读取登录cookie
* @param request
* @return
*/
public static String readLoginToken(HttpServletRequest request) {
Cookie[] cks = request.getCookies();
if (cks != null) {
for (Cookie ck : cks) {
//输出日志
log.info("read cookieName:{}, cookieValue:{}",ck.getName(), ck.getValue());
if (StringUtils.equals(ck.getName(), COOKIE_NAME)) {
log.info("return cookieName:{}, cookieValue:{}", ck.getName(), ck.getValue());
return ck.getValue();
}
}
}
return null;
}
/*
a:A
*/
/**
* 向用户浏览器写入cookie
* @param response
* @param token
*/
public static void writeLoginToken(HttpServletResponse response, String token) {
Cookie ck = new Cookie(COOKIE_NAME, token);
ck.setDomain(COOKIE_DOMAIN);
ck.setPath("/");//代表设置在根目录
ck.setHttpOnly(true);//防止脚本攻击
//cookie存活周期,单位是s
//若是不设置这个,cookie就不会写入硬盘,而是写在内存,只在当前页面有效
ck.setMaxAge(60 * 60 * 24 * 365);//设置有效期1年。若是-1,则是永久
log.info("write cookieName:{}, cookieValue:{}", ck.getName(), ck.getValue());
response.addCookie(ck);
}
/**
* 删除浏览器的cookie
* @param request
* @param response
*/
public static void delLoginToken(HttpServletRequest request, HttpServletResponse response) {
Cookie[] cks = request.getCookies();
if (cks != null) {
for (Cookie ck : cks) {
if (StringUtils.equals(ck.getName(), COOKIE_NAME)) {
ck.setDomain(COOKIE_DOMAIN);
ck.setPath("/");
ck.setMaxAge(0);//0--代表删除此cookie
log.info("del cookieName:{}, cookieValue:{}", ck.getName(), ck.getValue());
//若if命中,返回一个有效期为0的cookie给浏览器,浏览器就会把这个cookie删除掉
response.addCookie(ck);
return;
}
}
}
}
}
到这里,session共享,以及session的有效期都解决掉了。
然而事情还没有完,session的设定是用户无操作30 min后失效,一旦又有对服务器的请求之后,就会刷新session的有效期。这里只是在登录时在redis里面设定了有效期,并没有刷新机制。也就是说,自登陆开始,过30min后登陆失效。
为此,引入了一个过滤器来监控请求,一旦有请求发生就刷新在redis中对应变量的存活时间。
/**
* Created by makersy on 2019
*/
/**
* session过滤器,如果用户有其他的请求,则刷新session的持续时间
*/
public class SessionExpireFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String loginToken = CookieUtil.readLoginToken(httpServletRequest);
if (StringUtils.isNotEmpty(loginToken)) {
//判断loginToken是不是为空或""
//若符合条件,继续取user信息
String userJsonStr = RedisPoolUtil.get(loginToken);
User user = JsonUtil.string2Obj(userJsonStr, User.class);
if (user != null) {
//如果user不为空,则重置session的时间,即调用expire命令
RedisPoolUtil.expire(loginToken, Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
在web.xml中声明一下:
<filter>
<filter-name>sessionExpireFilterfilter-name>
<filter-class>com.mmall.controller.common.SessionExpireFilterfilter-class>
filter>
<filter-mapping>
<filter-name>sessionExpireFilterfilter-name>
<url-pattern>*.dourl-pattern>
filter-mapping>
这样,session的刷新也解决啦。