基于java config的springSecurity(五)--session并发控制

参考资料:spring-security-reference.pdf的Session Management.特别是Concurrency Control小节.
管理session可以做到:
a.跟踪活跃的session,统计在线人数,显示在线用户(org.springframework.security.core.session.SessionRegistry).
b.控制并发,即一个用户最多可以使用多少个session登录,比如设为1,结果就为,同一个时间里,第二处登录要么不能登录,要么使前一个登录失效(org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy#allowableSessionsExceed)


1.注册自定义的SessionRegistry(通过它可以做到上面的a点)

@Bean
public SessionRegistry sessionRegistry(){
	return new SessionRegistryImpl();
}
2.注册自定义的sessionAuthenticationStrategy
@Bean
public CompositeSessionAuthenticationStrategy sessionAuthenticationStrategy(){
	ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlAuthenticationStrategy=new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
	concurrentSessionControlAuthenticationStrategy.setMaximumSessions(2);
	concurrentSessionControlAuthenticationStrategy.setExceptionIfMaximumExceeded(true);

	SessionFixationProtectionStrategy sessionFixationProtectionStrategy=new SessionFixationProtectionStrategy();

	RegisterSessionAuthenticationStrategy registerSessionStrategy = new RegisterSessionAuthenticationStrategy(sessionRegistry());
	CompositeSessionAuthenticationStrategy sessionAuthenticationStrategy=new CompositeSessionAuthenticationStrategy(
			Arrays.asList(concurrentSessionControlAuthenticationStrategy,sessionFixationProtectionStrategy,registerSessionStrategy));
	return sessionAuthenticationStrategy;
}
ConcurrentSessionControlAuthenticationStrategy控制并发,SessionFixationProtectionStrategy可以防盗session,此处的RegisterSessionAuthenticationStrategy触发了注册新sessin.
3.注册并发Session Filter
@Bean
public ConcurrentSessionFilter concurrentSessionFilter(){
	return new ConcurrentSessionFilter(sessionRegistry(),"/login?expired");
}
4.使用SessionAuthenticationStrategy.
@Bean
public UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception {
	UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter=new UsernamePasswordAuthenticationFilter();
	usernamePasswordAuthenticationFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
	usernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
	return usernamePasswordAuthenticationFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
	http.authorizeRequests().anyRequest().authenticated()
			.and().formLogin().loginPage("/login").failureUrl("/login?error").usernameParameter("username").passwordParameter("password").permitAll()
			.and().logout().logoutUrl("/logout").logoutSuccessUrl("/login?logout").permitAll()
			.and().rememberMe().key("9D119EE5A2B7DAF6B4DC1EF871D0AC3C")
			.and().exceptionHandling().accessDeniedPage("/exception/403");
	http.sessionManagement().sessionAuthenticationStrategy(sessionAuthenticationStrategy()).and().addFilter(concurrentSessionFilter());
}
不要忘记在configure添加了concurrentSessionFilter.
5.在DispatcherServletInitializer注册HttpSessionEventPublisher Listener.让spring security更新有关会话的生命周期.

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
	super.onStartup(servletContext);
	FilterRegistration.Dynamic encodingFilter = servletContext.addFilter("encoding-filter", CharacterEncodingFilter.class);
	encodingFilter.setInitParameter("encoding", "UTF-8");
	encodingFilter.setInitParameter("forceEncoding", "true");
	encodingFilter.setAsyncSupported(true);
	encodingFilter.addMappingForUrlPatterns(null, true, "/*");
	servletContext.addListener(new HttpSessionEventPublisher());
}

6.在spring controller注入SessionRegistry,测试.


再次修改:
上面为了得到SessionRegistry,做法有点复杂了!既然想不出来,得到内部生成的SessionRegistry,那就要想办法把自己自定义的SessionRegistry设置进去了.看了一下org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer.ConcurrencyControlConfigurer#sessionRegistry,发现的确有这个set方法.OK,开始吧.
配置自定义的SessionRegistry.也就是上面的第2,3,4步可以使用下面做法代替就可以了(对于内部Filter用到SessionRegistry会自动注入进去,所以只要在ConcurrencyControlConfigurer配置一下就行了).修改org.exam.config.SecurityConfig#configure(org.springframework.security.config.annotation.web.builders.HttpSecurity)如下:

@Override
protected void configure(HttpSecurity http) throws Exception {
	http.authorizeRequests().anyRequest().authenticated()
		.and().formLogin().loginPage("/login").failureUrl("/login?error").usernameParameter("username").passwordParameter("password").permitAll()
		.and().logout().logoutUrl("/logout").logoutSuccessUrl("/login?logout").permitAll()
		.and().rememberMe().key("9D119EE5A2B7DAF6B4DC1EF871D0AC3C")
		.and().exceptionHandling().accessDeniedPage("/exception/403")
		.and().sessionManagement().maximumSessions(2).expiredUrl("/login?expired").sessionRegistry(sessionRegistry());
}

剩下的第1,5,6步是一样的.


