yushijinhun / authlib-injector

Build your own Minecraft authentication system.
https://authlib-injector.yushi.moe
GNU Affero General Public License v3.0
749 stars 67 forks source link

[proposal] 禁止 ALI 的多次重定向 & 明确支持 HTTP 3xx #78

Closed yushijinhun closed 4 years ago

yushijinhun commented 4 years ago
旧提案版本 ## 概述 这个提案会尝试解决以下问题: * 是否应该处理 HTTP 3xx 重定向?如果是,那么当响应状态码为 3xx 但同时又包含 ALI 头时,应该如何处理? ## 问题背景 * 目前 authlib-injector 并不完全支持 HTTP 3xx 重定向(仅限 GET 请求,POST 请求会失败),这造成了 #77 中的问题。 * authlib-injector 对 `littlesk.in`(LittleSkin 的短域名) 的处理是这一问题的很好反映。 由于 authlib-injector 先前并未考虑对 HTTP 重定向的处理,因此解析 API 地址时出现了十分诡异的行为: * 输入 `littlesk.in`,API 地址被解析为 `https://mcskin.littleservice.cn/api/yggdrasil/`(符合预期) * 输入 `littlesk.in/api/yggrasil`,API 地址被解析为 `https://littlesk.in/api/yggdrasil/`(不符合预期) ``` $ java -javaagent:authlib-injector.jar=littlesk.in [authlib-injector.config] [INFO] API root: https://littlesk.in/ [authlib-injector.config] [INFO] Redirect to: https://mcskin.littleservice.cn/api/yggdrasil/ $ java -javaagent:authlib-injector.jar=littlesk.in/api/yggdrasil [authlib-injector.config] [INFO] API root: https://littlesk.in/api/yggdrasil/ ``` 上面有关 URL 的响应如下(无关内容已省略): ``` > GET https://littlesk.in/ HTTP/1.1 307 Temporary Redirect location: https://mcskin.littleservice.cn/ x-authlib-injector-api-location: https://mcskin.littleservice.cn/api/yggdrasil > GET https://mcskin.littleservice.cn/ HTTP/1.1 200 OK X-Authlib-Injector-API-Location: https://mcskin.littleservice.cn/api/yggdrasil > GET https://littlesk.in/api/yggdrasil HTTP/1.1 307 Temporary Redirect location: https://mcskin.littleservice.cn/api/yggdrasil x-authlib-injector-api-location: https://mcskin.littleservice.cn/api/yggdrasil > GET https://mcskin.littleservice.cn/api/yggdrasil HTTP/1.1 200 OK ``` 这一迷惑表现的具体成因不在此赘述,简单来说是 Java 默认跟随 HTTP 重定向造成的。 对于这一例子,如果忽略 HTTP 3xx 重定向,我们则能获得期望的结果。但忽略 HTTP 重定向并非一个好的解决方法,具体见下。 * 考虑一个 HTTP 3xx 重定向的使用场景: 某皮肤站域名为 `www.example.com`,authlib-injector 的 API 地址为 `https://www.example.com/api/`。 为了方便用户使用,皮肤站希望在 authlib-injector 中可以直接使用 `example.com` 作为 URL。这一点可以通过 ALI 来实现,但这需要在 `https://example.com/` 的响应中包含自定义的 HTTP 头。一般来说,网站会选择域名提供商的「non-www to www 跳转」,而这些服务一般都是**不支持**自定义的 HTTP 头的。因此,皮肤站会希望 authlib-injector 能够识别这种情况下的 HTTP 3xx 重定向。 * HTTP 3xx 重定向是否改变 URL? 某个皮肤站的 API 地址为 `https://example.com/api/`,当我们访问它时,我们被 **307 Temporary Redirect** 重定向到了 `https://hk2.example.com/api/`,那请问 API 地址是 `https://example.com/api/` 还是 `https://cn2.example.com/api/`?后续的请求应当被发送到哪一个 URL? 如果是 **308 Permanent Redirect** 则又如何? 参考 [Redirections in HTTP - MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#Principle),我们可以了解到通常的处理方法: > ## Permanent redirections > These redirections are meant to last forever. They imply that the original URL should no longer be used, and replaced with the new one. Search engine robots, RSS readers, and other crawlers _**will update the original URL for the resource**_. > ## Temporary redirections > Search engine robots and other crawlers _**don't memorize the new, temporary URL**_. Temporary redirections are also used when creating, updating, or deleting resources, to show temporary progress pages. ## 结论 在考虑上面的问题后,我们可以得出以下论断: * ALI 应该被视为一种**只对 authlib-injector 有效的 Permanent Redirect**。 * 遇到 **Permanent Redirect** 时,authlib-injector 应当跟随重定向,并使用**重定向后的 URL** 作为新的 API 地址。 * 遇到 **Temporary Redirect** 时,authlib-injector 应当跟随重定向,但依然使用**原 URL** 作为 API 地址。 ## 解决方案 本提案将修改 [启动器技术规范 § 处理 API 地址指示](https://github.com/yushijinhun/authlib-injector/wiki/%E5%90%AF%E5%8A%A8%E5%99%A8%E6%8A%80%E6%9C%AF%E8%A7%84%E8%8C%83#%E5%A4%84%E7%90%86-api-%E5%9C%B0%E5%9D%80%E6%8C%87%E7%A4%BA) 为如下: 为了将用户输入的 URL 解析为真正的 API 地址,启动器需要进行如下操作: 1. 如果 URL 缺少协议,则将其补全为 HTTPS 协议。 2. 向 URL 发送 GET 请求。 * 如果响应包含 ALI 头(`X-Authlib-Injector-API-Location`),则直接将其视为到 **ALI 所指地址**的一次 **Permanent Redirect**。ALI 头优先于 HTTP 3xx 重定向。 * 如果 HTTP 状态码是 3xx,则处理重定向。 * 对于 Permanent Redirect,需要将 URL 更新为重定向后的 URL;对于 Temporary Redirect 则不需要。 * 最大重定向次数为 5 次。 3. 最后经过 Permanent Redirect 后(如果有的话)的 URL 地址即为真正的 API 地址。 伪代码: ``` function resolve_api_url(url) actual_url = current_url = url redirect_count = 0 repeat response = http_get(current_url) if response.headers["x-authlib-injector-api-location"] exists actual_url = current_url = to_absolute_url(response.headers["x-authlib-injector-api-location"]) else if response.status_code is 301 or 308 actual_url = current_url = to_absolute_url(response.headers["location"]) else if response.status_code is 302, 303 or 307 current_url = to_absolute_url(response.headers["location"]) else return actual_url redirect_count ++ if redirect_count > 5 error ``` ## 其他改动 * 对于 URL 末尾的斜杠 `/` 不再做要求。 * ALI 将不再允许自指。 ---- 本提案可能还有很多不完善的地方,甚至可能会有争议,因此我会尽可能多的征询意见。如果你对本提案有任何建议,请在下面评论。

