session 和 cookie 是保存用户状态的两种方式,session在服务端,cookie 在客户端。
本文章将记录 springboot 整合 session 的 demo 示例。
session(会话)
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.session
spring-session-core
org.springframework.session
spring-session-data-redis
org.springframework.boot
spring-boot-starter-test
test
@SpringBootApplication
@RestController
@EnableRedisHttpSession
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(SessionDemoApplication.class, args);
}
@RequestMapping("/hello")
public String printSession(HttpSession session, String name) {
String storedName = (String) session.getAttribute("name");
if (storedName == null) {
session.setAttribute("name", name);
storedName = name;
}
return "hello " + storedName;
}
}
@EnableRedisHttpSession 使用这个注解,开启 httpsession redis 的自动配置
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration
public @interface EnableRedisHttpSession {
}
然后看一下 RedisHttpSessionConfiguration
@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
SchedulingConfigurer {
static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
private String redisNamespace = RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
private RedisFlushMode redisFlushMode = RedisFlushMode.ON_SAVE;
private String cleanupCron = DEFAULT_CLEANUP_CRON;
private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();
private RedisConnectionFactory redisConnectionFactory;
private RedisSerializer
可以看到其实现了对 redis 的一些配置,同时RedisHttpSessionConfiguration 继承了 SpringHttpSessionConfiguration,那么看一下这个类的源码:
@Configuration
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
private final Log logger = LogFactory.getLog(getClass());
private CookieHttpSessionIdResolver defaultHttpSessionIdResolver = new CookieHttpSessionIdResolver();
private boolean usesSpringSessionRememberMeServices;
private ServletContext servletContext;
private CookieSerializer cookieSerializer;
private HttpSessionIdResolver httpSessionIdResolver = this.defaultHttpSessionIdResolver;
private List httpSessionListeners = new ArrayList<>();
@PostConstruct
public void init() {
CookieSerializer cookieSerializer = (this.cookieSerializer != null)
? this.cookieSerializer
: createDefaultCookieSerializer();
this.defaultHttpSessionIdResolver.setCookieSerializer(cookieSerializer);
}
@Bean
public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() {
return new SessionEventHttpSessionListenerAdapter(this.httpSessionListeners);
}
@Bean
public SessionRepositoryFilter extends Session> springSessionRepositoryFilter(
SessionRepository sessionRepository) {
SessionRepositoryFilter sessionRepositoryFilter = new SessionRepositoryFilter<>(
sessionRepository);
sessionRepositoryFilter.setServletContext(this.servletContext);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
......
SpringHttpSessionConfiguration 也是一个配置类,并且其注册了一些 bean,我们看一下这个 过滤器Bean SessionRepositoryFilter :
@Order(SessionRepositoryFilter.DEFAULT_ORDER)
public class SessionRepositoryFilter extends OncePerRequestFilter {
...
@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();
}
}
...
}
abstract class OncePerRequestFilter implements Filter {
}
可以看到 SessionRepositoryFilter 继承了 OncePerRequestFilter,并且实现了 filter 过滤器。
我们看一下 doFilterInternal 这个方法,其对 request 进行了一次封装.
private SessionRepositoryRequestWrapper(HttpServletRequest request,
HttpServletResponse response, ServletContext servletContext) {
super(request);
this.response = response;
this.servletContext = servletContext;
}
@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;
}
}
else {
// This is an invalid session id. No need to ask again if
// request.getSession is invoked for the duration of this request
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
}
setAttribute(INVALID_SESSION_ID_ATTR, "true");
}
if (!create) {
return null;
}
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
+ SESSION_LOGGER_NAME,
new RuntimeException(
"For debugging purposes only (not an error)"));
}
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(Instant.now());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
我们看到 SessionRepositoryRequestWrapper 中还提供了 getSeesion 的方法,其拿 session 的逻辑就在这里,让后我看下 SessionRepositoryFilter.this.sessionRepository.createSession() 这个方法:
点开其实现
@Override
public RedisSession createSession() {
RedisSession redisSession = new RedisSession();
if (this.defaultMaxInactiveInterval != null) {
redisSession.setMaxInactiveInterval(
Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
return redisSession;
}
发现其使用了 redis。到这使用redis 作为 session的流程就基本走完了。
那么我们访问一下
http://localhost:8080/hello?name=spring
我们看到第一次请求中 reponse header 中返回了 session,并且返回结果
hello spring
改一下 name 试一下:
发现 Response header 中没有了 set-cookies,并且请求头 request header 中有了 cookie 信息。并且返回结果没有变:
hello spring
查看一下 redis,我使用的 docker 运行 redis
> docker exec -it redis bash
> redis-cli
> keys *
1) "spring:session:expirations:1560958020000"
2) "spring:session:expirations:1560958140000"
3) "spring:session:sessions:51ffe6c7-9007-4ab9-9e90-0767189e5bf7"
4) "spring:session:sessions:056a46cf-2fbc-4f33-ace3-46d5122177eb"
5) "spring:session:sessions:expires:51ffe6c7-9007-4ab9-9e90-0767189e5bf7"
6) "spring:session:sessions:0fe747b1-691a-4ddf-93f1-8e0a5e538c71"
7) "spring:session:sessions:expires:056a46cf-2fbc-4f33-ace3-46d5122177eb"
8) "spring:session:sessions:expires:0fe747b1-691a-4ddf-93f1-8e0a5e538c71"
> hkeys "spring:session:sessions:0fe747b1-691a-4ddf-93f1-8e0a5e538c71"
1) "sessionAttr:name"
2) "creationTime"
3) "lastAccessedTime"
4) "maxInactiveInterval"