我正在编写游戏代码(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'被及时触发(会话过期后立即)。坦率地说,我不知道如何以正确和直接的方式实现它。
感谢您的帮助和建议。
答案 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'已正确发布和处理,并且已过期的会话不再存在。