感谢诸位参与讨论,我已经修改了提案。下面是新版本的提案。


概述

这个提案会尝试解决以下问题:

改动

要点

启动器技术规范 § 处理 API 地址指示 的改动

为了将用户输入的 URL 解析为真正的 API 地址,启动器需要进行如下操作:

  1. 如果 URL 缺少协议,则将其补全为 HTTPS 协议。
  2. 向 URL 发送 GET 请求(跟随重定向)。
  3. 如果响应包含 ALI 头部(HTTP 头 X-Authlib-Injector-API-Location),那么 ALI 指向的 URL 就是 API 地址。
    • 如果 ALI 指向其自身,那就意味着当前 URL 就是 API 地址。
  4. 如果响应不包含 ALI 头部,则默认认为当前 URL 是 API 地址。

伪代码:

function resolve_api_url(url)
    response = http_get(url) // follow redirects

    if response.headers["x-authlib-injector-api-location"] exists
        new_url = to_absolute_url(response.headers["x-authlib-injector-api-location"])
        if new_url != url
            return new_url

    // if you are going to fetch the metadata next, 'response' can be reused
    return url
yushijinhun commented 4 years ago

/cc @TT702 @huanghongxun @Nsiso @tnqzh123 @g-plane

kenvix commented 4 years ago

