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

原作地址:http://blog.csdn.net/xiejx618/article/details/42892951

 

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

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

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. @Bean    
  2.   
  3. public SessionRegistry sessionRegistry(){    
  4.     return new SessionRegistryImpl();    
  5. }    

2.使用session并发管理,并注入上面自定义的SessionRegistry

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. @Override    
  2.   
  3. protected void configure(HttpSecurity http) throws Exception {    
  4.     http.authorizeRequests().anyRequest().authenticated()    
  5.         .and().formLogin().loginPage("/login").failureUrl("/login?error").usernameParameter("username").passwordParameter("password").permitAll()    
  6.         .and().logout().logoutUrl("/logout").logoutSuccessUrl("/login?logout").permitAll()    
  7.         .and().rememberMe().key("9D119EE5A2B7DAF6B4DC1EF871D0AC3C")    
  8.         .and().exceptionHandling().accessDeniedPage("/exception/403")    
  9.         .and().sessionManagement().maximumSessions(2).expiredUrl("/login?expired").sessionRegistry(sessionRegistry());    
  10. }    

3.监听session创建和销毁的HttpSessionListener.让spring security更新有关会话的生命周期,实现上创建的监听只使用销毁事件,至于session创建,security是调用org.springframework.security.core.session.SessionRegistry#registerNewSession
针对servlet管理的session,应使用org.springframework.security.web.session.HttpSessionEventPublisher,方法有多种:
a.重写org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer#enableHttpSessionEventPublisher

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {  
  2.     @Override  
  3.     protected boolean enableHttpSessionEventPublisher() {  
  4.         return true;  
  5.     }  
  6. }  

 

