You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Keycloak的JEE应用:如何通过OIDC Backchannel Logout及REST调用销毁用户浏览器会话

解决Keycloak Backchannel Logout下的JEE会话失效问题

我之前在处理JEE应用结合Keycloak做SSO的场景时,刚好碰到过一模一样的问题,给你分享两个核心疑问的可行解决方案:


1. 处理Backchannel Logout时定位并失效用户浏览器会话

Servlet容器本身并没有提供直接根据用户ID查找会话的API,所以我们需要自己维护一个用户ID与HttpSession的映射关系,具体步骤如下:

- 维护全局会话映射

首先定义一个线程安全的全局存储,用来关联用户唯一标识(也就是LogoutToken里的sub字段)和对应的HttpSession集合:

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.servlet.http.HttpSession;

public class SessionRegistry {
    private static final ConcurrentMap<String, Set<HttpSession>> USER_SESSIONS = new ConcurrentHashMap<>();

    // 添加会话到用户映射
    public static void addSession(String userId, HttpSession session) {
        USER_SESSIONS.computeIfAbsent(userId, k -> ConcurrentHashMap.newKeySet()).add(session);
        // 给会话绑定一个属性,方便后续销毁时识别用户ID
        session.setAttribute("userId", userId);
    }

    // 根据用户ID获取所有会话
    public static Set<HttpSession> getSessionsForUser(String userId) {
        return USER_SESSIONS.getOrDefault(userId, ConcurrentHashMap.newKeySet());
    }

    // 从映射中移除会话
    public static void removeSession(HttpSession session) {
        String userId = (String) session.getAttribute("userId");
        if (userId != null) {
            Set<HttpSession> sessions = USER_SESSIONS.get(userId);
            if (sessions != null) {
                sessions.remove(session);
                // 如果该用户没有会话了,清理映射项
                if (sessions.isEmpty()) {
                    USER_SESSIONS.remove(userId);
                }
            }
        }
    }
}

- 登录时绑定会话与用户ID

创建一个Servlet Filter,在用户通过Keycloak登录成功后,把当前会话和用户的sub关联起来:

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import org.keycloak.KeycloakSecurityContext;

@WebFilter("/*")
public class SessionBindingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws Exception {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        KeycloakSecurityContext securityContext = (KeycloakSecurityContext) httpRequest.getAttribute(KeycloakSecurityContext.class.getName());
        
        // 只有当用户已登录且会话未绑定过用户ID时执行
        if (securityContext != null && httpRequest.getSession().getAttribute("userId") == null) {
            String userId = securityContext.getToken().getSubject();
            SessionRegistry.addSession(userId, httpRequest.getSession());
        }
        
        chain.doFilter(request, response);
    }

    // 初始化和销毁方法按需实现
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}
    @Override
    public void destroy() {}
}

- 监听会话销毁,清理映射

实现HttpSessionListener,防止会话过期或手动销毁后,全局映射中残留无效会话导致内存泄漏:

import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

@WebListener
public class SessionCleanupListener implements HttpSessionListener {
    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        SessionRegistry.removeSession(se.getSession());
    }

    @Override
    public void sessionCreated(HttpSessionEvent se) {}
}

- 在Backchannel Logout端点处理会话失效

现在在你的/backchannel-logout端点中,解析LogoutToken拿到sub后,就可以批量失效用户的所有会话:

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.keycloak.jose.jwt.Jwt;
import org.keycloak.jose.jwt.JwtParser;

@WebServlet("/backchannel-logout")
public class BackchannelLogoutServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) {
        String logoutTokenStr = request.getParameter("logout_token");
        
        // 解析LogoutToken(这里可以用Keycloak的JwtParser工具)
        JwtParser parser = new JwtParser();
        Jwt logoutToken = parser.parse(logoutTokenStr);
        String userId = logoutToken.getSubject();
        
        // 获取该用户的所有会话并失效
        SessionRegistry.getSessionsForUser(userId).forEach(session -> {
            try {
                session.invalidate();
            } catch (Exception e) {
                // 处理会话已失效的异常,比如日志记录
                e.printStackTrace();
            }
        });
        
        // 返回200表示处理成功
        response.setStatus(HttpServletResponse.SC_OK);
    }
}

2. 实现管理员批量登出用户的端点

基于上面的SessionRegistry,实现一个受权限保护的端点就非常简单了:

- 创建管理员登出端点

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.representations.AccessToken;

@WebServlet("/admin/logout-user")
public class AdminUserLogoutServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) {
        // 先验证当前请求的用户是否是管理员
        KeycloakSecurityContext securityContext = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
        AccessToken token = securityContext.getToken();
        if (!token.getRealmAccess().getRoles().contains("admin")) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            return;
        }
        
        // 获取请求参数中的userId
        String userId = request.getParameter("userId");
        if (userId == null || userId.isEmpty()) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }
        
        // 失效该用户的所有会话
        SessionRegistry.getSessionsForUser(userId).forEach(session -> {
            try {
                session.invalidate();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        
        response.setStatus(HttpServletResponse.SC_OK);
    }
}

- 注意事项

  • 一定要确保这个端点的权限控制严格,只有具备管理员角色的用户才能访问,避免滥用。
  • 可以根据实际需求调整权限验证逻辑,比如用Keycloak的角色或者其他权限机制。

内容的提问来源于stack exchange,提问作者amdev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 23:54:06