对于 Temporary Redirect 的单独处理表示困惑,如果跟随重定向的过程中发现了 ALI 头或进一步改变为 308 Permanent Redirect,这种情况应如何解释?这种特殊对待,是否会造成其他隐性问题迷惑一般使用者?

yushijinhun commented 4 years ago

对于 Temporary Redirect 的单独处理表示困惑,如果跟随重定向的过程中发现了 ALI 头或进一步改变为 308 Permanent Redirect,这种情况应如何解释?这种特殊对待,是否会造成其他隐性问题迷惑一般使用者?

RFC 2616,相关内容已加粗:

10.3.3 302 Found

The requested resource resides temporarily under a different URI. Since the redirection might be altered on occasion, the client SHOULD continue to use the Request-URI for future requests. This response is only cacheable if indicated by a Cache-Control or Expires header field.

10.3.8 307 Temporary Redirect

The requested resource resides temporarily under a different URI. Since the redirection MAY be altered on occasion, the client SHOULD continue to use the Request-URI for future requests. This response is only cacheable if indicated by a Cache-Control or Expires header field.

tnqzh123 commented 4 years ago

我觉得没什么大问题

bangbang93 commented 4 years ago

重定向请求带程序逻辑头,这本身就是一种未定义行为,没有什么太大必要手动处理逻辑吧,又是重复发明轮子了。

yushijinhun commented 4 years ago

重定向请求带程序逻辑头,这本身就是一种未定义行为,没有什么太大必要手动处理逻辑吧,又是重复发明轮子了。

3xx 响应带一个 x-authlib-injector-api-location 头部,是有这种场景的。例如,网站首页 https://example.com/ 307 到 https://example.com/login,但又希望能识别 ALI 头部。

这确实是一种未定义行为,所以现在我把它定义了。

bangbang93 commented 4 years ago

重定向请求的特色是两个endpoint应该是等价的,也就是说给重定向请求定义一个程序逻辑,有可能会破坏这个前提

yushijinhun commented 4 years ago

重定向请求的特色是两个endpoint应该是等价的,也就是说给重定向请求定义一个程序逻辑,有可能会破坏这个前提

那当然是破坏的。但这并无问题,因为严格来说 ALI 不算重定向,只是拿来做服务发现的。

TT702 commented 4 years ago

由于BakaXL执行请求后获取到的实际内容已经被底层在不被通知的情况下自动进行了重定向,且重定向及ALI的过程极大程度降低了用户使用体验,BakaXL 将仅会继续支持原始地址输入和DnD拖拽输入,不会对 HTTP 3XX 和 ALI 重定向提供支持。

Nsiso commented 4 years ago

其实这个东西还是应该交给AI核心来做。 我和几位启动器开发者讨论过,启动器的前提就是遵循原版Ygg验证器逻辑,启动器只是工具启动游戏,AI作为一个独立的应用,既然在ygg上添加了新的功能或协议,那么在你们的应用层应该实现这些东西。重定向处理是你们那边提升用户体验基于ygg上的一个功能,理论上AI支持会更符合情理

bangbang93 commented 4 years ago

所以我觉得这个提案可以精简为,直接由HTTP框架处理重定向,若最终响应200,则直接取当前响应的x-authlib-injector-api-location,若响应头不包含x-authlib-injector-api-location,也可以认为当前url不是ali的endpoint

LTCatt commented 4 years ago

由于 PCL2 执行 GET 请求后获取到的实际内容已经被底层在不被通知的情况下自动进行了重定向,且重定向及 ALI 的过程极大程度降低了用户使用体验,用户完全可以通过直接输入原始地址的方式完成此步骤,因此 PCL2 将仅会继续支持原始地址输入,不会对 ALI 重定向提供支持。

yushijinhun commented 4 years ago