b.在AbstractAnnotationConfigDispatcherServletInitializer的子类DispatcherServletInitializer添加
[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. @Override    
  2. public void onStartup(ServletContext servletContext) throws ServletException {    
  3.     super.onStartup(servletContext);    
  4.     FilterRegistration.Dynamic encodingFilter = servletContext.addFilter("encoding-filter", CharacterEncodingFilter.class);    
  5.     encodingFilter.setInitParameter("encoding", "UTF-8");    
  6.     encodingFilter.setInitParameter("forceEncoding", "true");    
  7.     encodingFilter.setAsyncSupported(true);    
  8.     encodingFilter.addMappingForUrlPatterns(null, false, "/*");    
  9.     servletContext.addListener(new HttpSessionEventPublisher());    
  10. }   
使用springSession,直接向servletContext添加的session销毁监听是没用的,看springSession的文档http://docs.spring.io/spring-session/docs/current/reference/html5/#httpsession-httpsessionlistener,将org.springframework.security.web.session.HttpSessionEventPublisher注册成Bean就可以了.它的底层是对springSession的创建和销毁进行监听,不一样的.
还要注意的是,添加对HttpSessionListener的支持是从spring Session 1.1.0开始的,写这博文的时候,这版本还没出来.所以,以前的源码有问题.
[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. @Configuration  
  2. @EnableRedisHttpSession  
  3. @PropertySource("classpath:config.properties")  
  4. public class HttpSessionConfig {  
  5.     @Resource  
  6.     private Environment env;  
  7.     @Bean  
  8.     public JedisConnectionFactory jedisConnectionFactory() {  
  9.         JedisConnectionFactory connectionFactory = new JedisConnectionFactory();  
  10.         connectionFactory.setHostName(env.getProperty("redis.host"));  
  11.         connectionFactory.setPort(env.getProperty("redis.port",Integer.class));  
  12.         return connectionFactory;  
  13.     }  
  14.     @Bean  
  15.     public HttpSessionEventPublisher httpSessionEventPublisher() {  
  16.         return new HttpSessionEventPublisher();  
  17.     }  
  18. }  
4.在spring controller注入SessionRegistry,测试.

附加session的创建与销毁分析:
至于session的创建比较简单,认证成功后,security直接调用
[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#doFilter{  
  2.     sessionStrategy.onAuthentication(authResult, request, response);  
  3. }  
  4. org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy#onAuthentication{  
  5.     sessionRegistry.registerNewSession(request.getSession().getId(), uthentication.getPrincipal());  
  6. }  
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的
[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public void logout(HttpServletRequest request, HttpServletResponse response,  
  2.         Authentication authentication) {  
  3.     Assert.notNull(request, "HttpServletRequest required");  
  4.     if (invalidateHttpSession) {  
  5.         HttpSession session = request.getSession(false);  
  6.         if (session != null) {  
  7.             logger.debug("Invalidating session: " + session.getId());  
  8.             session.invalidate();  
  9.         }  
  10.     }  
  11.   
  12.     if (clearAuthentication) {  
  13.         SecurityContext context = SecurityContextHolder.getContext();  
  14.         context.setAuthentication(null);  
  15.     }  
  16.   
  17.     SecurityContextHolder.clearContext();  
  18. }  
这里可以看到使session失效,调用SecurityContextHolder.getContext().setAuthentication(null),清理SecurityContext
spring security登出操作和session过期都会引起session被销毁.就会触发org.springframework.security.web.session.HttpSessionEventPublisher#sessionDestroyed事件.源码如下
[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public void sessionDestroyed(HttpSessionEvent event) {  
  2.     HttpSessionDestroyedEvent e = new HttpSessionDestroyedEvent(event.getSession());  
  3.     Log log = LogFactory.getLog(LOGGER_NAME);  
  4.     if (log.isDebugEnabled()) {  
  5.         log.debug("Publishing event: " + e);  
  6.     }  
  7.     getContext(event.getSession().getServletContext()).publishEvent(e);  
  8. }  
getContext(event.getSession().getServletContext())得到的是Root ApplicationContext,所以要把SessionRegistryImpl Bean注册到Root ApplicationContext,这样SessionRegistryImpl的onApplicationEvent方法才能接收上面发布的HttpSessionDestroyedEvent事件.
[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public void onApplicationEvent(SessionDestroyedEvent event) {  
  2.     String sessionId = event.getId();  
  3.     removeSessionInformation(sessionId);  
  4. }  
这里就看removeSessionInformation(sessionId);这里就会对SessionRegistryImpl相关信息进会更新.进而通过SessionRegistryImpl获得那些用户登录了,一个用户有多少个SessionInformation都进行了同步.

再来讨论getContext(event.getSession().getServletContext())
[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. ApplicationContext getContext(ServletContext servletContext) {  
  2.     return SecurityWebApplicationContextUtils.findRequiredWebApplicationContext(servletContext);  
  3. }  
  4. public static WebApplicationContext findRequiredWebApplicationContext(ServletContext servletContext) {  
  5.     WebApplicationContext wac = _findWebApplicationContext(servletContext);  
  6.     if (wac == null) {  
  7.         throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener registered?");  
  8.     }  
  9.     return wac;  
  10. }  
  11. private static WebApplicationContext _findWebApplicationContext(ServletContext sc) {  
  12.     //从下面调用看,得到的是Root ApplicationContext,而不是Servlet ApplicationContext  
  13.     WebApplicationContext wac = getWebApplicationContext(sc);  
  14.     if (wac == null) {  
  15.         Enumeration<String> attrNames = sc.getAttributeNames();  
  16.         while (attrNames.hasMoreElements()) {  
  17.             String attrName = attrNames.nextElement();  
  18.             Object attrValue = sc.getAttribute(attrName);  
  19.             if (attrValue instanceof WebApplicationContext) {  
  20.                 if (wac != null) {  
  21.                     throw new IllegalStateException("No unique WebApplicationContext found: more than one " +  
  22.                             "DispatcherServlet registered with publishContext=true?");  
  23.                 }  
  24.                 wac = (WebApplicationContext) attrValue;  
  25.             }  
  26.         }  
  27.     }  
  28.     return wac;  
  29. }  
  30. public static WebApplicationContext getWebApplicationContext(ServletContext sc) {  
  31.     return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);  
  32. }  
再假设得到的Servlet ApplicationContext,它还有parent(Root ApplicationContext),那么它也会通知Root ApplicationContext下监听SessionDestroyedEvent事件的Bean,(哈哈,但是没有那么多的如果);
但我还要如果用户就想在servlet注册SessionRegistryImpl,我觉得你可以继承HttpSessionEventPublisher,重写getContext方法了

针对于servlet容器的session,至于session过期,如果想测试,可以去改一下session的有效期短一点,然后等待观察.下面是我的测试web.xml全部内容
[html] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <web-app  
  3.         xmlns="http://xmlns.jcp.org/xml/ns/javaee"  
  4.         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  5.         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"  
  6.         metadata-complete="true"  
  7.         version="3.1">  
  8.     <session-config>  
  9.         <session-timeout>3</session-timeout>  
  10.     </session-config>  
  11. </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拦截,
[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. HttpSession session = request.getSession(false);  
  2. if (session != null) {  
  3.     SessionInformation info = sessionRegistry.getSessionInformation(session.getId());  
  4.     if (info != null) {  
  5.         if (info.isExpired()) {  
  6.             // Expired - abort processing  
  7.             doLogout(request, response);  
  8.             //其它代码忽略  
  9.         }  
  10.     }  
  11. }  
这里就会触发了用户登出.还有一种思路,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>();

既然分布式应用也会有问题,这时就要实现自己的SessionRegistry,将session的信息应保存到一个集中的地方进行管理.
posted @ 2016-12-19 21:25  搬砖工的奋斗史  阅读(655)  评论(0编辑  收藏  举报