多cookie导致的登录问题

问题如下

  本系统登录用户中心后,用户中心返回一个cookie给浏览器,浏览器携带cookie访问我们后台服务,后台服务通过cookie,从redis里面获取到信息后,向用户中心发送rpc请求,从而进行权限校验。问题如下,用户中心修改配置后,账号登录变成单端登录,A登录a账号,B登录a账号,B把A挤了下来。但是A不知道,继续访问后台服务,报错,没有权限,但是浏览器端有了新的cookie。再次登录,用户中心报错。

分析如下

  2个方案,用户中心分析这个cookie,选择正确的cookie处理,或者本系统在那个情况下不生成这个cookie。

源码解析

  springmvc 请求一个服务,要经过一系列的过滤器链,而我们使用了springsession,那么其中就有一个优先级很高的一个过滤器SessionRepositoryFilter。核心方法为

@Override
protected void doFilterInternal(HttpServletRequest request,
        HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
    request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

    SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
            request, response, this.servletContext);
    SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
            wrappedRequest, response);

    try {
        filterChain.doFilter(wrappedRequest, wrappedResponse);
    }
    finally {
        wrappedRequest.commitSession();
    }
}

  我们注意到,无论如何都要执行的finally代码块。进去观察

private void commitSession() {
        HttpSessionWrapper wrappedSession = getCurrentSession();
        if (wrappedSession == null) {
            if (isInvalidateClientSession()) {
                SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this,
                        this.response);
            }
        }
        else {
            S session = wrappedSession.getSession();
            clearRequestedSessionCache();
            SessionRepositoryFilter.this.sessionRepository.save(session);
            String sessionId = session.getId();
            if (!isRequestedSessionIdValid()
                    || !sessionId.equals(getRequestedSessionId())) {
                SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this,
                        this.response, sessionId);
            }
        }
    }

  最后一行代码会进入CookieHttpSessionIdResolver的setSessionId方法中,我们观察这个方法。

@Override
public void setSessionId(HttpServletRequest request, HttpServletResponse response,
        String sessionId) {
    if (sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {
        return;
    }
    request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId);
    this.cookieSerializer
            .writeCookieValue(new CookieValue(request, response, sessionId));
}

  其会进入DefaultCookieSerializer的writeCookieValue方法里面

@Override
public void writeCookieValue(CookieValue cookieValue) {
    HttpServletRequest request = cookieValue.getRequest();
    HttpServletResponse response = cookieValue.getResponse();

    StringBuilder sb = new StringBuilder();
    sb.append(this.cookieName).append('=');
    String value = getValue(cookieValue);
    if (value != null && value.length() > 0) {
        validateValue(value);
        sb.append(value);
    }
    int maxAge = getMaxAge(cookieValue);
    if (maxAge > -1) {
        sb.append("; Max-Age=").append(cookieValue.getCookieMaxAge());
        OffsetDateTime expires = (maxAge != 0)
                ? OffsetDateTime.now().plusSeconds(maxAge)
                : Instant.EPOCH.atOffset(ZoneOffset.UTC);
        sb.append("; Expires=")
                .append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));
    }
    String domain = getDomainName(request);
    if (domain != null && domain.length() > 0) {
        validateDomain(domain);
        sb.append("; Domain=").append(domain);
    }
    String path = getCookiePath(request);
    if (path != null && path.length() > 0) {
        validatePath(path);
        sb.append("; Path=").append(path);
    }
    if (isSecureCookie(request)) {
        sb.append("; Secure");
    }
    if (this.useHttpOnlyCookie) {
        sb.append("; HttpOnly");
    }
    if (this.sameSite != null) {
        sb.append("; SameSite=").append(this.sameSite);
    }

    response.addHeader("Set-Cookie", sb.toString());
}

  找到了,原来在这里会在response里面写入cookie,在这个方法里面只要我们指定了cookie的name和domain和用户中心传给我们的一样,那么就会解决多cookie问题。

  从系统启动开始看,加了EnableRedisHttpSession这个注解,会采用spring session,点进去里面有个@import注解,引入了RedisHttpSessionConfiguration这个配置类,这个类里面配置引入了很多bean。同时继承了一个SpringHttpSessionConfiguration这个抽象类,在这个抽象类里面有2个方法和我们强相关

