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

Keycloak单设备登录:实现旧设备自动登出的技术问询

解决方案

一、旧设备自动登出实现

1. 基于Keycloak会话事件的实时通知

利用Keycloak的会话事件机制,结合后端SSE通道推送登出指令:

  • 在Spring Boot后端通过Keycloak Admin API监听SESSION_REMOVED事件(可采用长轮询或Keycloak 17+支持的Webhook方式)。
  • 当检测到用户旧会话被终止时,调用SSE连接管理器向该用户的旧设备连接发送登出消息。
  • Angular端监听SSE的logout事件,收到消息后立即执行登出逻辑(清除本地令牌、跳转登录页),无需手动刷新。

2. 后端令牌会话校验拦截器

不要仅依赖JWT签名有效性,每次请求(包括SSE连接)都校验令牌对应的Keycloak会话状态:

  • 实现Spring Boot全局拦截器,调用Keycloak的Introspect Token API校验令牌关联的会话是否活跃。
  • 若会话已失效,直接返回401状态码,Angular的HTTP拦截器捕获后触发自动登出流程。

二、SSE单活跃连接保障

1. 后端用户连接映射管理

维护全局连接映射,确保同一用户仅保留一个活跃SSE连接:

  • 用ConcurrentHashMap<String, SseEmitter>存储用户ID与对应SSE发射器的映射。
  • 新连接建立时,先关闭该用户的旧连接并发送登出通知,再将新连接存入映射。
  • 连接超时或完成时自动移除映射,避免资源泄漏。

2. 前端连接状态管控

Angular端封装SSE服务,确保同一时间仅存在一个有效连接:

  • 页面刷新或路由切换时,先关闭旧连接再创建新连接。
  • 监听连接异常,自动重试前检查是否已有有效连接,避免重复创建。

代码示例

Spring Boot SSE连接管理器

@Component
public class SseConnectionManager {
    private final Map<String, SseEmitter> userEmitters = new ConcurrentHashMap<>();

    public SseEmitter createEmitter(String userId) {
        // 关闭旧连接并发送登出通知
        SseEmitter oldEmitter = userEmitters.remove(userId);
        if (oldEmitter != null) {
            try {
                oldEmitter.send(SseEmitter.event().name("logout").data("会话已被新登录终止"));
                oldEmitter.complete();
            } catch (IOException e) {
                // 异常处理
            }
        }
        // 创建新连接
        SseEmitter emitter = new SseEmitter(3600000L);
        userEmitters.put(userId, emitter);
        // 连接结束时清理映射
        emitter.onCompletion(() -> userEmitters.remove(userId));
        emitter.onTimeout(() -> userEmitters.remove(userId));
        return emitter;
    }

    // 主动发送登出通知
    public void sendLogoutNotification(String userId) {
        SseEmitter emitter = userEmitters.get(userId);
        if (emitter != null) {
            try {
                emitter.send(SseEmitter.event().name("logout").data("你的账号在其他设备登录,当前会话已终止"));
                emitter.complete();
                userEmitters.remove(userId);
            } catch (IOException e) {
                userEmitters.remove(userId);
            }
        }
    }
}

Angular SSE服务与登出监听

@Injectable({ providedIn: 'root' })
export class SseService {
  private eventSource: EventSource | null = null;
  private userId: string | null = null;

  constructor(private authService: AuthService) {
    this.userId = this.authService.getCurrentUserId();
  }

  connect(): Observable<MessageEvent> {
    // 关闭已有连接
    if (this.eventSource) {
      this.eventSource.close();
    }
    this.eventSource = new EventSource(`/api/sse/connect?userId=${this.userId}`);
    
    return new Observable(observer => {
      this.eventSource!.addEventListener('logout', () => {
        this.authService.logout(); // 执行登出:清除本地令牌、跳转登录页
      });

      this.eventSource!.onerror = () => {
        this.eventSource?.close();
        this.eventSource = null;
        observer.error('SSE连接异常');
      };
    });
  }
}

Keycloak会话事件监听(Spring Boot)

@Component
public class KeycloakSessionMonitor {
    private final Keycloak keycloak;
    private final SseConnectionManager sseManager;

    public KeycloakSessionMonitor(Keycloak keycloak, SseConnectionManager sseManager) {
        this.keycloak = keycloak;
        this.sseManager = sseManager;
    }

    // 长轮询检测已终止会话,可根据业务调整轮询间隔
    @Scheduled(fixedDelay = 5000)
    public void checkTerminatedSessions() {
        List<UserSessionRepresentation> terminatedSessions = keycloak.realm("你的Realm名称")
                .users()
                .list()
                .stream()
                .flatMap(user -> keycloak.realm("你的Realm名称").users().get(user.getId()).getUserSessions().stream())
                .filter(session -> !session.isActive())
                .toList();

        terminatedSessions.forEach(session -> sseManager.sendLogoutNotification(session.getUserId()));
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 18:07:42