稍微复杂一些的 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
)。