cookie+redis实现单点登录并保持session有效期

该方案的实现场景是:现有一个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。

这是整个过程的流程图。
cookie+redis实现单点登录并保持session有效期_第1张图片

具体实现需要配置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的刷新也解决啦。

你可能感兴趣的:(Web)