1. 程式人生 > >Spring Boot Spring Security 後臺登出使用者

Spring Boot Spring Security 後臺登出使用者

最近在開發一個基於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操作了。

總結一下,整個過程還是很容易理解的,而且程式碼實現也並不複雜,具體實現不再展示。