@Nsiso @LTCatt 照例来说 ALI 这个东西早就有了(#18),我是搞不懂为什么现在炸这么多意见出来。

ALI 就是用来做服务发现的,我引用一段 #18 中的:

假定 https://example.com/ 是一个皮肤站,其 Yggdrasil API 地址为 https://skin.example.com/api/yggdrasil。页面 https://example.com/ 使用了 ALI。

  • 配置启动器时,仅需要输入 example.com
  • 配置服务端时,仅需要添加参数 -javaagent:authlib-injector.jar=example.com
g-plane commented 4 years ago

伪代码中的 current_url 具体有什么作用?我没看到有在哪里被使用。 我瞎了。

LTCatt commented 4 years ago

我有以下几个问题:

  1. 为什么用户不直接在文本框里输入完整地址,或者直接使用 DnD 获得完整地址?举一个对等的例子,就像 MC 进服一样,玩家一直都是直接复制粘贴完整的服务器 URL,没有跳转也没有简写,但是这对用户的体验并没有造成任何影响。
  2. 关于 3xx 重定向,就我目前经验而言,.Net 相关支持库会在收到 3xx 请求时自动进行重定向,直到所有重定向结束后再继续,这一过程用户代码无法参与处理。
  3. 如果确实需要实现重定向,我也认为这应该是 Authlib 自身处理的部分,而不是让 多个启动器 为了 Authlib 一方 要求的新特性去更新多次代码,造成时间上的浪费。
tnqzh123 commented 4 years ago

@TT702 @LTCatt 我不明白为什么说「ALI 的过程极大程度降低了用户使用体验」。我的看法恰好相反,ALI 是能在一定程度上改善用户体验的。

正如 @yushijinhun 所说,ALI 就是拿来做服务发现的。用户不需要输入完整的地址,只要偷懒输一个域名就可以(比如输入 littlesk.in 即可识别为 LittleSkin 的 Yggdrasil API),相较输入完整地址,用户的记忆成本更低(毕竟 Yggdrasil 这个词不像 skin 这样好记,我见到过很多拼不对这个词的人),而 DnD 需要打开验证服务器网页再拖动按钮到启动器,会浪费一些时间。

tnqzh123 commented 4 years ago

@LTCatt

我有以下几个问题:

  1. 为什么用户不直接在文本框里输入完整地址,或者直接使用 DnD 获得完整地址?举一个对等的例子,就像 MC 进服一样,玩家一直都是直接复制粘贴完整的服务器 URL,没有跳转也没有简写,但是这对用户的体验并没有造成任何影响。
  2. 关于 3xx 重定向,就我目前经验而言,.Net 相关支持库会在收到 3xx 请求时自动进行重定向,直到所有重定向结束后再继续,这一过程用户代码无法参与处理。
  3. 如果确实需要实现重定向,我也认为这应该是 Authlib 自身处理的部分,而不是让 多个启动器 为了 Authlib 一方 要求的新特性去更新多次代码,造成时间上的浪费。

对于您的第一点,Minecraft 服务器其实也有类似的操作,就是「SRV 解析」。

IceCream-QAQ commented 4 years ago

@TT702 @LTCatt 我不明白为什么说「ALI 的过程极大程度降低了用户使用体验」。我的看法恰好相反,ALI 是能在一定程度上改善用户体验的。

正如 @yushijinhun 所说,ALI 就是拿来做服务发现的。用户不需要输入完整的地址,只要偷懒输一个域名就可以(比如输入 littlesk.in 即可识别为 LittleSkin 的 Yggdrasil API),相较输入完整地址,用户的记忆成本更低(毕竟 Yggdrasil 这个词不像 skin 这样好记,我见到过很多拼不对这个词的人),而 DnD 需要打开验证服务器网页再拖动按钮到启动器,会浪费一些时间。

用户真的需要记忆地址吗?
简单的说,浏览器为什么要有书签栏的功能?
或者说,你愿意放弃书签栏去单独记忆网站域名吗?

你这个论点没有意义。

TT702 commented 4 years ago

@tnqzh123 据我所知大部分用户输入地址的方式均是通过直接复制粘贴皮肤站上显示的yggdrasil服务器地址,截至目前位置我并未收到任何用户对于自动重定向这种功能的需求。 恕我直言我其实并无法理解添加重定向功能的必要性,因为yggdrasil验证服务器地址在用户使用某个皮肤站时,在启动器上只需配置一次。

而启动器在考虑实现这些过程时,需要对启动器本身进行yggdrasil验证的逻辑进行破坏性修改。 考虑以下几个问题:

1、用户在进行3XX重定向时,进行到第X次时与服务器连接失败。 2、用户在进行ALI重定向时,进行到第X次时与服务器连接失败。 3、用户的互联网连接非常缓慢,以至于完成每一次重定向均耗费大量的时间。

启动器应当如何正确引导用户处理此类问题? 显示服务器连接不正确?但是前X次请求是正常的? 进行重试?这中途又要浪费用户多少时间? 有这个时间都已经打开网站复制粘贴了。

tnqzh123 commented 4 years ago

@IceCream-Open

用户真的需要记忆地址吗? 简单的说,浏览器为什么要有书签栏的功能? 或者说,你愿意放弃书签栏去单独记忆网站域名吗?

你这个论点没有意义。

我觉得是有意义的。

我恰好就是那种愿意去记网址的人,我的收藏夹里只有几个我极其常用的链接,收藏夹仅是我的快捷方式罢了。

我认为书签栏过于混乱也不利于提高效率,毕竟相比在一堆快捷方式里花上几分钟找来找去,还是三秒钟输个网址更方便。

IceCream-QAQ commented 4 years ago

我们不应该对 HTTP 协议本身的规定,工作流程做出任何修改。
市面上大多(现代)语言的 HTTP 库,都对重定向有着支持,而不需要也很难去手动干扰。

开发者在无特殊需求的情况下也不应该干扰 HTTP 协议的正常工作方式。
就比如你硬要 404 状态码展示正常页面内容,这个需求本身就不正常。

LTCatt commented 4 years ago

@IceCream-Open 用户真的需要记忆地址吗? 简单的说,浏览器为什么要有书签栏的功能? 或者说,你愿意放弃书签栏去单独记忆网站域名吗? 你这个论点没有意义。

我觉得是有意义的。

我恰好就是那种愿意去记网址的人,我的收藏夹里只有几个我极其常用的链接。

你每次启动游戏之前都需要输一次 Authlib 校验地址? 浏览器有短网址你可以说是因为你要反复进很多次,要打很多次,但是这个地址,老铁,它只需要你复制一次。 为了让这仅仅一次的过程 少复制几个字符,启动器需要加几十行代码,每次登录需要多几次跳转。 这真的值得吗?

Nsiso commented 4 years ago

@TT702 @LTCatt 我不明白为什么说「ALI 的过程极大程度降低了用户使用体验」。我的看法恰好相反,ALI 是能在一定程度上改善用户体验的。

正如 @yushijinhun 所说,ALI 就是拿来做服务发现的。用户不需要输入完整的地址,只要偷懒输一个域名就可以(比如输入 littlesk.in 即可识别为 LittleSkin 的 Yggdrasil API),相较输入完整地址,用户的记忆成本更低(毕竟 Yggdrasil 这个词不像 skin 这样好记,我见到过很多拼不对这个词的人),而 DnD 需要打开验证服务器网页再拖动按钮到启动器,会浪费一些时间。

问题不在这些上面,问题是你们添加的功能我们要进行匹配,好几个启动器作者要为这个匹配和更新,不对劲的,我问下AI内部完成这些逻辑很难吗,还是说,在启动器上实现很必要吗

IceCream-QAQ commented 4 years ago

@IceCream-Open 用户真的需要记忆地址吗? 简单的说,浏览器为什么要有书签栏的功能? 或者说,你愿意放弃书签栏去单独记忆网站域名吗? 你这个论点没有意义。

我觉得是有意义的。

我恰好就是那种愿意去记网址的人,我的收藏夹里只有几个我极其常用的链接,收藏夹仅是我的快捷方式罢了。

我认为书签栏过于混乱也不利于提高效率,毕竟相比在一堆快捷方式里花上几分钟找来找去,还是三秒钟输个网址更方便。

建议你直接发起投票,看看大多数人是如何选择的。
一般某个服务器都会有其独立的客户端,很少有玩家去手动组成一个完整的客户端。

一个客户端内有多个服务器的授权的几率就更微乎其微了。
启动器也会保留这个数据,而并非每次都需要去获取一次。

玩家也不会在乎他是复制了一个域名,还是一个完整的地址,因为是复制,所以他不在乎。
玩家同样也不会背下来服务器的域名,或是服务器的 IP 地址。

bangbang93 commented 4 years ago

重定向请求带程序逻辑头,这本身就是一种未定义行为,没有什么太大必要手动处理逻辑吧,又是重复发明轮子了。

3xx 响应带一个 x-authlib-injector-api-location 头部,是有这种场景的。例如,网站首页 https://example.com/ 307 到 https://example.com/login,但又希望能识别 ALI 头部。

这确实是一种未定义行为,所以现在我把它定义了。

其实这种需求应该用UA来区分客户端,ALI的请求不要丢回300

g-plane commented 4 years ago

我也认为不要去干预 HTTP 库的行为——即使库允许手动处理重定向行为,但那是非常复杂的。

yushijinhun commented 4 years ago

看起来这一提案果然引起了很大的争论。为了保证讨论能够解决问题,我暂时开启了 interaction limit,并且会清理掉部分无关评论,interaction limit 会在评论清理完毕后关闭。


我整理了一下目前各位提出的意见,大致如下:

  1. 用户应该输入完整的 URL,或者通过拖拽来添加验证服务器,ALI 没有存在的必要。
  2. 对于类如本提案中的改动,需要各个启动器去适配更新,成本太大。
  3. 本提案中的改动需要手动处理 HTTP 重定向,而原本这可以让 HTTP 类库自行完成,这一改动增加了复杂度。

我的观点

1. ALI 有没有存在的必要?

有。我们需要一个方案,使得在只输入域名的情况下,启动器(或 authlib-injector)就能找到相关的 API 接口地址。理由如下:

  1. ALI 在 2018 年 10 月就被提出,并且之后不久就得到了 HMCL 和 Blessing Skin Server 的支持。截至我创建此 issue 前,我没有收到任何「这一特性无用」的意见。我有理由相信,这个特性哪怕不能算是好,但也绝对不能说是坏。
  2. ALI 有明确的使用场景,即让 API 地址的输入变得简便。我没有做过用户调查,不知道具体有多少人使用这一功能,但显然 @tnqzh123 (据其自己所称)就是一个用户。
    • 如果你认为用户就应该输入完整的 URL,这点麻烦不算什么,那么 DnD 添加验证服务器的特性也是赘余的。
  3. ALI 没有坏处。哪怕用户输入地址时,可以省掉协议、省掉路径,但他依然可以选择输入完整的 URL。

2. 能否让各个启动器免去适配及更新的麻烦?

希望如此。如果你能做到,请务必详细述说,我会十分感激。不用写一行代码就能添加新的特性,谁不想要呢?

3. 实现此提案需要 hack HTTP 类库的重定向处理

这确实是个很糟糕的设计,虽然单单看起来不错,但实现起来却很恶心。讨论的重点其实应该在这个问题上,我下面会尝试针对这一问题,提出一个新的解决方案(替代我先前提出的方案)。

新的解决方案

首先明确:

那么显然处理用户输入的 URL 的逻辑为:

  1. 如果 URL 缺少协议,则将其补全为 HTTPS 协议。
  2. 向 URL 发送 GET 请求(跟随重定向)。
  3. 如果响应包含 ALI 头部,那么 ALI 指向的 URL 就是 API 地址。
    • 如果 ALI 指向其自身,那就意味着当前 URL 就是 API 地址。
  4. 如果响应不包含 ALI 头部,则默认认为当前 URL 是 API 地址。

伪代码:

function resolve_api_url(url)
    response = http_get(url) // follow redirects

    if response.headers["x-authlib-injector-api-location"] exists
        new_url = to_absolute_url(response.headers["x-authlib-injector-api-location"])
        if new_url != url
            return new_url

    // if you are going to fetch the metadata next, 'response' can be reused
    return url

对于 URL 末尾的斜杠 / 依然不做要求。

yushijinhun commented 4 years ago

我已经更新了提案,新的方案看起来应该较无争议。我会让这个 proposal 多晾几天,如果你有意见,依然可以在下面评论。

yushijinhun commented 4 years ago

Wiki 已更新:a8d4f27a5641e8da1b9443d7889c4e447606b702