Hash 路由的迷思与单点登录跳转

2024-06-25, 星期二, 12:06

稍微复杂一些的 SPA 应用都需要用到前端路由去管理「页面」,其中一种主流的方式是 hash 模式,即在真实 URL 后添加 # 符号并拼接路径。当路径发生变化时,不由浏览器重新发起请求,而是响应 onhashchange 事件切换页面。

在对接某系统的单点登录时,提供了这样一种对接方式:应用 cool.app 检测到用户无登录状态时会跳转到认证页面 https://auth.app/login?service=https://cool.app/。用户在认证页面完成登录后,auth.app 将重定向到 https://cool.app/?ticket=12345,其中 ticket 是用来获取用户信息的凭证。

如果前端希望登录后重定向到一个特定的落地页,例如 hash 路由的 https://cool.app/#landing,cool.app 会将用户重定向到 https://auth.app/login?service=https://cool.app/#landing

客户端在获取文档时不应将 URI 片段发送到服务器,并且如果没有本地应用程序的帮助,片段不会参与 HTTP 重定向。

此时 # 与其后的内容作为 Fragment Identifier 并不会通过网络传输,服务器只会获得用户在访问 https://auth.app/login?service=https://cool.app/ 的信息。

如果 auth.app 只通过域名和端口识别服务名称(例如笔者现在对接的认证服务),或使用了前缀匹配,那么 https://cool.app/ 还是可以和 https://cool.app/#landing 匹配的,只不过由于认证服务器只收到了 https://cool.app/,认证成功后的重定向会指向 https://cool.app/?ticket=12345

当然,如果 auth.app 的实现允许分别配置认证服务(地址)和回调地址,那确实可以设置回调地址为 https://cool.app/#landing,服务器也乐意在重定向的过程中将 fragment 的信息带回来。很遗憾,笔者对接的这个系统并不支持。

你可以用一段基于 Spring Web 的简单代码验证这个过程:

private final String REGISTERED_SERVICE = "https://cool.app/#landing";

public void auth(@RequestParam("service") String service, HttpServletResponse response) throws IOException {
    if (REGISTERED_SERVICE.equalsIgnoreCase(service)) {
        response.sendRedirect(REGISTERED_SERVICE + "?ticket=12345");
    } else {
        response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Service not registered");
    }
}

办法也不是没有,也可以在 SPA 的 Application 初始化前执行一些额外的 JavaScript,判断 URL 中有没有相关的 ticket 参数,有的话就做一个本地的重定向到 #landing?ticket={ticket}(或者 window.location.hash)。