在Spring Security中手动过期(无效)后,会话不会被销毁

时间:2018-01-04 18:16:51

标签: java spring spring-security

我正在编写游戏代码(Web应用程序),我想实现以下策略。允许用户登录一次。第二次登录会产生警告消息并选择:离开或坚持登录。如果用户选择登录,则他之前的所有会话都将过期。由于Spring Security工具,它运行良好:

public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { ... .formLogin() .failureHandler(new SecurityErrorHandler()) .and() .sessionManagement() .maximumSessions(1) .maxSessionsPreventsLogin(true) ...

如果由于maxSessionsPreventsLogin(true)而第二次登录,则抛出异常。然后SecurityErrorHandler捕获它并练习重定向到警告页面:

public class SecurityErrorHandler extends SimpleUrlAuthenticationFailureHandler {

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
    if (exception.getClass().isAssignableFrom(SessionAuthenticationException.class)) {
        request.getRequestDispatcher("/double_login/"+request.getParameterValues("username")[0]).forward(request, response);   //...the rest of the method

直到现在一切正常。如果尽管有警告,用户选择第二次登录(他可能关闭了浏览器而没有注销第一个会话无法管理的话),他按下一个按钮,然后控制器调用特殊服务使之前的会话过期此用户(使用他登录的用户名):

 public void expireUserSessions(String username) {
        for (Object principal : sessionRegistry.getAllPrincipals()) {
            if (principal instanceof User) {
                UserDetails userDetails = (UserDetails) principal;
                if (userDetails.getUsername().equals(username)) {
                    for (SessionInformation information : sessionRegistry.getAllSessions(userDetails, true)) {
                        information.expireNow();
                    }
                }
            }
        }
}

此外还有一个SessionEventListener(由于注销,自然过期或由'information.expireNow()'强制到期而无关紧要)观察会话销毁事件并实现特定逻辑,例如保存用户的持久数据,干净的缓存等)。这个逻辑非常重要。执行此操作的代码如下:

public class SessionEventListener extends HttpSessionEventPublisher {

@Override
public void sessionCreated(HttpSessionEvent event) {
    super.sessionCreated(event);
    event.getSession().setMaxInactiveInterval(60*3);
}

@Override
public void sessionDestroyed(HttpSessionEvent event) {
    String name=null;
    SessionRegistry sessionRegistry = getSessionRegistry(event);
    SessionInformation sessionInfo = (sessionRegistry != null ? sessionRegistry
            .getSessionInformation(event.getSession().getId()) : null);
    UserDetails ud = null;
    if (sessionInfo != null) {
        ud = (UserDetails) sessionInfo.getPrincipal();
    }
    if (ud != null) {
        name=ud.getUsername();
        //OUR IMPORTANT ACTIONS IN CASE OF SESSION CLOSING 
        getAllGames(event).removeByName(name);
    }
    super.sessionDestroyed(event);
}



public AllGames getAllGames(HttpSessionEvent event){
    HttpSession session = event.getSession();
    ApplicationContext ctx =
            WebApplicationContextUtils.
                    getWebApplicationContext(session.getServletContext());
    return (AllGames) ctx.getBean("allGames");
}

public SessionRegistry getSessionRegistry(HttpSessionEvent event){
    HttpSession session = event.getSession();
    ApplicationContext ctx =
            WebApplicationContextUtils.
                    getWebApplicationContext(session.getServletContext());
    return (SessionRegistry) ctx.getBean("sessionRegistry");
}

}

然后地狱发生了。尽管我的期望,'sessionDestroyed'方法的事件不是在会话到期后立即发生,而是仅在用户登录第二次之后(由于他之前的会话在那个时刻到期而被允许,但令我惊讶的是,此前一次会议不是直到现在才被Spring Security摧毁。因此,在服务'getAllGames(event).removeByName(name)'中实现的逻辑(从'sessionDestroyed'调用)发生得太晚了,更糟糕的是,在用户第二次登录之后。它打破了逻辑。

我可以实施不同的解决方法,所以说拐杖。但是,如果有人知道如何直接解决它,我希望你给我建议。

说明。我打电话给'session.invalidate()',但也无济于事。

对于已经体现的逻辑,重要的是SessionEventListener中的'sessionDestroyed'被及时触发(会话过期后立即)。坦率地说,我不知道如何以正确和直接的方式实现它。

感谢您的帮助和建议。

1 个答案:

答案 0 :(得分:0)

我自己发现的唯一答案如下。在此会话中发送下一个HTTP请求之前,仍未销毁过期的会话。因此,要终止过期的会话,您需要代表会话模拟此类请求。我通过创建并调用以下方法来完成它:

 void killExpiredSessionForSure(String sessionID) {

            //sessionID - belongs to a session you want to kill
            HttpHeaders requestHeaders = new HttpHeaders();
            requestHeaders.add("Cookie", "JSESSIONID=" + sessionID);
            HttpEntity requestEntity = new HttpEntity(null, requestHeaders);
            RestTemplate rt = new RestTemplate();
            rt.exchange("http://localhost:8080", HttpMethod.GET, requestEntity, String.class);          
 }

此方法被调用后,事件' sessionDestroyed'已正确发布和处理,并且已过期的会话不再存在。