昨天(160421)有人私信我,失效的session 怎么删除啊.
如果没有特殊的修改,org.springframework.security.web.authentication.logout.LogoutFilter#handlers只有一个元素org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler,如果主动logout,就会触发org.springframework.security.web.authentication.logout.LogoutFilter#doFilter,进而调用org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler#logout,从这个方法可以看出别人是怎么处理失效的session的
public void logout(HttpServletRequest request, HttpServletResponse response,
        Authentication authentication) {
    Assert.notNull(request, "HttpServletRequest required");
    if (invalidateHttpSession) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            logger.debug("Invalidating session: " + session.getId());
            session.invalidate();
        }
    }

    if (clearAuthentication) {
        SecurityContext context = SecurityContextHolder.getContext();
        context.setAuthentication(null);
    }

    SecurityContextHolder.clearContext();
}
这里可以看到使session失效,调用SecurityContextHolder.getContext().setAuthentication(null),清理SecurityContext.
从上面知道,spring security登出操作和session过期都会引起session被摧毁.在servlet里,如果注册了HttpSessionListener,如果失效后,那么就会触发javax.servlet.http.HttpSessionListener#sessionDestroyed事件.所以上面的第5步提到要注册HttpSessionEventPublisher Listener(参考资料也有提到),其中基于javaConfig注册的方法,只要在org.exam.config.DispatcherServletInitializer#onStartup添加servletContext.addListener(new HttpSessionEventPublisher());或者servletContext.addListener(HttpSessionEventPublisher.class);其中org.springframework.security.web.session.HttpSessionEventPublisher#sessionDestroyed如下
public void sessionDestroyed(HttpSessionEvent event) {
    HttpSessionDestroyedEvent e = new HttpSessionDestroyedEvent(event.getSession());
    Log log = LogFactory.getLog(LOGGER_NAME);
    if (log.isDebugEnabled()) {
        log.debug("Publishing event: " + e);
    }
    getContext(event.getSession().getServletContext()).publishEvent(e);
}
getContext(event.getSession().getServletContext())得到的是Root ApplicationContext,所以要把SessionRegistryImpl Bean注册到Root ApplicationContext,这样SessionRegistryImpl的onApplicationEvent方法才能接收上面发布的HttpSessionDestroyedEvent事件.
public void onApplicationEvent(SessionDestroyedEvent event) {
    String sessionId = event.getId();
    removeSessionInformation(sessionId);
}
这里就看removeSessionInformation(sessionId);这里就会对SessionRegistryImpl相关信息进会更新.进而通过SessionRegistryImpl获得那些用户登录了,一个用户有多少个SessionInformation都进行了同步.再来讨论getContext(event.getSession().getServletContext())
ApplicationContext getContext(ServletContext servletContext) {
    return SecurityWebApplicationContextUtils.findRequiredWebApplicationContext(servletContext);
}
public static WebApplicationContext findRequiredWebApplicationContext(ServletContext servletContext) {
    WebApplicationContext wac = _findWebApplicationContext(servletContext);
    if (wac == null) {
        throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener registered?");
    }
    return wac;
}
private static WebApplicationContext _findWebApplicationContext(ServletContext sc) {
    //从下面调用看,得到的是Root ApplicationContext,而不是Servlet ApplicationContext
    WebApplicationContext wac = getWebApplicationContext(sc);
    if (wac == null) {
        Enumeration<String> attrNames = sc.getAttributeNames();
        while (attrNames.hasMoreElements()) {
            String attrName = attrNames.nextElement();
            Object attrValue = sc.getAttribute(attrName);
            if (attrValue instanceof WebApplicationContext) {
                if (wac != null) {
                    throw new IllegalStateException("No unique WebApplicationContext found: more than one " +
                            "DispatcherServlet registered with publishContext=true?");
                }
                wac = (WebApplicationContext) attrValue;
            }
        }
    }
    return wac;
}
public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
    return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}
再假设得到的Servlet ApplicationContext,它还有parent(Root ApplicationContext),那么它也会通知Root ApplicationContext下监听SessionDestroyedEvent事件的Bean,(哈哈,但是没有那么多的如果);
但我还要如果用户就想在servlet注册SessionRegistryImpl,我觉得你可以继承HttpSessionEventPublisher,重写getContext方法了

至于session过期,如果想测试,可以去改一下session的有效时期短一点,然后等待观察.下面是我的测试web.xml全部内容

<?xml version="1.0" encoding="UTF-8"?>
<web-app
        xmlns="http://xmlns.jcp.org/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
        metadata-complete="true"
        version="3.1">
    <session-config>
        <session-timeout>3</session-timeout>
    </session-config>
</web-app>

对于用户主动关闭浏览器,据我了解,服务端是没有触发sessionDestroyed的,等待session过期应该是大多数开发者的需求.

关于踢下线功能:使用org.springframework.security.core.session.SessionRegistry#getAllSessions就可以得到某个用户的所有SessionInformation,SessionInformation当然包括sessionId,剩下的问题就是根据sessionId获取session,再调用session.invalidate()就可以完成需求了.但是javax.servlet.http.HttpSessionContext#getSession已过期,并且因为安全原因没有替代方案,所以从servlet api2.1以后的版本,此路是不通的.
spring security提供了org.springframework.security.core.session.SessionInformation#expireNow,它只是标志了一下过期,直到下次用户请求被org.springframework.security.web.session.ConcurrentSessionFilter#doFilter拦截,
HttpSession session = request.getSession(false);
if (session != null) {
    SessionInformation info = sessionRegistry.getSessionInformation(session.getId());
    if (info != null) {
        if (info.isExpired()) {
            // Expired - abort processing
            doLogout(request, response);
            //其它代码忽略
        }
    }
}
这里就会触发了用户登出.还有一种思路,session保存在redis,直接从redis删除某个session数据,详细看org.springframework.session.SessionRepository,不太推荐这么干.

还有SessionRegistryImpl实现的并发控制靠以下两个变量实现的用户在线列表,重启应用这两个实例肯定会销毁,对于分布式应用也会有问题.
/** <principal:Object,SessionIdSet> */
private final ConcurrentMap<Object, Set<String>> principals = new ConcurrentHashMap<Object, Set<String>>();
/** <sessionId:Object,SessionInformation> */
private final Map<String, SessionInformation> sessionIds = new ConcurrentHashMap<String, SessionInformation>();




你可能感兴趣的:(spring,spring,mvc,Security)