您的位置:首页 > 编程语言 > Java开发

Spring Boot Spring Security 后台注销用户

2017-12-27 14:17 267 查看
最近在开发一个基于Oauth2的认证服务器,其中有一个需求,就是用户在注销客户端应用的同时,也要注销认证服务器上的登录状态。网上查了一下资料,基本上都是使用SessionRegistry实现的

1.首先配置WebSecurityConfigurerAdapter

@Configuration
@EnableWebSecurity
public class CustomWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public SessionRegistry getSessionRegistry() {
SessionRegistry sessionRegistry = new SessionRegistryImpl();
return sessionRegistry;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/").permitAll().antMatchers("/user/**").hasRole("USER");
http.formLogin().loginPage("/login").defaultSuccessUrl("/user");
http.logout().logoutUrl("/logout").logoutSuccessUrl("/login");
http
.sessionManagement()
.invalidSessionUrl("/login")
.maximumSessions(-1)
.sessionRegistry(getSessionRegistry());
}
}


2.注销的类

@Controller
public class LogoutDemo{
@Autowired
SessionRegistry sessionRegistry;

@Autowired
private CustomUserDetailsService service;

@RequestMapping(value = "/logout_demo")
public void logout() {
UserDetails user = service.loadUserByUsername("admin");
List<SessionInformation> allSessions = sessionRegistry.getAllSessions(user, false);
if (allSessions != null) {
for (int i = 0; i < allSessions.size(); i++) {
SessionInformation sessionInformation = allSessions.get(i);
sessionInformation.getSessionId();
sessionInformation.expireNow();
}
}
}
}


3.产生错误

然而这种实现方式是存在问题的,当java后台将用户注销后,用户在浏览器再次访问,页面显示错误信息,只有一句话。

错误来自ConcurrentSessionFilter的doFilter方法,内容如下:

This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).

然后用户刷新浏览器,就可以重新跳转到登录页面。这绝对是有问题的,严重影响用户体验,而对于我的认证服务来说,这更是致命的。

用户刷新浏览器,然后登录,这之后浏览器会跳转到defaultSuccessUrl,不会重定向到客户端应用的页面。

4.错误分析

开始分析报错的原因,下面是ConcurrentSessionFilter的部分源码

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

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);

String targetUrl = determineExpiredUrl(request, info);

if (targetUrl != null) {
redirectStrategy.sendRedirect(request, response, targetUrl);

return;
}
else {
response.getWriter().print(
"This session has been expired (possibly due to multiple concurrent "
+ "logins being attempted as the same user).");
response.flushBuffer();
}

return;
}
else {
// Non-expired - update last request date/time
sessionRegistry.refreshLastRequest(info.getSessionId());
}
}
}

chain.doFilter(request, response);
}


后台注销用户会导致报错,而正常的用户session过期就不会产生这种错误,正常的session过期,会导致request.getSession(false)方法返回null。

HttpServletRequest的getSession(boolean create)方法有如下说明:

Returns the current HttpSession associated with this request or, if there is no current session and create is true, returns a new session.

If create is false and the request has no valid HttpSession, this method returns null.

To make sure the session is properly maintained, you must call this method before the response is committed. If the container is using cookies to maintain session integrity and is asked to create a new session when the response is committed, an IllegalStateException is thrown.

Parameters:

create true to create a new session for this request if necessary; false to return null if there’s no current session

Returns:

the HttpSession associated with this request or null if create is false and the request has no valid session

5.错误总结

正常的session过期,会使HttpSession的状态为invalid,而SessionInformation的expireNow方法并没有实现这一目的。

我的解决方案:

使用全局的Map存储HttpSession信息,和username与HttpSession的关联信息

private static HashMap<String, HttpSession> SessionIdAndSessionMap = new HashMap<String, HttpSession>();

private static HashMap<String, List<String>> usernameAndSessionIdListMap = new HashMap<String, List<String>>();


①存储Session相关信息

在用户认证成功之后,存储信息到两个map里面,并执行HttpSession的setAttribute方法,插入键值,比如

session.setAttribute(“username”, authResult.getName());

这一键值在销毁session的时候需要使用。

我是把这一步的操作写到了AbstractAuthenticationProcessingFilter里面,属于修改了Spring的源码,这个操作并不好,我暂时没想到更好的办法。

②注销用户

以username为key从usernameAndSessionIdListMap中取sessionId,再以sessionId为key从SessionIdAndSessionMap中取到session,然后执行HttpSession的invalidate方法,注销完成。

③清除map中的废弃值

用户注销之后,map中就不应再保留对应的值,需要对其进行删除,下面通过监听器实现。

首先为启动类上Application添加注解@ServletComponentScan,然后新建MySessionListener,实现HttpSessionListener接口,代码如下:

@WebListener
public class MySessionListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent httpSessionEvent) {
}
@Override
public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
HttpSession session = httpSessionEvent.getSession();
// TODO
}


在sessionDestroyed中获取到session,然后通过getAttribute取到username,这样就得到了两个map的key值,就可以执行map的remove操作了。

总结一下,整个过程还是很容易理解的,而且代码实现也并不复杂,具体实现不再展示。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