Spring Session产生的sessionid与cookies中的sessionid不一样的问题 && httpOnly 设置不起作用的问题??
背景:
Springboot 2.0 (spring-session-data-redis + spring-boot-starter-web)
需求:
通过cookies中取到的 sessionid 获取到 session
预期效果:
@Autowired
private SessionRepositry sessionRepositry;
...
Session session = sessionRespositry.findById(sessionId);
真实结果: 获取到的session是null, 然而通过 request.getSession(); 可以获取到session, 说明 session是存在的.
问题追踪后发现问题:
cookie中的sessionId 与 session.getId() 不一样!!!
DEBUG:
1. 先看一看SpringSession是如何从Cookie中获取sessionid的! (相关类: org.springframework.session.web.http.DefaultCookieSerializer)
2. 再看一看 useBase64Encoding 的值是啥, 首先看默认值
3. 看看这些配置是在哪里被(赋值)确认的, 一路追踪到 org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration 配置类中
看看 createDefaultCookieSerializer() 是如何实现的
4. 从上面可以得出结论, 我们无法 通过配置文件 中 server.servlet.session.** 来配置 useBase64Encoding. 使 cookie中的 sessionid 与 session.getId() 保持一致
5. 期间发现的另一个问题: 虽然 sessionCookieConfig 有httpOnly相关配置, 但这里并未将配置设入 cookieSerializer 中, 导致配置文件中的 server.servlet.session.cookie.httpOnly = false 不起作用
解决方案:
第一种方案: 通过配置 自定义的 CookieSerializer 来指定配置信息(如果觉得麻烦请直接看第二种方案), 如下
a) 首先因为 SessionCookieConfig 接口中并没有定义 isUseBase64Encoding() 等接口, 导致缺少了部分配置, 所以我 自定义了一个 MySessionCookieConfig 接口继承了 SessionCookieConfig, 并写了一个默认实现 MyDefaultSessionCookieConfig
package com.cardgame.demo.center.config; import javax.servlet.SessionCookieConfig; /** * * 补充 SessionCookie 中未定义的配置项 * @author yjy * 2018-06-15 13:30 */ public interface MySessionCookieConfig extends SessionCookieConfig { String getDomainPattern(); void setDomainPattern(String domainPattern); String getJvmRoute(); void setJvmRoute(String jvmRoute); boolean isUseBase64Encoding(); void setUseBase64Encoding(boolean useBase64Encoding); }
package com.cardgame.demo.center.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * * 涵盖 CookieSerializer 所有配置项 * @author yjy * 2018-06-15 13:31 */ @Component @ConfigurationProperties(prefix = "server.servlet.session.cookie") public class MyDefaultSessionCookieConfig implements MySessionCookieConfig { private String name = "SESSION"; private String path; private String domain; private String comment; private int maxAge = -1; private String domainPattern; private String jvmRoute; private boolean httpOnly = true; private boolean secure = false; private boolean useBase64Encoding = false; @Override public String getDomainPattern() { return domainPattern; } @Override public void setDomainPattern(String domainPattern) { this.domainPattern = domainPattern; } @Override public String getJvmRoute() { return jvmRoute; } @Override public void setJvmRoute(String jvmRoute) { this.jvmRoute = jvmRoute; } @Override public boolean isUseBase64Encoding() { return useBase64Encoding; } @Override public void setUseBase64Encoding(boolean useBase64Encoding) { this.useBase64Encoding = useBase64Encoding; } @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } @Override public String getPath() { return path; } @Override public void setPath(String path) { this.path = path; } @Override public String getDomain() { return domain; } @Override public void setDomain(String domain) { this.domain = domain; } @Override public String getComment() { return comment; } @Override public void setComment(String comment) { this.comment = comment; } @Override public boolean isHttpOnly() { return httpOnly; } @Override public void setHttpOnly(boolean httpOnly) { this.httpOnly = httpOnly; } @Override public boolean isSecure() { return secure; } @Override public void setSecure(boolean secure) { this.secure = secure; } @Override public int getMaxAge() { return maxAge; } @Override public void setMaxAge(int maxAge) { this.maxAge = maxAge; } }
b) 利用 MyDefaultSessionCookieConfig 携带的配置, 自定义 CookieSerializer Bean
package com.cardgame.demo.center.config; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; import org.springframework.session.security.web.authentication.SpringSessionRememberMeServices; import org.springframework.session.web.http.CookieSerializer; import org.springframework.session.web.http.DefaultCookieSerializer; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import javax.servlet.ServletContext; import javax.servlet.SessionCookieConfig; /** * * @author yjy * 2018-06-08 14:53 */ @Slf4j @Configuration @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600, redisNamespace = "center") public class RedisSessionConfig implements ApplicationContextAware { @Bean public CookieSerializer cookieSerializer(ServletContext servletContext, MySessionCookieConfig sessionCookieConfig) { DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer(); if (servletContext != null) { if (sessionCookieConfig != null) { if (sessionCookieConfig.getName() != null) cookieSerializer.setCookieName(sessionCookieConfig.getName()); if (sessionCookieConfig.getDomain() != null) cookieSerializer.setDomainName(sessionCookieConfig.getDomain()); if (sessionCookieConfig.getPath() != null) cookieSerializer.setCookiePath(sessionCookieConfig.getPath()); if (sessionCookieConfig.getMaxAge() != -1) cookieSerializer.setCookieMaxAge(sessionCookieConfig.getMaxAge()); if (sessionCookieConfig.getDomainPattern() != null) cookieSerializer.setDomainNamePattern(sessionCookieConfig.getDomainPattern()); if (sessionCookieConfig.getJvmRoute() != null) cookieSerializer.setJvmRoute(sessionCookieConfig.getJvmRoute()); cookieSerializer.setUseSecureCookie(sessionCookieConfig.isSecure()); cookieSerializer.setUseBase64Encoding(sessionCookieConfig.isUseBase64Encoding()); cookieSerializer.setUseHttpOnlyCookie(sessionCookieConfig.isHttpOnly()); } } if (ClassUtils.isPresent( "org.springframework.security.web.authentication.RememberMeServices", null)) { boolean usesSpringSessionRememberMeServices = !ObjectUtils .isEmpty(this.context .getBeanNamesForType(SpringSessionRememberMeServices.class)); if (usesSpringSessionRememberMeServices) { cookieSerializer.setRememberMeRequestAttribute( SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR); } } return cookieSerializer; } private ApplicationContext context; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context = applicationContext; } }
c) 修改配置文件配置
d) 配置完成后重新启动, 再看 SpringHttpSessionConfiguration 加载的时候, 拿到了我们自定义的 CookieSerializer, 我想要的配置都有了!! 打开浏览器测试通过!!
第二种方案: 拿到 Cookie 中的 sessionId 后, 手动解码, 再 通过 sessionRespositry.findById(sessionId); 获取session
a) 解码的方案 从 DefaultSerializer 类中 copy 一个 , 如下:
/** * Decode the value using Base64. * @param base64Value the Base64 String to decode * @return the Base64 decoded value * @since 1.2.2 */ private String base64Decode(String base64Value) { try { byte[] decodedCookieBytes = Base64.getDecoder().decode(base64Value); return new String(decodedCookieBytes); } catch (Exception e) { return null; } }
b) 获取步骤:
String cookieSessionId = "XXX";
String sessionId = base64Decode(cookieSessionId);
Session session = sessionRespositry.findById(sessionId);
c) 搞定! (此方案未解决 httpOnly 不起效的问题, 如果要解决 httpOnly = false , 请看方案一)