前言
需求:如何保证同一个账号保证只有一个在线。(即:我在设备A上先登录账号guest,同时另外一个人在设备B上也登陆账号guest,此时,设备A上的账号将会被挤下线)
思路
- 账号登录成功后,在数据库或redis中查询当前用户绑定的sessionId
- 如果有值,则调用SessionRepository 删除当前session
- 在数据库或redis 记录当前登录账号对应的新的sessionId
步骤
- 在pom.xml引入依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
- 开启spring-session
这里使用内存,存储session,当然也可以使用 redis 或者 数据库
这里就不讲了,redis和数据库存储spring都有实现,请找度娘。
@EnableSpringHttpSession
public class MySessionConfig {
@Bean
public MapSessionRepository sessionRepository() {
return new MapSessionRepository(new HashMap<>());
}
}
- 伪代码,模拟登录成功后,删除旧session
...此处省略登录代码
...登录成功
HttpSession httpSession = request.getSession();
//从数据库查询旧sessionId
String sessionId = userService.getUserSessionId(loginUserId);
//删除 old sessionId
remove(request,sessionId);
//保存user session 数据
request.getSession().setAttribute("user",userInfo);
//保存新的sessionId关系
userService.saveUserSessionId(userId,httpSession.getId());
- 根据sessionId删除session函数
// 删除sessionId 方法
public static boolean remove(HttpServletRequest request , String sessionId) {
HttpSession httpSession = request.getSession();
if (httpSession.getId().equals(sessionId)) {
// 当前session
httpSession.invalidate();
return true;
} else {
// 非当前Session
SessionRepository sessionRepository = (SessionRepository) request.getAttribute(SessionRepositoryFilter.SESSION_REPOSITORY_ATTR);
if (sessionRepository != null) {
Session session = sessionRepository.findById(sessionId);
if (session != null) {
log.debug("删除会话,ID: {}", sessionId);
}
sessionRepository.deleteById(sessionId);
return true;
}
}
return false; // session ID not found
}
原理
- 进入EnableSpringHttpSession
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({ java.lang.annotation.ElementType.TYPE })
@Documented
@Import(SpringHttpSessionConfiguration.class)
@Configuration(proxyBeanMethods = false)
public @interface EnableSpringHttpSession {
}
- 引入了SpringHttpSessionConfiguration 进去看一下
关键看注册了 SessionRepositoryFilter
@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
- 瞅一瞅 SessionRepositoryFilter
主要看SessionRepositoryFilter 的 doFilterInternal 函数 因为父类 doFilter 中有调用它。
@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);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
wrappedRequest.commitSession();
}
}
- 看一下包装类
private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
... 省略不关键代码
@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.markNotNew();
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)"));
}
//这里调用sessionRepository创建session
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(Instant.now());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
...省略不关键代码
}
总结: spring-session 使用过滤器将request对象进行了包装,所以我们拿到的是包装后的request对象,从而我们获取session也是调用的包装对象的getSession函数,所以根据sessionId删除session时可以调用:sessionRepository.deleteById(sessionId);
问题
这里我有点不理解:
在springmvc 集成spring-session时需要在web.xml中配置代理filter
DelegatingFilterProxy并且指定名称为springSessionRepositoryFilter
为什么SessionRepositoryFilter过滤器在spring boot中通过注解@Bean注入到spring容器中,就可以直接在filter的过虑链中执行呢,有知道的博友帮忙解答下。谢谢。
记录一小步,成长一大步。
有问题欢迎大家在评论区讨论。