我正在编写一个游戏(web应用程序)的代码,我想在其中实现以下策略。允许用户登录一次。第二次登录会导致警告消息和一个选择:是离开还是坚持登录。如果用户选择登录,那么他之前的所有会话都将到期。多亏了Spring的安全设施,它工作得很好:
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‘是很重要的(在会话到期后立即触发)。坦率地说,我不知道如何让它以正确和直接的方式发生。
我将非常感谢你的帮助和建议。
发布于 2018-01-06 04:05:42
我自己找到的唯一答案是这样的。在会话中发送下一个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”,过期的会话将不复存在。
https://stackoverflow.com/questions/48101252
复制相似问题