@Autowired(required = false)
public void setCookieSerializer(CookieSerializer cookieSerializer) {
    this.cookieSerializer = cookieSerializer;
}

@PostConstruct
public void init() {
    CookieSerializer cookieSerializer = (this.cookieSerializer != null)
            ? this.cookieSerializer
            : createDefaultCookieSerializer();
    this.defaultHttpSessionIdResolver.setCookieSerializer(cookieSerializer);
}

  第一个方法是当容器中存在CookieSerializer实例时,就注入到这个bean中,第二个方法是这个bean执行完构造方法和所有注入后执行的方法,这个方法里面的逻辑如下,如果上下文不存在CookieSerializer这个实例,那么直接new一个。

  那么解决方案就出来。我们可以创建一个bean注入到spring容器里面即可

@Configuration
public class CookieConfiguration {
@Value("${cookie.name}")
private String cookieName;

@Value("${cookie.domain}")
private String domain;

@Bean
public DefaultCookieSerializer defaultCookieSerializer() {
    DefaultCookieSerializer serializer = new DefaultCookieSerializer();
    serializer.setCookieName(cookieName);
    serializer.setDomainName(domain);
    return serializer;
}
}

  如此即可,返回时写cookie时,name和domain就指定了,就会覆盖之前的cookie,不会出现2个cookie了。问题解决。

什么情况下会写入cookie。

  再往上走,会发现

if (!isRequestedSessionIdValid()
                    || !sessionId.equals(getRequestedSessionId())) 

  这个条件下,才会执行写入cookie的情况。我们的上述情况一定在这个里面。主要是第一个条件我们看看,点进去看看

@Override
    public boolean isRequestedSessionIdValid() {
        if (this.requestedSessionIdValid == null) {
            S requestedSession = getRequestedSession();
            if (requestedSession != null) {
                requestedSession.setLastAccessedTime(Instant.now());
            }
            return isRequestedSessionIdValid(requestedSession);
        }
        return this.requestedSessionIdValid;
    }

  这个方法主要和requestedSessionIdValid这个值相关。我们通过eclipse的call功能,找到了只有这个方法才对这个属性做了修改。

@Override
    public HttpSessionWrapper getSession(boolean create) {
        HttpSessionWrapper currentSession = getCurrentSession();
        if (currentSession != null) {
            return currentSession;
        }
        S requestedSession = getRequestedSession();
        if (requestedSession != null) {
            if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
                requestedSession.setLastAccessedTime(Instant.now());
                this.requestedSessionIdValid = true;
                currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
                currentSession.setNew(false);
                setCurrentSession(currentSession);
                return currentSession;
            }
        }
        ........
    }

  我省略了一部分代码。他会根据requestedSession来判断是否设值进去,因此进入方法getRequestedSession。

private S getRequestedSession() {
        if (!this.requestedSessionCached) {
            List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver
                    .resolveSessionIds(this);
            for (String sessionId : sessionIds) {
                if (this.requestedSessionId == null) {
                    this.requestedSessionId = sessionId;
                }
                S session = SessionRepositoryFilter.this.sessionRepository
                        .findById(sessionId);
                if (session != null) {
                    this.requestedSession = session;
                    this.requestedSessionId = sessionId;
                    break;
                }
            }
            this.requestedSessionCached = true;
        }
        return this.requestedSession;
    }

  这里我就不再进入方法内部了,我就直接说这些方法的功能。先获取事先约定好的cookie的value值,然后通过这个value值从redis里面获取对应的hash值,然后组装成session返回给方法,如果不存在,则返回null。最后若是session不存在,那么就会创建一个丢到redis里面去。并且丢到浏览器前端里面。

  因此当这个cookie过期,或者cookie在redis里面不存在时就会写入cookie。