聯邦式 OAuth 構想

剛剛在架 wallabag ,開源的 Pocket / Instapaper 替代品。

然後突然想到,我架一個這個就只有自己用、自己維護耶,而且又是另外一組帳號密碼。

不知道有沒有辦法設計一套聯邦式的 OAuth 認證機制,不同的組織(網域)可以加入聯邦,所有人使用同一個 OAuth endpoint 但帳號的後綴不同,這樣志願的服務供應者(譬如說我很樂意讓別人一起使用我維護的 Wallabag)就可以直接選用這個 OAuth 認證服務,再選擇要讓哪一些組織的人登入。

這樣的話,既能達到單一帳號的功能(在不同服務間可以使用同一帳號,不用記多組帳號密碼),也不像直接採用 Google OAuth 這種任何人都可以擁有帳號的服務,志願的服務供應者就可以只允許他信任的組織的使用者使用他的服務。

可能有點難理解,舉個例子說明:

姑且把這個中央代理 OAuth 服務叫做 Distauth,接下來有志願的服務供應者 A, B 和組織 X, Y, Z,還有組織 X, Y, Z 的成員甲、乙、丙。

A 因為自己的需求,架了一個 Nextcloud ,他願意分享一些伺服器空間給其他人使用,但是又怕有人濫用。

B 為了要自己作筆記,架了一個 hackmd ,他也願意讓其他人使用他維護的 hackmd 服務。

X 組織是某個資訊社群,他們擁有一個 xxxx.tw 的網域,也有一套自己的帳號系統,要申請 xxxx.tw 的帳號需要經過組織訂定的審查流程,並簽署不濫用的同意書,甲已經通過,因此有 jia@xxxx.tw 這個帳號。

Y 組織是另一個鬆散的社群,他們提供所有成員一個帳號,只要填一個線上表單就可以申請,乙擁有 yee@yyyy.tw 這個帳號。

Z 組織是一間大學,所有的在校生都有帳號,帳號會在畢業之後失效,丙擁有 bing@zzzz.tw 這個帳號。

X, Y, Z 都加入了 Distauth 聯盟。

Distauth 是一個特殊的 OAuth 認證伺服器 (Authorization Server) ,任何組織(只要有一套帳號系統)都可以加入 Distauth , 任何服務供應者、應用程式都可以和 Distauth 註冊,成為 OAuth client (application) ,取得獨特的 App ID 和 App Secret 。

A 因為伺服器空間有限,而且他想要確保來使用的人不會濫用他提供的服務,他認為 X 的審查流程足夠嚴格,Y, Z 則不,所以他在 Distauth 取得 App ID 和 Secret 的時候就設定,只有組織 X 的人可以登入。

如此一來,甲已經擁有了 X 的帳號,所以他可以直接使用 A 提供的 Nextcloud 服務,X 組織未來任何的新成員也都可以直接使用 A 的 Nextcloud ;同時,乙和丙雖然可以成功登入 Distauth ,但 Distauth 不會授權乙丙使用 A 的 Nextcloud 。

B 架設的 hackmd ,因為只有純文字儲存功能,所以佔用資源很少,他認為不需要嚴謹限制使用者,但還是希望是認識的社群的成員才能使用,所以他在 Distauth 設定 X, Y, Z 的成員都可以使用,但其他組織不行。

整個概念大概就是這樣,現在看來好像沒有類似的東西,自認這構想還不錯,不過一樣,要獲得廣泛採用本來就很困難 :-p

寫爬蟲絕對不要太客氣也不可以心軟

尤其是要爬像是 Facebook 這種蓄意想要鎖住資訊,把 Internet 變成一個巨大內網的邪惡公司的時候。

之前在 Github 看到 rss-bridge 這個專案,用爬蟲去爬網頁然後產生 RSS/Atom feed ,原本是想要把它用在 Facebook 上面,這樣就可以訂閱粉專的每一篇貼文、照時間排序、不用被 Facebook 的 filter bubble 審查資訊,也不會因為你訂閱什麼就汙染你的 stream 。

架起來試了一下發現效果還不錯,訂閱了一堆粉專就去睡了。

結果隔天早上醒來發現所有 feed 都失效,因為 Facebook 要求 Captcha 驗證。原本的 rss-bridge 會把驗證圖片 proxy 顯示給瀏覽 feed 的使用者,使用者可以解完之後送出,rss-bridge 會再送給 Facebook ,但這功能實測的時候發現無論如何送出都會失敗,把這 bug 解掉之後又發現 captcha 只會當次有效,也就是要發另一個 request 的時候又會被跳 captcha 。

自己開瀏覽器測試了一下發現,瀏覽器的 captcha 只要解一次,未來的 request 就都會過,想說大概是因為 rss-bridge 的爬蟲不支援 cookie 的問題,所以就花了好幾天時間把 rss-bridge 的爬蟲換成 PHP curl (原本是用 file_get_contents() ),因為 PHP curl 有 cookie jar 的功能,管理 cookie 比較方便。

結果新的 code 終於會動,但儘管送了 cookie 還是每次被跳 captcha ,挖到底層去測試了很多東西,檢查 request header 和 response header ,確定不是我實作有問題之後,我發現問題不在 cookie。

問題在於 rss-bridge 的爬蟲預設都會多送一個 GET 參數 _fb_noscript=1 ,有這個參數的時候顯示出來的 captcha 會不一樣!

_fb_noscript=1 的時候顯示的 captcha 會直接是一個 <img src="..."> ,而且這個 captcha 也只有單次有效!下次再請求,就算有 cookie, 也會再被要求解另一個 captcha 。

沒有那個參數的時候的 captcha 圖片會使用 AJAX 載入,然後解開之後設定的 cookie 也會多次有效。

我拿舊的實作改成 curl 的時候沒有特別改這個東西,結果到現在才發現這件事。

所以,這次學到的教訓是,寫爬蟲的時候,不要仁慈,直接開 PhantomJS/CasperJS 吧……

P.S. 我改過的 rss-bridge 在這兒: https://github.com/pellaeon/rss-bridge/tree/enhance

更新:不想用 Javascript 的話 Python 的 Scrapy + Splash (Splash 是 headless browser)看起來還不錯(沒用過)。還有 django-dynamic-scraper 可以直接把爬到的東西結構化成 Django model object ,每一個 field 直接關聯一個 selector ,scrape 的時候讀進去,太神啦。

2017/2/24 更新2:後來又繼續試著解決這個問題。研究了一下 django-dynamic-scraper , 幾乎找不到如何處理 captcha 的說明和範例,雖然說應該是可以解但是感覺又要研究 scrapy 很久,所以還是決定回去幫 rss-bridge 加上 proxy 的功能。改出來的東西在 https://github.com/pellaeon/rss-bridge/tree/enhance2 。然後還花了一些心力研究要怎麼取得免費的 proxy 清單,最後發現 HideMyAss.com 的不錯,寫了個小小的 Javascript 去撈 proxy 清單