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

Vertx+Pac4j实现SAML SP登出异常:登出后仍可访问受保护资源

基于Vertx+pac4j的SAML SP登出后仍可访问受保护资源问题排查与解决

问题描述

我用Vertx、pac4j及pac4j-vertx构建了一个基于SAML协议的Web应用,作为服务提供商(SP),身份提供商(IdP)采用simpleSAMLphp。

当前问题:用户在SP端执行登出操作后,无需重新进行SAML认证就能直接访问受保护资源/msg.html。通过Chrome的SAML-tracer插件验证,SAML与IdP的登出流程已正常完成,用户在IdP端已成功登出。

应用包含两个静态HTML页面:

  • 登录页login.html(路径/login,无需保护)
  • 受保护资源msg.html(路径/msg.html,需认证后访问)

请问:

  1. 该问题的原因是什么?
  2. 如何解决才能让用户在SP端登出后无法访问受保护资源,必须重新完成SAML认证?
  3. Vertx与pac4j是否支持静态HTML页面?

相关代码

主Verticle代码

public class MainVerticle extends AbstractVerticle {

  @Override
  public void start(Promise<Void> startPromise) throws Exception {

    HttpServerOptions serverOptions = new HttpServerOptions().setMaxFormAttributeSize(65536);
    HttpServer server = vertx.createHttpServer(serverOptions);
    Router router = Router.router(vertx);
    LocalSessionStore vertxSessionStore = LocalSessionStore.create(vertx);
    SessionStore sessionStore = new VertxSessionStore(vertxSessionStore);
    Pac4jAuthProvider authProvider = new Pac4jAuthProvider();
    SessionHandler sessionHandler = SessionHandler.create(vertxSessionStore);

    router.get("/login").handler(StaticHandler.create("src/main/resources/static/login.html"));

    SAML2Client saml2Client = this.saml2Client();
    Config config = new Config("http://localhost:8888/callback",saml2Client);

    router.route().handler(sessionHandler);
    
    SecurityHandlerOptions options = new SecurityHandlerOptions().setClients("SAML2Client");
    router.route("/msg.html").handler(new SecurityHandler(vertx, sessionStore, config, authProvider,options));
    router.get("/msg.html").handler(rc ->{
      rc.response().putHeader(CONTENT_TYPE, TEXT_HTML);
      rc.next();
    });
    router.get("/msg.html").handler(StaticHandler.create("src/main/resources/static/msg.html"));

    CallbackHandlerOptions callbackHandlerOptions = new CallbackHandlerOptions()
                .setDefaultUrl("/")
                .setMultiProfile(true);
    CallbackHandler callbackHandler = new CallbackHandler(vertx, sessionStore, config, callbackHandlerOptions);
    router.get("/callback").handler(callbackHandler);
    router.post("/callback").handler(BodyHandler.create().setMergeFormAttributes(true));
    router.post("/callback").handler(callbackHandler);

    router.get("/logout").handler(SingleLogouthandler(vertx, config, sessionStore));

    server.requestHandler(router).listen(8888, http -> {
      if (http.succeeded()) {
            startPromise.complete();
            System.out.println("HTTP server started on port 8888");
          } else {
            startPromise.fail(http.cause());
          }
    });
  }

  private SAML2Client saml2Client(){
    SAML2Configuration cfg = new SAML2Configuration("samlConfig/samlKeystore.jks", 
                                    "pac4j-demo-passwd", 
                                    "pac4j-demo-passwd", 
                                    "samlConfig/idp-metadata.xml");

    cfg.setResponseBindingType(SAMLConstants.SAML2_POST_BINDING_URI);
    cfg.setServiceProviderEntityId("http://localhost:8888/callback?client_name=SAML2Client");
    cfg.setServiceProviderMetadataPath(new File("target", "sp-metadata.xml").getAbsolutePath());
    return new SAML2Client(cfg);
  }

  private Handler<RoutingContext> SingleLogouthandler(Vertx vertx, Config config, SessionStore sessionStore){
    LogoutHandlerOptions logoutOptions = new LogoutHandlerOptions()
        .setCentralLogout(true)
        .setLocalLogout(true)
        .setDefaultUrl("http://localhost:8888/login");
    return new LogoutHandler(vertx, sessionStore, logoutOptions, config);
  }

}

登录页login.html代码

<!DOCTYPE html>
<html>
<head>
    <title>Login page</title>
</head>
<body>
    <h2>Log in</h2>
    <a href="/msg.html">Login using SAML</a><br/>
</body>
</html>

受保护资源msg.html代码

<!DOCTYPE html>
<html>
<head>
    <title>Login Successful</title>
</head><body>
    <h1>Login Successful</h1>
    <a href="/logout">local logout</a>
</body>
</html>

问题分析与解决方案

问题原因

  1. SP端会话未彻底清除:虽然IdP端登出成功,但SP本地会话中的pac4j认证信息可能未被正确销毁。即便配置了setLocalLogout(true),也可能因会话存储交互问题,导致SecurityHandler误判用户仍处于已认证状态。
  2. 路由顺序或缓存问题:浏览器可能缓存了/msg.html页面,无需请求服务器即可直接显示;或者StaticHandler的执行优先级高于SecurityHandler,导致请求未经过认证校验就返回了页面内容。

解决方法

  1. 强制销毁SP本地会话
    在登出处理器后添加手动销毁会话的逻辑,确保SP端认证状态完全清除:

    router.get("/logout")
          .handler(SingleLogouthandler(vertx, config, sessionStore))
          .handler(rc -> {
              rc.session().destroy(); // 手动销毁当前会话
              rc.response().redirect("http://localhost:8888/login");
          });
    
  2. 修正路由执行顺序
    确保SecurityHandler是/msg.html请求的第一个处理器,避免StaticHandler提前返回内容:

    router.route("/msg.html")
          .handler(new SecurityHandler(vertx, sessionStore, config, authProvider, options))
          .handler(rc -> {
              rc.response().putHeader(CONTENT_TYPE, TEXT_HTML);
              rc.next();
          })
          .handler(StaticHandler.create("src/main/resources/static/msg.html"));
    
  3. 禁用静态资源缓存
    配置StaticHandler关闭浏览器缓存,防止受保护页面被缓存:

    StaticHandler.create("src/main/resources/static/msg.html")
                 .setCachingEnabled(false)
                 .setCacheTime(0);
    

Vertx与pac4j对静态HTML页面的支持

Vertx原生通过StaticHandler支持静态HTML页面,pac4j-vertx的SecurityHandler可以与StaticHandler完美配合,只要保证认证校验处理器先于静态资源处理器执行,就能实现对静态HTML页面的访问控制,两者完全兼容。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 14:02:03