<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Yu-Cheng Chuang’s Blog</title>
        <link>https://blog.yorkxin.org</link>
        <description></description>
        <copyright>(C) Yu-Cheng Chuang</copyright>
        <pubDate>Mon, 01 Jul 2024 04:07:19 +0000</pubDate>
        <lastBuildDate>Wed, 05 Mar 2025 12:47:15 +0000</lastBuildDate>
        <atom:link href="https://blog.yorkxin.org/feed.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title>Copy as Markdown 3.0: A Milestone</title>
            <link>https://blog.yorkxin.org/posts/copy-as-markdown-3-en/</link>
            <pubDate>Mon, 01 Jul 2024 04:07:19 +0000</pubDate>
                
            <guid>a43bcf75-2006-48fb-984e-44fc2c080b5e</guid>
            
            <description><![CDATA[
                <p><a href="https://github.com/yorkxin/copy-as-markdown">Copy as Markdown</a> is a browser extension I've been developing since 2012. The main function is to convert hyperlinks or pages on the web into Markdown, and then copy them to the clipboard. It has around 10K WAU, available on the official stores for <a href="https://chromewebstore.google.com/detail/fkeaekngjflipcockcnpobkpbbfbhmdn?authuser=0&amp;hl=zh-TW">Chrome</a>, <a href="https://addons.mozilla.org/en-US/firefox/addon/copy-as-markdown/">Firefox</a>, and <a href="https://microsoftedge.microsoft.com/addons/detail/cbbdkefgbfifiljnnklfhnhcnlmpglpd">Edge</a>. It has always been minimally maintained, mainly because I had no time to manage it alongside my job. Recently, I had some free time during parental leave to make some changes. As my leave is ending soon, I may not have time to work on it again, so I'm documenting this milestone.</p>
<p>Over the past 3 months, I’ve done the following:</p>
<ul>
<li>Added E2E testing running on Selenium</li>
<li>Made most permissions optional</li>
<li>Improved the stability of the Firefox version (mainly by reverting to Manifest V2)</li>
<li>Added a feature to copy bookmarks in Firefox</li>
<li>Added a feature to copy tab groups in Chrome</li>
<li>Added a feature to copy HTML as Markdown</li>
<li>Various code improvements (mainly switching to async/await)</li>
<li>Used <a href="https://bulma.io/">Bulma.css</a> for styling</li>
</ul>
<p><em>This post is also available in Chinese. 本文亦有</em><a href="https://blog.yorkxin.org/posts/copy-as-markdown-3-zh"><em>中文版</em></a><em>。</em></p>
<h2 id="e2e-testing">E2E Testing</h2>
<p>As mentioned above, with minimal maintenance, it has few features, and relied on manual testing. When adding new features, I had to do cross-browser and cross-OS testing, which occasionally broke functions over the past 12 years. I primarily developed and tested on macOS and had never tested properly on Windows or Linux desktops. Recently, I got a second-hand PC, so I could test on Windows and Linux. But manual testing was still exhausting, so I researched E2E testing.</p>
<p>Ideally, E2E testing would directly trigger keyboard and mouse events. However, testing browser extensions isn't easy, especially when interacting with native UI elements like tabs, keyboard shortcuts, right-click menus, and permission prompts. Chrome's official <a href="https://developer.chrome.com/docs/extensions/how-to/test/end-to-end-testing">guide</a> lists tools like Selenium, WebDriver, and Puppeteer, but the documentation isn't comprehensive. I spent a lot of time exploring these tools and also considered Sikuli and Robot Framework, but they weren’t easy to use, so I didn’t try any of them. Tools like Puppeteer/WebDriver, based on the Chrome Developer Protocol, can only control the web page within the browser and can't interact with the OS's native UI, making them unsuitable.</p>
<p>In the end, I chose Selenium's Java version because it can call AWT's <a href="https://docs.oracle.com/javase%2F8%2Fdocs%2Fapi%2F%2F/java/awt/Robot.html">Robot</a> library to send keyboard and mouse events to the OS. This decision proved correct because Java's cross-platform nature saved me a lot of trouble. Selenium and AWT are also well-established tools, making it easy to find Q&amp;A and unusual tips &amp; tricks. For example:</p>
<ul>
<li>Robot can only send events to the current application. When opening Selenium with TestNG, the focus is on the Test Runner, so it is necessary to use <code>driver.switchTo().window(handle)</code> to switch the window focus.</li>
<li>Sending macOS combination keys with Robot requires a 200ms delay between modifier keys and non-modifiers; on Windows, you may need to call <code>keyRelease()</code> depending on the situation.</li>
<li>When navigating the right-click menu, you can directly "type" with keyboard events, but it doesn't always work.</li>
<li>In Selenium, only the Window is intractable, not the Tab, and certainly not Tab Groups. Therefore, I created an <a href="https://github.com/yorkxin/copy-as-markdown/tree/master/e2e/support/e2e-test-extension">E2E Testing Support Extension</a> to call Chrome's API to open test tabs.</li>
<li>For optional permission prompts, in Firefox, you can directly set <code>extensions.webextOptionalPermissionPrompts=false</code> to disable the prompts. In Chrome, you only need to allow once, so I send keyboard events to allow all permissions at once before running test cases.</li>
</ul>
<p>Running all tests takes about 3 minutes, which is acceptable but might be over-testing. There are still issues on Windows and Linux (KDE) that prevent all tests from passing. I'm considering moving some tests to Puppeteer, directly calling <code>background.js</code> event handlers, assuming the browser can correctly trigger keyboard and right-click menu events. However, this would require setting up another framework, so I'll postpone it for now.</p>
<p>During development, I used <a href="https://www.jetbrains.com/aqua/">JetBrains Aqua</a> and <a href="https://pastenow.app/">PasteNow</a> to record clipboard content, which I highly recommend.</p>
<h2 id="optional-permissions">Optional Permissions</h2>
<p>Copy as Markdown has had the export tabs feature from early versions, so it required the <code>tabs</code> permission. Recently, I added the <code>tabGroups</code> feature to export tab groups in Chrome and Edge. When the extension requests new permissions, Chrome shows a warning. Shortly after, users started complaining, "Why do you need to see my browsing history?" Many users also chose to uninstall (about 5%). This is because Chrome's permission warning displays "Read your browsing history," which technically isn't incorrect since accessing tab data means you can monitor browsing history. But this is alarming to general users, who might think the program is constantly monitoring them.</p>
<p>This churn rate was too damaging, so I started spending time changing permissions to avoid any warnings during installation. I moved all <a href="https://developer.chrome.com/docs/extensions/reference/permissions-list">permissions that trigger warnings</a> to optional, allowing users to decide whether to grant, and providing a feature to revoke permissions anytime, hoping to mitigate the churn.</p>
<p>Although my program is open-source, I can't expect all 10K weekly active users to read and understand the code before using it. As a practical PM, I have an obligation to improve this part of the UI. The lesson here is to give users the option to access sensitive information.</p>
<p>In the past 3 months, besides making the original tabs permission optional, I also made the new <code>tabGroups</code> and <code>bookmarks</code> permissions optional. The latter must be optional because the related feature only supports Firefox.</p>
<h2 id="firefox-version-reverts-to-manifest-v2-temporarily">Firefox Version Reverts to Manifest V2 (Temporarily)</h2>
<p>Manifest versions can be understood as API versions for browser extensions. The latest version is V3, which is nominally a spec driven by a W3C workgroup, but practically driven by Chrome's market dominance. MV3 is mandatory in Chrome, and MV2 is no longer accepted. However, for various reasons, Firefox currently supports both MV2 and MV3. The APIs are mostly similar, with small differences mainly being Promise, but those small differences require adding many <code>if (typeof chrome.xyz === 'undefined')</code> and callback-to-Promise wrappers, which is quite annoying.</p>
<p>Since Chrome is pushing MV3, Firefox is forced to follow. I initially assumed MV3 would unify everything and chose MV3 for Firefox considering code maintenance. However, this caused a mysterious <a href="https://github.com/yorkxin/copy-as-markdown/issues/109">right-click menu disappearing issue</a> in Firefox. I tried several hacks but couldn't fix it, so I reverted to MV2.</p>
<p>This brings back the callback vs. Promise API issue. Initially, I thought I could manually wrap a few APIs, but it became increasingly cumbersome. So, I introduced Mozilla's <a href="https://github.com/mozilla/webextension-polyfill">webextension-polyfill.js</a>, allowing most code to be written using async/await.</p>
<p>My long-term goal is still to migrate to MV3, but for now, I'll leave it as is since it works, and I have E2E tests to prevent breaking changes (hopefully).</p>
<h2 id="acknowledgements">Acknowledgements</h2>
<p>This major update was possible thanks to my parental leave. First, I thank my daughter Rena for being born; this 3.0 version is named in her honor. Second, I thank the mother of the child, my wife <a href="https://www.serenayang.art/">Serena</a>, who is also a full-time UI/UX designer, for providing valuable feedback during the UI design process. I also thank my company BONX Inc for allowing me to take parental leave. Our software development team is continuously <a href="https://herp.careers/v1/bonx/requisition-groups/e05e5be0-0224-4b43-b3a4-a5ac960905f3">hiring</a> in Tokyo, Japan. Lastly, I thank the Japanese government for the parental leave allowance.</p>
<hr />
<p>By the way, Copy as Markdown now has a <a href="https://ko-fi.com/yorkxin">donation link</a>. Please feel free to donate!</p>
<p>(This post was translated from Chinese with assistance from Notion's ChatGPT Translator.)</p>

            ]]></description>
        </item>
        <item>
            <title>Copy as Markdown 3.0: 里程碑</title>
            <link>https://blog.yorkxin.org/posts/copy-as-markdown-3-zh/</link>
            <pubDate>Mon, 01 Jul 2024 04:07:00 +0000</pubDate>
                
            <guid>76d938eb-90ed-4dc0-96f4-3196e6e40f5e</guid>
            
            <description><![CDATA[
                <p><a href="https://github.com/yorkxin/copy-as-markdown">Copy as Markdown</a> 是我自2012年開發至今的瀏覽器擴充套件，主要功能是把網頁上的超連結或分頁轉換成 Markdown，然後複製到剪貼簿。WAU大約一萬人。有在 <a href="https://chromewebstore.google.com/detail/fkeaekngjflipcockcnpobkpbbfbhmdn?authuser=0&amp;hl=zh-TW">Chrome</a>、 <a href="https://addons.mozilla.org/en-US/firefox/addon/copy-as-markdown/">Firefox</a>、<a href="https://microsoftedge.microsoft.com/addons/detail/cbbdkefgbfifiljnnklfhnhcnlmpglpd">Edge</a> 的官方商城上架。一直以來都維持低度維護，主要是工作之餘無暇兼顧。最近因為育嬰假有些空閒可以來改，而隨著育嬰假即將結束，也許不會再有時間改了，所以記錄一下這個里程碑。</p>
<p>過去三個月以來主要做了這些事情：</p>
<ul>
<li>加入基於 Selenium 的 E2E 測試</li>
<li>將大部分權限改為選配</li>
<li>改善了 Firefox 版本的穩定度（主要是改回 Manifest V2）</li>
<li>在 Firefox 加上了複製書籤的功能</li>
<li>在 Chrome 加上了複製分頁群組的功能</li>
<li>加入HTML複製成Markdown的功能</li>
<li>一些程式改善（主要是改成 async/await）</li>
<li>CSS套版，主要是 <a href="https://bulma.io/">Bulma.css</a></li>
</ul>
<p><em>本文亦有英文版。This post is also available</em> <a href="https://blog.yorkxin.org/posts/copy-as-markdown-3-en"><em>in English</em></a></p>
<h2 id="e2e-ce-shi">E2E 測試</h2>
<p>如前所述，向來都是低度維護的情況下，功能很少，測試工具都是靠手動，一時要加上新功能，就要各種瀏覽器各種OS交叉測試，過去12年也不時會把功能改爛。以往都是在 macOS 開發測試，從來沒有在 Windows 和 Linux 桌面好好測試過；最近買了一台二手的 PC，至少有 Windows 和 Linux 可以測。但手動測還是很累，所以研究了 E2E 測試。</p>
<p>所謂 E2E 測試，理想上還是直接發鍵盤滑鼠事件，但實際上要測試瀏覽器擴充套件並不是那麼容易，尤其要與原生UI互動，這其中包括分頁、鍵盤快速鍵、滑鼠右鍵選單、權限警告等。Chrome 官方的<a href="https://developer.chrome.com/docs/extensions/how-to/test/end-to-end-testing">指南</a>並不是很齊全，只大略列出有哪些工具可以用，例如 Selenium、WebDriver、Puppeteer等，但各家的指南也不是很齊全，花了不少時間自己摸索，也曾經考慮 Sikuli 和 Robot Framework，但工具鏈不是很好上手，所以作罷。又如 Puppeteer / WebDriver 是基於 Chrome Developer Protocol 的，只能控制瀏覽器裡面的網頁，摸不到 OS 的 native UI，也不太適合。</p>
<p>最後選用了 Selenium 的 Java 版本，因為可以調用 AWT 的 <a href="https://docs.oracle.com/javase%2F8%2Fdocs%2Fapi%2F%2F/java/awt/Robot.html">Robot</a> 函式庫來對 OS 送出鍵盤滑鼠事件。事實證明這是對的決定，因為 Java 的跨平台特性讓我少掉很多麻煩，Selenium 和 AWT 也都是歷史悠久的工具，要找問答和奇怪的 tips &amp; tricks 並不難。例如：</p>
<ul>
<li>Robot 只能送事件到當前應用程式。用 TestNG 開 Selenium，焦點會在 Test Runner，要用 <code>driver.switchTo().window(handle)</code> 切換視窗焦點。</li>
<li>用 Robot 送出 macOS 的組合鍵需要在 modifier keys 和 non-modifiers 之間加入 200ms 的 delay；在 Windows 則需要視情況呼叫 <code>keyRelease()</code> 。</li>
<li>瀏覽右鍵選單的時候可以直接用鍵盤事件「打字」——但並非每次都會成功。</li>
<li>在 Selenium 裡面只看得到 Window，看不到 Tab，當然也看不到 Tab Groups，因此我另外做了一個 <a href="https://github.com/yorkxin/copy-as-markdown/tree/master/e2e/support/e2e-test-extension">E2E Testing Support Extension</a> 來調用 Chrome 的 API 開啟測試用分頁。</li>
<li>選配權限警告，在 Firefox 可以直接改 <code>extensions.webextOptionalPermissionPrompts=false</code> 關閉警告，在 Chrome 則只需要允許一次，所以就在跑測試前先用鍵盤事件全部允許。</li>
</ul>
<p>全部跑完大約要3分鐘左右，其實還算可以接受，但有點覺得是不是過度測試，而且 Windows 和 Linux (KDE) 還是有些問題無法全部通過。正在想把一些測試改到 Puppeteer，直接呼叫 <code>background.js</code> 的 event handlers 就算數，畢竟我可以假設瀏覽器本身可以正確呼叫鍵盤和右鍵選單的事件。但那又要折騰另一套 framework，所以暫時作罷。</p>
<p>開發的過程中，用了 <a href="https://www.jetbrains.com/aqua/">JetBrains Aqua</a>，及 <a href="https://pastenow.app/">PasteNow</a> 來紀錄剪貼簿內容，很方便，推薦。</p>
<h2 id="xuan-pei-quan-xian">選配權限</h2>
<p>Copy as Markdown 在很初期就有匯出分頁的功能，所以有 <code>tabs</code> 權限。日前加上了 <code>tabGroups</code> 的功能，可以同時匯出Chrome和Edge的分頁群組。當擴充套件要求新的權限時，Chrome就會出現警告。不久後就有零星的用戶抱怨「為什麼要看我的瀏覽紀錄？」而且也有很多使用者選擇解除安裝（大約5%）。原來是 Chrome 的權限警告會顯示「讀取您的瀏覽紀錄」，其實就是存取分頁的 <code>tabs</code> 權限。技術上也沒錯，因為可以拿到分頁的資料就表示可以隨時監控你瀏覽了什麼網頁，但老實說這實在是太嚇唬人了，一般用戶會覺得這個程式會無時無刻監控。</p>
<p>我覺得這個churning實在太傷，所以開始花時間改權限，目標是安裝時不會出現任何權限警告，把所有<a href="https://developer.chrome.com/docs/extensions/reference/permissions-list">會出現警告的</a>都放在選配，讓使用者自行決定，也做了功能讓使用者隨時取消，希望可以藉此止血。</p>
<p>沒錯，我的程式是開放原始碼，但我不可能期待那10000週活用戶都先看得懂程式碼再來用。作為一個實質上的PM，當然有義務改善這部分的UI。這件事的教訓就是，要給使用者選擇是否可以存取敏感的資訊。</p>
<p>在過去三個月裡，除了原本的分頁 <code>tabs</code> 改成了選配，新功能分頁群組 <code>tabGroups</code> 和書籤 <code>bookmarks</code> 也改成了選配。後者必須是選配，因為相關功能只支援 Firefox。</p>
<h2 id="firefox-ban-hui-gui-manifest-v2-zan-shi">Firefox 版回歸 Manifest V2（暫時）</h2>
<p>Manifest 的版本可以簡單理解為瀏覽器擴充套件的 API 版本。現在的最新版是 V3，名義上是某 W3C 群組主導的規格，實務上是 Chrome 挾市場地位主導的。MV3 在 Chrome 已經變成必備，舊版 MV2 已經無法上架，但因為各種原因，Firefox 目前是 MV2 和 MV3 都同時支援。兩者之間的 API 大同小異，主要差異在 Promise，但那些小異已經讓我必須加入一大堆 <code>if (typeof chrome.xyz === 'undefined')</code> ，以及 callback-to-Promise wrapper，相當煩人。</p>
<p>由於 Chrome 力推 MV3，帶著 Firefox 被迫跟上。原本我也是抱持 MV3 會統一天下，加上程式碼的維護考量，所以在 Firefox 上了 MV3。豈料 Firefox 因此出現了神秘的<a href="https://github.com/yorkxin/copy-as-markdown/issues/109">右鍵選單消失問題</a>，我試了好幾種 hack 都無法解決，最後還是退回了 MV2。</p>
<p>然而這就回到了 callback vs Promise API 的問題。本來是覺得只有幾個 API 就自己包，但愈寫愈覺得麻煩，於是就導入了 Mozilla 出的 <a href="https://github.com/mozilla/webextension-polyfill">webextension-polyfill.js</a> ，至少現在大部分的程式都可以用 async/await 寫了。</p>
<p>長期目標還是想要改為 MV3，但現在能動就暫時放著吧，反正有 E2E 測試不怕改爛（？</p>
<h2 id="ming-xie">鳴謝</h2>
<p>這三個月有時間可以來做大改動，是來自育嬰假，首先感謝我的女兒 Rena 出生，這個 3.0 版是為了紀念她而跳的版號。第二是孩子的媽，我的太太 <a href="https://www.serenayang.art/">Serena</a>，她同時也是全職 UI/UX 設計師，在 UI 設計的過程中給了我很多寶貴意見。此外也要感謝我的公司 BONX Inc 讓我休育嬰假，我們的軟體開發團隊長期在<a href="https://herp.careers/v1/bonx/requisition-groups/e05e5be0-0224-4b43-b3a4-a5ac960905f3">徵才</a>（日本東京）。最後也要感謝日本政府的育嬰假津貼。</p>
<hr />
<p>附帶一提，現在 Copy as Markdown 有<a href="https://ko-fi.com/yorkxin">贊助連結</a>，如果想要贊助的話請隨緣～</p>

            ]]></description>
        </item>
        <item>
            <title>為小孩申請日本的定住者簽證（永住者的小孩）</title>
            <link>https://blog.yorkxin.org/posts/japan-long-term-visa-for-child-of-pr/</link>
            <pubDate>Wed, 12 Jun 2024 13:06:27 +0000</pubDate>
                
            <guid>e0d80fed-2a18-4762-ac47-baf3bb3ef6cd</guid>
            
            <description><![CDATA[
                <p>日前我和太太為了生女兒而回到臺灣。但我們都長期住在日本，生完就要回日本工作了。為了帶剛出生的女兒來日本居住，我們為她申請了日本簽證。本文記錄申請的過程。</p>
<p><strong>提醒</strong>：簽證政策和申請規則可能會改變，本文寫在2024年，如果您在太久的未來看到這篇文章，有可能不再適用，請自行查證。</p>
<hr />
<p>我跟太太都沒有日本國籍。因為小孩是在日本國外出生，視同外國人新規入國。原則上必須先取得定住者簽證才能在日本居住有身分。雖然網上可以找到有人先用免簽進日本再申請在留資格變更的記錄 <a href="https://www.ptt.cc/bbs/Japan_Living/M.1676875588.A.FF5.html">(1)</a> <a href="https://double-wings.com/family-visa-application/">(2)</a>，但考慮到就醫問題，所以不走這條路。</p>
<p>小孩的簽證類型，根據sponsor的身份不同而有所不同。我是永住者，帶親生孩子來日本居住，其簽證為「定住者」。前提是sponsor本人已經在日本居住（有住民票）。在日本國內出生的話可以參考<a href="https://www.kifjp.org/child/threeprocedure">這個網站</a>。</p>
<p>順序：</p>
<ol>
<li>取得在留資格認定証明書（Certificate of Eligibility，以下簡稱CoE）</li>
<li>用CoE申請簽證</li>
<li>入境日本，在入境審查取得在留卡</li>
<li>住民登錄</li>
</ol>
<p>時程：</p>
<ul>
<li>2024/03/06 送出CoE申請（線上）</li>
<li>2024/03/09 代理申請人離開日本</li>
<li>2024/04/30 寫信去要求提早審查（EMS國際郵件）</li>
<li>2024/05/10 CoE審查通過（Email收件）</li>
<li>2024/05/13 申請簽證（日本臺灣交流協會高雄事務所）</li>
<li>2024/05/16 簽證取件</li>
<li>2024/05/27 跟小孩一起入境日本</li>
<li>2024/05/28 住民登錄（役所）</li>
</ul>
<h2 id="qu-de-coe">取得CoE</h2>
<p>2023年3月起，可以用マイナンバーカード<a href="https://www.moj.go.jp/isa/applications/guide/onlineshinsei.html">線上申請</a>家人的在留資格證明書，而且可以Email收件。線上申請需讀卡機（可以用臺灣賣的晶片讀卡機），需Windows。操作上很複雜，要有心理準備。辦帳號隔天才能生效，身份要選「法定代理人」。送出申請時代理人要在日本境內（<a href="https://www.moj.go.jp/isa/applications/online/online-QA.html">Q4-17</a>，而且系統會擋海外IP），但是送件後可以出國（<a href="https://www.moj.go.jp/isa/applications/online/online-QA.html">Q4-18</a>）。</p>
<p>申請分成兩個步驟：填表格、上傳資料。首先填完表格後，從登入後的頁面搜尋案件（在搜尋畫面什麼條件都不輸入就能全部列出），然後上傳照片和審查材料，再按下正式提交申請的按鈕。<strong>審查材料只能上傳一次，僅限一個PDF檔案</strong>，上限10MB，包含所有掃描檔，都必須在同一個檔案。可以用macOS的預覽程式<a href="https://support.apple.com/zh-tw/guide/preview/prvw11793/mac">編輯</a>、<a href="https://support.apple.com/zh-tw/guide/preview/prvw1509/mac">壓縮</a>。</p>
<p>關於線上申請填表的問題，入管有<a href="https://www.moj.go.jp/isa/about/region/tokyo/">專線</a>可以打去問。關於審查的問題，可以直接問永住審查部門。</p>
<p>雖然是線上申請，但所有文件都跟現場申請一樣，但不需附回郵信封。所需文件都在入管的官網上有公布：<a href="https://www.moj.go.jp/isa/applications/status/longtermresident04_1.html">在留資格「定住者」（外国人（申請人）の方が「永住者」、「定住者」、「日本人の配偶者等」、「永住者の配偶者等」又は「特別永住者」のいずれかの方の扶養を受けて生活する、未成年で未婚の実子である場合）</a>。</p>
<ul>
<li><a href="https://www.moj.go.jp/isa/applications/procedures/photo_info_00002.html"><strong>相片4x3cm</strong></a><strong>電子檔</strong>：我們是請月子中心的合作攝影師幫忙拍（NT$1,000）。</li>
<li><strong>扶養者の直近１年分の住民税の課税（又は非課税）証明書及び納税証明書</strong>：可以在役所申請
<ul>
<li>所謂最近1年份，在6月以前只能拿到前年度的，在6月以後可以拿到去年度的。總之就申請最新的。不懂的話可以把入管官網的說明給役所的人看。</li>
<li>我是3月申請的，所以只能拿到前年度的。為免入管跟我要去年的納稅證明，我還提供了去年的源泉徵收書和確定申告書（海外所得）。</li>
</ul>
</li>
<li><strong>扶養者の在職証明書</strong>：有沒有寫年收都沒差</li>
<li><a href="https://www.moj.go.jp/isa/content/001373949.pdf"><strong>身元保証書</strong></a>：列印簽名掃描</li>
<li><strong>理由書（扶養を受けなければならないことを説明したもの、適宜の様式）</strong>：列印簽名掃描</li>
<li><strong>申請人の本国（外国）の機関から発行された出生証明書</strong>：英文版出生證明（洽出生醫院），及日文翻譯</li>
<li><strong>代理人與申請人的關係證明文件</strong>：英文版戶籍謄本（洽區公所），及日文翻譯</li>
<li>文件如果不是日文，必須附上<strong>日文翻譯</strong>。可以自己翻譯。在角落寫翻譯者姓名、聯絡電話和日期。</li>
<li>為保險起見，所有我自己寫文件，我都列印簽名再掃描。</li>
<li><strong>不需要先辦護照</strong>就能申請，但需要附上理由書（<a href="https://www.kifjp.org/child/threeprocedure">參考</a>），系統上的護照資訊是選填。但是外文姓名必須全部一致：英文版戶籍謄本=護照外文姓名=CoE申請表=在留卡。</li>
</ul>
<p>3月初送件後，本來打算慢慢等3個月，但因為太太工作的緣故，需要6月以前入境。我在4月底打電話問受理的東京品川入管（永住審査部門），他們說現在案件很多，標準處理期間是3個月。可以寫理由書請他們加速，但不保證可以如願加速。而先用觀光簽入國再變更在留資格，不是不行，但審查要重來，耗費的時間差不多也是三個月。</p>
<p>抱著試試的心態，我在4月底寫了理由書，請入管提前審查（紙本，EMS從台灣寄到永住審查部門），寄達後2週即審核通過。</p>
<p>批下來的CoE只有電子郵件。上面有在留資格認定證明書的編號。很重要，別刪除。</p>
<h2 id="ban-qian-zheng">辦簽證</h2>
<p>要辦簽證必須有小孩的護照。</p>
<p>在日本台灣交流協會申請。有台北、高雄事務所，要去哪一間，原則上看戶籍地，但似乎可以跟交流協會要求在比較近的事務所辦理（未證實，請自行詢問）。我們戶籍在臺南，所以去高雄事務所，人很少。因為是嬰兒所以家長代理即可，本人不需到場。表格在<a href="https://www.koryu.or.jp/tw/visa/kaohsiung/detail2/">交流協會的網站</a>可以下載。</p>
<p>要帶的文件：</p>
<ul>
<li><strong>小孩護照</strong>：繳交正本及個人資料頁影本
<ul>
<li>需簽名。家長代簽，再寫「母代/父代」即可。</li>
<li>外交部領事事務局說家長代簽不會使護照失效。</li>
</ul>
</li>
<li><strong>簽證申請書</strong>：自行打字，列印，最下面簽名。所有欄位預設都是必填。</li>
<li><strong>證件照 4.5 x 3.5 一張</strong>：規格同台灣護照</li>
<li><strong>戶籍謄本中文版正本</strong>：用自然人憑證取得的PDF列印可</li>
<li><strong>在留資格認定證明書</strong>：Email列印</li>
<li><strong>委託書</strong>：家長代寫</li>
<li><strong>代理人身分證</strong>：出示正本，繳交影本</li>
<li><a href="https://www.immigration.gov.tw/5385/7244/7250/20406/190354/190435/"><strong>出入國紀錄（移民署）</strong></a>：可以用小孩的健保卡申請PDF檔，列印出來</li>
<li><strong>代理人在留卡</strong>：官網沒寫但是被要求出示</li>
</ul>
<p>護照收走後，櫃檯會告知取件日期。我是過兩天可以去取件。</p>
<h2 id="ru-jing-ri-ben">入境日本</h2>
<p>取得簽證後就可以入境日本。在入境處需要提示CoE紙本或電子郵件；單憑簽證是不夠的。兒童的在留卡沒有照片。一家三口可以一起擠一個櫃檯。</p>
<p>還發現原來有在留資格的人，持再入國許可入境時不需要出示在留卡，他們只看（みなし）再入國許可。此外CoE申請時的上陸預定港有異動的話其實沒差。</p>
<p>順便抱怨：羽田T3入境審查的嚮導員也不知道我們該走哪個櫃檯。起先看我們有嬰兒推車，帶我們去優先櫃檯（嬰兒/身障用），結果說不能發在留卡，叫我們去隔壁日本人櫃檯，當然是被拒絕了。最後是帶我們插隊一般觀光客的櫃檯。<strong>關鍵句：「在留カードを作りたい。」</strong></p>
<h2 id="yi-suo-zhu-min-deng-lu">役所住民登錄</h2>
<p>取得在留卡還是需要在居住地登記（住民登錄），才能有身份。這點所有在日外國人應該不陌生。</p>
<p>乳幼兒的話，家長代理去即可，小孩不需要出面。帶小孩的護照、台灣的英文版戶籍謄本、日文翻譯（收走）去辦。</p>
<p>マイナンバー本身會立刻發行，但是マイナンバーカード日後才會寄申請用明信片到家。</p>
<p>為了打預防接種，先申請了國民健康保險，當場拿健康保險證，之後再跟公司申請扶養家族健康保險證（手續可以詢問公司的人事部）。預防接種票需要去保健所申請。我帶了台灣的兒童手冊和英文版接種證明，讓保健所的人方便核對。或是自己告知他們需要打什麼疫苗（自我責任）。</p>
<hr />
<p>整體來說覺得入管的規則寫得很詳細，雖然很複雜，但模凌兩可的地方很少，不愧是日本。線上申請可以省下往返入管的麻煩，電子收件也讓我可以回台灣照顧小孩，不用守在日本。但網站的設計真的很差，就好像是2000年代的企業內部資訊系統，幸好我是做Web開發的，大概可以摸索操作方式。其中最麻煩的是檔案只能上傳1次。</p>
<p>最酷的是因為是定住者的在留資格，所以沒有工作限制「就労制限なし」🤯</p>
<p>等待審核的時間是最痛苦的，只能說服自己先不要一直想，好好過日子。不過在留期間只有1年，不知道是不是因為我催促XD</p>

            ]]></description>
        </item>
        <item>
            <title>給小孩的台灣護照取外文名字</title>
            <link>https://blog.yorkxin.org/posts/foreign-name-on-taiwan-passport/</link>
            <pubDate>Thu, 22 Feb 2024 12:02:12 +0000</pubDate>
                
            <guid>ce515444-f81e-482c-b555-73fc4337e042</guid>
            
            <description><![CDATA[
                <p>最近為自己的小孩辦人生的第一本台灣護照。由於有雙重國籍，又我們全家短期之內沒有要回台灣生活的打算，考慮到外國護照和台灣護照的名字如果不同，不只去國外生活會遇到公務體系行政上的困擾，還會給小孩的身分認同增加變數。跟太太討論了之後，決定想辦法在台灣護照上取個外文名字，而不是中文姓名的音譯。</p>
<p>那個外國的台灣代表處規定，台灣英文戶籍謄本上的名字就是該國護照上的官方姓名。又台灣的英文戶籍謄本遵循名從主人原則，只要拿得出證明（如出生證明、外國身分證件），就可以在戶籍謄本上寫這個名字，跟台灣護照加註外文姓名的原則一樣。戶政事務所的人員對英文姓名的寫法很有彈性，可以寫 「[台灣名音譯] a.k.a. [外文姓名]」 並列，也可以單寫「[外文姓名]」，總之只要能提出證明即可——畢竟是我們自己提出給外國機關，有錯我們自己負責。從父姓或母姓還是必須跟中文姓名一致。</p>
<p>在台灣剛出生的孩子還沒有出國，沒有外國的身分證件，所以唯一的方法是請醫院開英文版出生證明。我們孩子出生的醫院對於出生證明上的英文名字的寫法並沒有特別的規定，櫃檯人員聽到是雙重國籍就幫我們辦了。最後也順利在戶政事務所取得英文版戶籍謄本，寫的就是我們給孩子取的英文名字。</p>
<p>接著辦台灣護照，一樣是名從主人，如果非中文音譯必須提交證明，我們提交了醫院開的英文版出生證明和英文版戶籍謄本。在戶政事務所做人別確認的時候，戶所人員有把出生證明掃描上傳到外交部，並交代我們辦護照的時候要提交正本查驗。不過最後外交部是把英文版戶籍謄本收走了。</p>
<p>最終就得到了一本台灣護照上寫的是我們給孩子取的英文姓名，而不是中文音譯。</p>
<p>～～～</p>
<p>雖然還不知道這樣會不會對孩子未來造成混亂，但就我自己在日本生活（搞砸）的<a href="https://blog.yorkxin.org/posts/chungwen-hsingming-tsai-jipen/">經驗</a>，姓名還是一個用到底就好，不要耍帥節外生枝。</p>

            ]]></description>
        </item>
        <item>
            <title>Linux 桌面</title>
            <link>https://blog.yorkxin.org/posts/linux-desktop/</link>
            <pubDate>Mon, 11 Dec 2023 08:12:20 +0000</pubDate>
                
            <guid>51105394-683a-4cb2-9581-152cafb240d0</guid>
            
            <description><![CDATA[
                <p>一直以來都是用Mac，上次用Linux桌面似乎是2010以前的事情了，而且都是嚐鮮性質，從來沒有當主要作業系統來用。最近因爲公司配了一臺PC筆電，用Windows無法開發Linux伺服器軟體[1]，才換到Linux桌面。</p>
<p>跟2010年比起來，現在的Linux桌面系統對使用者比較友好。不止外觀設計變了許多，也有各種新UX的嘗試，不再是單純仿製經典Windows的邏輯。CJK也有了思源黑體和思源宋體。筆電廠商對於Linux桌面的支援也好很多了，至少公司配的Lenovo ThinkPad有列在Ubuntu官方支持名單上，硬體驅動沒什麼問題。雖然後來知道NVIDIA顯卡驅動在Linux上還是有很多問題，但這臺是Intel內顯，也就少了個問題。</p>
<p>目前工作機是用Ubuntu LTS，主要還是爲了各種開發工具的相容性。印象中以前GNOME和KDE大同小異，選擇哪一套其實是基於個人審美。但某一版GNOME大改版，變得極簡風格，發明了新的視窗管理邏輯，聽說還有許多人因此跳到其他桌面系統，也有人fork了舊版的GNOME來維持經典操作邏輯。如今GNOME和KDE有明顯的不同，而非單純的審美差異。我用了一陣子Kubuntu，又換回了Ubuntu (GNOME)，還是看在大部分App只支援GNOME的網路效益。至於UI，也不是那麼難學，反而它借鑑了一些macOS的操作方式，很好上手。套一句雨傘店的廣告：顏色不好看，看久會習慣。</p>
<p>然而有些系統軟體還是很吃發行版和桌面環境。例如輸入法。過去的15年我都在用蘋果的繁體拼音輸入法，生活在日本需要日文輸入法，換到其他作業系統也是需要的。Linux有Rime的繁體拼音可以用，也有mozc的日文輸入法（據說是Google日文輸入法的開源版）。但輸入法畢竟是跟系統高度整合的程式，偶爾軟體更新會遇到問題。在fcitx5和ibus之間換來換去，哪個能用就用，反正都是同樣的Rime、同樣的mozc。之前用Windows內建的日文輸入法偶爾會壞掉，折騰了很久還是裝了Google日文輸入法。回想起來蘋果的多國語言輸入既穩定又包容[2]，輸入法這種東西就是平常用沒感覺，一旦壞掉就很令人煩躁。</p>
<p>又例如羅技鍵盤滑鼠。我用的是MX Keys Mini for Mac和MX Master 3，都沒有官方Linux支援，需要第三方軟體如Solaar，又因爲是系統軟體，在各發行版、Wayland的支援都不盡完美，也許那天軟體更新了就不能用。話說回來，MX Keys的Mac版不如一般版，後者還能切換Mac/Windows佈局，主要是交換Command（Meta/Win）和Option（Alt）的位置。Mac版不能在硬體上交換這兩個按鍵，對於肌肉記憶是一個很大的挑戰（但其實更大的問題是macOS的Emacs風格快速鍵，<a href="https://support.apple.com/zh-tw/HT201236">用Control移動遊標</a>的那一組)。</p>
<p>總體來說，三個月用下來，Linux桌面給我的感覺是堪用、夠用。總是會有一些地方壞掉、當掉，需要在網上找答案，也許因此我對於各種完美的要求降低了。以前會很喜歡macOS應用程式所謂的原生界面設計，但離開macOS之後發現，Windows內建的App都沒有統一的，各種常用的生產力工具也都有自己的UI元件，尤其是Electron做的App，甚至字型渲染都沒有統一。如果我們都能用這些軟體來工作，那麼所謂「原生UI」似乎是個假議題：你可以選擇用原生SDK做一個App，但因爲一個App不夠原生就拒絕使用，豈不是把個人審美放在生產力之上，本末倒置。</p>
<hr />
<p>[1]: 對，我嘗試過Windows WSL，但實在不喜歡折騰在兩個作業系統之間的不相容問題：換行字元、IDE相容問題、Docker、虛擬機橋接網路等。後來我認定直接在Linux上面開發Linux伺服器軟體，比在WSL簡單許多。</p>
<p>[2]: sort of 包容，至少macOS/iOS是唯一內建臺灣發音繁體漢語拼音的作業系統。字形輸入法如無蝦米就只能自求多福了。我也是因爲十年前iPhone不能打無蝦米也不習慣動態注音鍵盤，所以才學了漢語拼音，從那之後就再也離不開拼音。</p>

            ]]></description>
        </item>
        <item>
            <title>6年9個月又4天，或2468天</title>
            <link>https://blog.yorkxin.org/posts/japan-pr/</link>
            <pubDate>Wed, 31 May 2023 00:05:13 +0000</pubDate>
                
            <guid>646c22e2ca3ec79153db62cc</guid>
            
            <description><![CDATA[
                <p>六年九個月，這是我移居日本到取得日本永住權經過的歲月。</p>
<p>對一些人來說，二十五到三十五歲不只是問卷調查上的一個年齡段選項，還是人生最充滿幹勁的精華時期。最幸運的那些人，沒有家庭的壓力，可以毫無後顧之憂地在事業中成長。我想我也是那群幸運的人之一。</p>
<p>而我就在日本度過了這個精華歲月。這個國家給了我永住權，還是百分之百是我自己努力來的，通過<a href="https://www.moj.go.jp/isa/publications/materials/newimmiact_3_system_index.html">高度人才</a>資格申請的永住。</p>
<p>事實上申請的過程中一直自我懷疑：真的這麼簡單嗎？Just like that? 這個國家認同我是「高度」人才嗎？明明放在台灣就是個普通的軟體工程師而已，普通大學本科畢業，沒有高學歷，收入也沒有特別高，幾乎就是壓線過關的。就因為過了最低標準，一般要等十年的永住申請門檻，我六年出頭就可以辦了。偶爾會參考其他西方國家的工作簽證，甚至台灣的<a href="https://goldcard.nat.gov.tw/en/qualification/">就業金卡</a>，雖然申請永久居留證的居住要求很短，但資格門檻要求很高，相較之下，日本真的對我這種普通人很友善。</p>
<p>但是拿到了就是拿到了，從此人們不會再問我簽證效期剩多久、有沒有回國的打算、拿工作簽裸辭會不會被遣返等等。就像當初勉強高中畢業、勉強大學畢業、勉強考到駕照、勉強考過日檢，過了就過了，人們只看那張證照。</p>
<p>＊＊＊</p>
<p>來日本之前是抱著一個自大的心態。那時候前一家公司倒閉不久，正在求職，但過程不是很順利。也不是台灣沒有機會，就是個自大而已，自以為值得更高的薪水，以當時我的能力是高攀的。再加上我是個北漂的台南孩子，在台北住了八年從來沒喜歡過，於是就有了離開台北的藉口：既然都要改變生活方式，那就出國吧。</p>
<p>因為學了一些日語，就抱著試試的心態去找了日本的<a href="https://www.wahlandcase.com/home">人力仲介</a>，用蹩腳的日語，找到了第一份工作。加上當時還是個動漫宅，對日本生活存有粉紅泡泡的幻想，於是就踏上了赴日之旅。</p>
<p>現在回想起來，移居日本大概是我人生中做過的決定之中，最值得的一個。</p>
<p>東京在一些方面比台北來得宜居。天氣不像台北那麼濕冷悶熱，衣服曬得乾。日本的租房市場比台灣成熟，雖然一定得透過仲介，但CP值是很穩定的。日本是行人的天堂，行人路權受到尊重，且人行空間很舒適；或應該說台灣對行人不友善是舉世聞名的。東京的大眾運輸以鐵路為首，四通八達，出行不需仰賴機車汽車 [1]。</p>
<p>這些剛好都是我很重視的生活條件，在台北找不到的，在東京找到了。</p>
<p>＊＊＊</p>
<p>在日本的生活也讓我得到了意想不到的收穫。</p>
<p>第一是<strong>英文能力</strong>。雖然英語不是東京的通用語，但在軟體業有一些公司只要求英語能力。文化上會更接近台灣人想像的美國外商，卻又不是外商。因為必須遵守日本勞動法，該有的社會福利都有，不像美國公司那樣把健保當福利，說解僱就解僱。而且不知何故，這些工作的起薪也比一般日企高，外國人當主管的也比比皆是，明明做的事情都差不多。</p>
<p>通英語的公司對英語的要求也通常不是那麼嚴格，畢竟大家都是從外國來日本討生活的，聽得懂就好，沒有人在管發音夠不夠道地。我就曾在一家充滿各國人的公司用英語上班，結果就是我在日本竟然把英語練起來了，還混了台美英澳星馬日口音。以前英文老師總是警告，口音不正胡言亂語，顯然在日本是不通用的；反而，只要講得出口，就有許多機會。</p>
<p>第二是<strong>下廚</strong>。台北大部分的單人套房不附廚房，日本就算是六坪的單人套房都有廚房。日本不像台灣有那麼多早餐店，外食也很貴，為了存錢不得不自己煮。剛來的時候，為了做三餐費盡了心思，從跟媽媽求救，到可以讀懂日本食譜；從張羅一餐兩小時，到現在俐落地每天準備一家兩口三餐。這是無數次的失敗累積來的。聽人家說台灣人一出國唸書就會變食神，至少我是學會了怎麼做台菜孝敬老婆。</p>
<p>第三是喜歡<strong>走路</strong>。由於對行人友善，我在東京養成了散步的習慣。最開始會去觀光景點，慢慢擴展到觀光景點附近的小徑小溪小山，最近兩三年會在住家附近的巷子到處漫步，或搭車去一個近郊車站，走幾公里到另一條鐵路的車站。以前連一個台北捷運600公尺的站距都懶得走，現在週末有空的話就會走個10公里。</p>
<p>最後是在東京遇見了<strong>人生的伴侶</strong>。雖然因為同為外國人，期間經歷了長久的遠距離關係，也因COVID而有長達一年無法見面，但最終我們還是在日本定居了。如果有小孩的話也可以靠我的永住權定居在日本，也少了一個煩惱。</p>
<p>現在想想，如果我沒移居日本的話，這些都不會出現在我的人生吧，至少不會是2023年。</p>
<p>＊＊＊</p>
<p>2468天。永住是一個淡淡的節點，除了在留卡換了一張，公所叫我去更新IC卡，其他似乎沒什麼改變；在台灣遞交結婚書約時的衝擊感還比較大，但也有 “just like that?” 的衝擊感。也許生活在法治國家的好處就是，公務員照章辦事，該給你的就給你，該是你的就是你的，蓋個章，發個文件，你就有了新的身分。Just like that。</p>
<p>而日子還是要過。</p>
<hr />
<p>[1] 拿東京和台北的鐵路來比較其實很不公平。東京在20世紀初有郊外鐵路大拓荒時代，連結周邊衛星城鎮、名勝地或運輸砂石等資源。東京腹地大，城際鐵路途經之地當年多為農田，土地容易取得。現代的一出站就到家，多為都市擴張的結果，最初不見得是為了服務居民興建鐵路，更何況這些鐵路很多是私營的。台北先有都市化才有捷運，盆地造成腹地有限，無法形成廣域衛星城鎮，加上崇拜美國公路建設，城際鐵路沒有政治條件也沒有市場條件。台灣的都市若要以東京為目標發展鐵路，其實是要追趕一世紀份的進度。</p>

            ]]></description>
        </item>
        <item>
            <title>Terraform Merge: An Unpleasant Journey</title>
            <link>https://blog.yorkxin.org/posts/terraform-merge/</link>
            <pubDate>Tue, 07 Mar 2023 09:03:55 +0000</pubDate>
                
            <guid>640695ee2c80f86b62dedae4</guid>
            
            <description><![CDATA[
                <p>Managing AWS resources with <a href="https://www.terraform.io/">Terraform</a> has been a significant part of my job recently. Our team used to organize Terraform templates by microservices, but we finally hit a limitation: hidden mutual dependencies that make it too hard to work on without following a long operation manual, which cancels a lot of benefits that came with Terraform. Therefore, we decided to merge multiple templates into one, and I was in charge of this task.</p>
<p>I'll explain some backgrounds at the end, but I'd like to focus on how I did the merger because if you find this article, chances are that you have 10 reasons backing your decision to merge multiple Terraform templates, and want to prevent recreating the production resources; YMMV, but you find here.</p>
<p>All I can say is that it was an unpleasant journey.</p>
<p>At first, I thought it is as simple as:</p>
<ul>
<li>Copy-paste the existing code and put them into modules (folders)</li>
<li>Pass variables from the root module to sub-modules</li>
<li>Find all the resources from the old state file</li>
<li>Figure out the address of the new resources (prefix the resource's address with <code>module.${new_module_name}</code>)</li>
<li>Run batch <code>terraform import</code> commands on all the resources</li>
<li>Run <code>terraform plan</code> until Terraform is not planning to destroy or create any resources.</li>
</ul>
<p>Code modification was easy. Listing all resources was not hard (although I created a script to extract them). However <code>terraform import</code> was not as easy as it seems.</p>
<p>A typical Terraform Import workflow is usually:</p>
<ul>
<li>Find the identifier of the resource</li>
<li>Run <code>terraform import</code> to add the resource to the state file</li>
</ul>
<p>Sounds easy? But it won't work for all types of resources.</p>
<p>Import is a feature that must be implemented in the provider itself. So these situations exist:</p>
<ul>
<li>The identifier depends on the implementation of the provider – it may not be obvious. It may be an AWS ARN, the resource's name displayed on the AWS Console, or some combination of its parameters. Please refer to the manual.</li>
<li>Resources may support <code>import</code> command, but not all attributes will be imported, because not all attributes can be fetched from (AWS) API endpoints, e.g. passwords. In this case, Terraform <em>may</em> recreate the resource.</li>
<li>Resources may not support <code>import</code> command at all. Terraform will think they don't exist and create new instances.</li>
</ul>
<h2 id="the-id-guessing-game">The ID Guessing Game</h2>
<p>My approach is trial-and-error — assuming all resources can be imported with the <code>id</code> attribute in the Terraform State file, and if the Import command complains, check the documentation and find out what should be used as an ID. Most of them work. Just some exceptions. Some.</p>
<ul>
<li>The ID of <code>aws_cloudwatch_log_stream</code> is <code>${log_group_name}:${name}</code></li>
<li>The ID of <code>aws_iam_user_policy_attachment</code> is <code>${username}/${policy_arn}</code></li>
<li>The ID of <code>aws_appautoscaling_target</code> is <code>${service_namespace}/${resource_id}/${scalable_dimension}</code></li>
<li>The ID of <code>aws_ecs_task_definition</code> is the ARN, obviously</li>
<li>The ID of <a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule#import"><code>aws_security_group_rule</code></a> is a combination of <em>"<code>security_group_id</code>, <code>type</code>, <code>protocol</code>, <code>from_port</code>, <code>to_port</code>, and source(s)/destination(s) (e.g., <code>cidr_block</code>) separated by underscores <code>_</code>)."</em> (cited from the official document)</li>
</ul>
<p>Most resources I have encountered fall into some simple combinations of their attributes. The case of <code>aws_security_group_rule</code> is especially complicated. I assume there are some legacy compatibility issues, so I am not here to judge it. Just for your convenience, here is the JavaScript code I was using (not guaranteed to work for all combinations):</p>
<pre data-lang="javascript" class="language-javascript "><code class="language-javascript" data-lang="javascript">&#x2F;&#x2F; attrs: the attributes property of that resources&#x27;s instance from Terraform state
function aws_security_group_rule(attrs) {
    let source = &quot;&quot;;
    if (attrs.cidr_blocks.length !== 0) {
        source = attrs.cidr_blocks.join(&#x27;_&#x27;);
    }  else if (attrs.source_security_group_id) {
        source = attrs.source_security_group_id;
    } else {
        console.error(&quot;CANNOT CONSTRUCT source OF aws_security_group_rule&quot;, attrs)
    }

    if (attrs.self === true) {
        source = `self_${source}`;
    }
    return `${attrs.security_group_id}_${attrs.type}_${attrs.protocol}_${attrs.from_port}_${attrs.to_port}_${source}`;
}
</code></pre>
<h2 id="secrets">Secrets</h2>
<p>Furthermore, I encountered some resources that are <a href="https://github.com/hashicorp/terraform-provider-random/issues/106#issuecomment-871579571">not possible</a> to be imported by simply running <code>terraform import</code>:</p>
<ul>
<li><a href="https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password#import"><code>random_password</code></a></li>
<li><a href="https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string#import"><code>random_string</code></a></li>
</ul>
<p>As documented (links above), importing the result of the random-generated strings may still trigger re-creation on the next plan. The reason is that Terraform keeps a track of the random string's recipe (upper case, numbers, length, etc.) in the state file. <code>import</code> does not put those parameters into the state file when importing the value (because how do you know its recipe when you only have the dish, I mean, result?), therefore Terraform may think that the value does not match the recipe and try to recreate a new password, which invalidates the old password. To avoid this, refer to the documentation above, or just edit the state file and copy-paste the recipe (everything under <code>attributes</code>) from the old state file.</p>
<p>Other than the tainted value problem, you may also encounter political problems when importing <code>random_password</code>. As described above, to import a random value, you have to specify the random value's result to import, which is the actual secret password it generated, in the command line, that may stay in your shell history. This may sound like a security issue, but the fact that managing secrets in Terraform templates is already a security risk — the generated password is stored in the state file anyways, so anyone who can read the state file, i.e. anyone who can run Plan, can retrieve the password already — so, in reality, this is not more dangerous per see. But for your job security, check your company's security policy before doing this, whether or not it makes sense to you.</p>
<p>In addition to random values, if you have provisioned a <a href="https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/private_key"><code>tls_private_key</code></a> in Terraform, you'll find it not possible to import, too. It is arguable whether or not creating a private key in the template is a good idea, but if the only thing you can do is import the state, in this case, the only solution is to copy-paste from the old state file.</p>
<h2 id="false-positive-drifts">False-Positive Drifts</h2>
<p>Besides, some resources may trigger replacement because the Import was not implemented completely:</p>
<p>The <code>master_password</code> of an RDS instance cannot be imported. If this password is assigned by a <code>random_password</code>, make sure the random password won't be re-created (see above).</p>
<p>The secret value of <code>aws_secretsmanager_secret_version</code> might be marked as "need to change". In this case, if you are certain that the secret values won't change, likely, Terraform is just trying to mark the <code>secret_string</code> attribute as sensitive. The changes are internal only; they will not refresh the secret itself. To get rid of this, try manually editing the state file and modify to the <code>sensitive_attributes</code> array.</p>
<p>EC2 instances <code>aws_instance</code> may still trigger a replacement because Terraform <a href="https://github.com/hashicorp/terraform-provider-aws/issues/16567#issuecomment-966532464">believes</a> <code>network_interface</code> should be recreated. I am not sure what condition triggers this behavior, but at least I workaround this issue by manually copying <code>network_interface</code> from the old state file over to the new one:</p>
<pre data-lang="diff" class="language-diff "><code class="language-diff" data-lang="diff">             &quot;network_interface&quot;: [
+               {
+                 &quot;delete_on_termination&quot;: false,
+                 &quot;device_index&quot;: 0,
+                 &quot;network_card_index&quot;: 0,
+                 &quot;network_interface_id&quot;: &quot;eni-XXXXXXXXXXXXXXXXX&quot;
+               }
            ],
</code></pre>
<h2 id="the-harmless-re-creations">The Harmless Re-Creations</h2>
<p>Some resources simply don't support <code>terraform import</code>, so when running <code>terraform plan</code> it will create new resources. But the only case I have encountered, <a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate_validation"><code>aws_acm_certificate_validation</code></a> , is harmless if they are recreated, so just let it run.</p>
<hr />
<p>Other than the above resource ID issues, here are some tricks that helped me:</p>
<ul>
<li>Terraform 1.3.x will try to <a href="https://github.com/hashicorp/terraform/issues/32146">evaluate</a> the whole template before importing the resources. This slows down the process, and sometimes Terraform refuses to run when there are hidden dependencies (dynamic <code>for_each</code> being one example). I was using Terraform 1.2.9 to avoid this problem.</li>
<li>Running <code>terraform import</code> requires a <code>-var-file</code>. If you are using Terraform Cloud, just specify a file that has nothing in it.</li>
<li>Editing Terraform state file might be helpful if you don't want to deal with unwanted changes on the next Plan. It's simple: <code>terraform state pull &gt; state.json</code>, edit it, increase <code>serial</code> in the root, then <code>terraform state push state.json</code>.</li>
<li><code>terraform import</code> also requires read access to the resources that you want to import into the state. So for example, if the template uses some cross-account AWS credentials or providers from other cloud services, you'll want those credentials to be ready in your shell before running.</li>
</ul>
<hr />
<h2 id="how-did-we-end-up-here">How Did We End Up Here</h2>
<p>Prior to this merger, our templates were organized by microservices. There is a 'base' template for the resources that all the microservices depend on (e.g. network), then for each microservice, we have a template, that reads <code>terraform_remote_state</code> created by the base template, then builds its resources on top of them.</p>
<p>At first, we thought it will be a dependency tree, that there will be no circular dependencies. It late became clear that there are hidden dependencies managed outside Terraform's control, but back in the beginning we thought the dependencies can be easily found by reading the code, and we were able to manage them: a microservice A that wants to use resources from microservice B, and B may require resources created by A. Before we can refactor the template and create a dependency C that both A and B depend on, we must rollout to production, and proceed to another project. Cross-workspace references of <code>terraform_remote_state</code> can easily end up with mutual dependencies that we cannot manage easily, and at some point, we have to write a long operation guide, describing the dependencies of each microservices, and a step-by-step deployment guide to provision everything.</p>
<p>In short, <strong>starting with multiple Terraform templates is a premature optimization</strong>.</p>
<p>If you are about to adopt Terraform into your infrastructure management, and you are not sure whether to create one template or many, read the <a href="https://developer.hashicorp.com/terraform/cloud-docs/recommended-practices">Terraform Recommended Practices,</a> and <strong>put all things together as possible as you can</strong>, until you hit the border of the organization's structure (e.g. you have a DNS that is not managed by your team). Someday down the road, you'll find a necessity to split the mono-repo template into multiple ones, and by that time, you'll have a better idea of what should be separated. Probably you'll have an SRE team supporting you at that time. Avoid premature optimization at all costs.</p>
<hr />
<p>Finally, this is not to say that Terraform isn't perfect and should be avoided. No tool is perfect. The benefits that Terraform brings to our team have exceeded the overheads that comes with it. Personally, I prefer Terraform (and similar declarative, type-checked Infra-as-Code tools) to e.g. AWS CDK. For the foreseeable future, I believe this will still be an important part of my work.</p>

            ]]></description>
        </item>
        <item>
            <title>2022年納め</title>
            <link>https://blog.yorkxin.org/posts/2022-osame/</link>
            <pubDate>Tue, 27 Dec 2022 02:12:53 +0000</pubDate>
                
            <guid>63a903d549a6da17a9cea04b</guid>
            
            <description><![CDATA[
                <p>隨意紀錄一下2022年的人生。納め (Osame) 指的是收尾。</p>
<p>2022年心態上最大改變是不再堅持細節了。日語常說人有「こだわり」(Kodawari)，可以形容一個職人對自己的工藝有堅持，也可以形容任何人對枝微末節過度拘泥。我以前該算後者吧，很多事情明明非我所及也堅持到底。今年在工作和生活都嘗試著不再計較細節，怎麼方便怎麼來，怎麼順眼怎麼來，學習睜一隻眼閉一隻眼，學習放過自己，學習接受現實。人生變得比較自在。</p>
<h2 id="gong-zuo">工作</h2>
<p>還在新創打拼。日本的新創從來不像矽谷那樣充滿幹勁和冒險精神，但累應該都是一樣累。當基層也不是那麼輕鬆，福利也是基本款。偶爾有一些高薪福利佳還發股權的也是屈指可數，並非常態。但也因為公司小，所以有很多討論空間，也不太有傳統日商的規訓，所謂規矩都不是聖經，都是可以商量的。最近我們組也多了許多外國人，雖然大家日語都很溜，但文件都寫英文了，偶爾也能用英語跟同事戰技術，在日本不必被苛求日語能力，還能練兩種外語，也算是一種小確幸吧。</p>
<p>有時候會懷念之前在大公司的日子，既不用擔心工作 (work) 哪裡來，也不用擔心錢，福利又好。現在想想，大概是還存著最後一絲少年心，想要拼一把的心情吧。說好聽一點是有挑戰精神，說難聽一點就是坐不住、定不下來。在這個歲數，也許是該收心了吧。以前前輩在說要安穩我都不信，現在我是真的懂了。</p>
<p>偶爾會想自己是不是拿的工資不夠資格，但到了年底，左耳聽說誰家裁員，右耳聽說誰家招人開的預算只有我工資的六成，就想說既然還存得了錢，手頭變緊了也還算緩衝範圍內，沒被公司共體時艱就感恩了。</p>
<h2 id="zhuan-ye">專業</h2>
<p>儘管新創比較累、風險比較高，但挑戰比較多、成長速度比較快。2022年我的科技樹都點在 Terraform、AWS 和 Go/gRPC。有厲害的同事帶路，學起來也特別快，概念一點就通的那種。</p>
<p>Terraform 比 CloudFormation 好的地方在於有自己的程式語言，後者 YAML 寫久了都在 debug YAML 本身。Terraform 和 CFN 都是 declarative 的，你寫的是成品最後的形狀，而不是裝機過程的腳本。AWS CDK 跟普通的腳本式裝機沒什麼兩樣，只是你可以選擇自己喜歡的程式語言，通常這就代表不同組用不同語言的混亂，再乘上各種語言的眉眉角角。而且最後還是生成 CFN template，為了部署 CDK 還要先部署一堆基建，又 vendor lock-in，何必呢？總之 Infra-as-Code 我現在還是比較看好 Terraform 的。</p>
<p>再說 AWS 吧，我廠主要還是用 AWS，但因為一些業務需求，有研究一下 Google Cloud 和 Azure，最後覺得要跑伺服器的話，還是 AWS 比較成熟，其他家都是追著 AWS 的車尾燈在跑。誠然 AWS 也是有許多細節不是那麼完美，但光考慮穩定度和靈活度，AWS 還是我心裡的雲端第一選擇。2022 年練了許多 ECS 的功，還把一套在 ElasticBeanstalk 跑的 API service 搬到 ECS 了，當然全部是用 Terraform 設計的。大概可以自信地說自己是系統架構師了吧。</p>
<p>再說 Go 跟 gRPC 吧。這大概是我這一年最成功的轉換跑道。2020 年我還在 Ruby on Rails 的世界裡打滾，遇到的各種挫折都覺得這不就是後端工程師的人生嗎？亂糟糟但是可以賺錢啊，公司先活下去再求好啊，it pays my bills 啊。後來研究了 Go 一陣子，才發現他們的世界並不像 Ruby 那麼亂七八糟，而是有條有理的，開發速度也快，新創也愛用，速度也快。有 static typing 就先避開了一大堆的問題。Go 寫起來行雲流水，GoLand 就像是手把手在幫我寫程式，頓時懷疑為什麼 Ruby 要把編程搞得這麼痛苦。以前討厭 IDE 單純是基於 RubyMine 太肥太慢，這還得歸咎於 Ruby 本身沒有 type checking，巧婦難為無米之炊。現在 Ruby 3 才在慢慢追上，工具鏈還像是紙糊的。總之，Go 是我相見恨晚的程式語言，原來寫程式可以這麼愉快。接下來幾年大概都要託他照顧了吧。</p>
<p>OAuth 2 繼續在我的工作中陰魂不散，今年認真學習了 PKCE 的流程以及 SAML 2.0，仔細拆開來看哪裡可以駭。偶爾讀一些 security breach 的新聞，也體會到這世界上沒有完美的系統安全，只有風險控管。</p>
<h2 id="li-cai">理財</h2>
<p>如果我要選個年度 Emoji 的話大概是「💰」，因為 2022 年的日本經濟像雲霄飛車：日圓貶值、通膨沖天、央行升息，造成我沒幾天就有想要讀懂經濟新聞的焦慮，有時候還會後悔大學時沒跟同學去上總體經濟學的通識。這些新聞沒幾天就會出現一次，又在考慮買房子所以學了房貸101。我覺得這一年都在惡補經濟學的基本知識。</p>
<p>手頭既然還有餘裕就很想要投資，畢竟日圓放一天就會變薄，最近是變得特別薄（關鍵字：<a href="https://www.jil.go.jp/kokunai/statistics/covid-19/c20.html">消費者物価指数 (CPI)</a>），至少也得抗一下通膨吧。這是很有實感的，兩年來換了3次工作，工資沒漲多少，但手頭變緊了。以前在台灣工作的時候，存錢就是為了去日本玩，其他就是月光了，沒閒錢投資，就算有也是亂買一通，最近重新看了一遍 portfolio 才發現風險相當高。最近日本政府出台新的 NISA 免稅投資制度，接下來應該會認真用它了。這又是另一個前輩們耳提面命我卻當耳邊風的東西。也不知道算不算 FOMO 焦慮，就是每年都在後悔去年沒投資的那種。</p>
<p>但說到底，開源還不如節流來得有效。受到某些 Minimalism 極端派的影響，覺得信用卡是一種借錢花錢、所有的現金回饋都是消費主義，所以 2020 年以來就慢慢減少信用卡的使用，能用 Debit 卡就用，但又擔心 Debit 被盜刷整個戶頭被清空。現在在用的是 Kyash，這其實算儲值卡，可以跟銀行綁定自動加值、Visa 實體卡可以感應刷卡。COVID 之後「無現金社會」成為日本消費經濟的關鍵字，QR 掃碼支付的普及度在這兩年提升很多，但我還是懶得用掃碼，能刷卡就刷卡，留個 Line Pay 備用來刷 PayPay 條碼和 Apple Pay 感應。儲值卡的消費紀錄跟 Debit 一樣即時反應，每個月對帳比較輕鬆（信用卡有隱形的未請款消費額度），比較容易把握收支。缺點是現在要管理兩張儲值卡。要是 Kyash 可以支援 Apple Pay Visa 就好了。</p>
<h2 id="yue-ting">閱聽</h2>
<p>Podcasts 依然是我生活中很重要的資訊來源。2022 年很滿意且 2023 年會繼續追的 Podcast 節目如下：</p>
<ul>
<li>跟錢有關：<a href="https://www.npr.org/sections/money/">Planet Money</a>、<a href="https://www.npr.org/podcasts/510325/the-indicator-from-planet-money">The Indicator by Planet Money</a>、<a href="https://shows.acast.com/the-new-bazaar">The New Bazaar</a>、<a href="https://www.bbc.co.uk/programmes/p02nrss1/episodes/downloads">More or Less: Behind the Stats</a></li>
<li>滿足好奇心：<a href="https://99percentinvisible.org/">99% Invisible</a>、<a href="https://www.20k.org/">Twenty Thousand Hertz</a>、<a href="https://podcasts.apple.com/tw/podcast/%E6%97%A5%E7%9F%A5%E5%BD%95/id1527514372?l=zh">日知录</a>、<a href="https://shenghuomanyou.com/">生活漫游指南</a>、<a href="http://www.tianshuguangbo.com/">天书广播</a> （歷史研究）、<a href="https://www.npr.org/podcasts/510351/short-wave">Short Wave</a> （科普）、<a href="https://lingthusiasm.com/">Lingthusiasm</a> （語言學）</li>
<li>時事深度探討：<a href="https://etw.fm/">声东击西</a>、<a href="https://www.wnyc.org/shows/pulse">The Pulse from WHYY</a></li>
<li>哲學：<a href="https://podcast.weareones.com/">迟早更新</a>、<a href="https://bukelilun.com/">不可理论</a>、<a href="https://www.casticle.fm/">Casticle</a></li>
<li>軟體工程：<a href="https://avocadotoast.live/">牛油果烤面包</a>、<a href="https://changelog.com/podcast">The Changelog</a></li>
<li>其他：<a href="https://podcasts.apple.com/tw/podcast/%E6%95%85%E4%BA%8B-fm/id1256399960">故事 FM</a> （人間異語）</li>
</ul>
<p>今年又重新拾起 RSS 閱讀的習慣。故事應該跟許多人一樣：受不了社群媒體的演算法推薦，想要捏一套自己的口味。上次大量使用 RSS 大概是 2013? 的時候了。這次除了重新找回一些台灣軟體工程師的 blog，也開始嘗試閱讀英文長文。此外 YouTube 其實還保留了 RSS 功能，從 Feed Reader 訂的話，可以略過首頁的推薦區。最近有訂閱的是 <a href="https://www.youtube.com/feeds/videos.xml?channel_id=UC2C_jShtL725hvbm1arSV9w">CPG Grey</a>、<a href="https://www.youtube.com/feeds/videos.xml?channel_id=UCHnyfMqiRRG1u-2MsSQLbXA">Veritasium</a> 和 <a href="https://www.youtube.com/feeds/videos.xml?channel_id=UCLXo7UDZvByw2ixzpQCufnA">Vox</a>。閱讀器用的是 NetNewsWire 搭配 FeedBin 雲端同步。</p>
<p>新聞閱讀則是儘量避免看台灣的新聞網站，太亂了。因為住在日本，主要還是上《Yahoo! ニュース》讀新聞，中文新聞主要看《BBC中文網》。科技新聞則首先看《The Verge》，但英文閱讀還是太慢，所以很仰賴他們的影音評測。《報導者》有感興趣的主題會偶爾看一下。</p>
<h2 id="yu-le">娛樂</h2>
<p>該放鬆的時候完全不想看到日文和英文，而且2022年一直在尋找中文的環境，所以重新開通了 Spotify 台灣帳號。今年的洗腦專輯是《<a href="https://open.spotify.com/album/3UcgDBH5bUky3YRHKaYp6o">自本</a>》和《<a href="https://open.spotify.com/album/7zs78ybiX4moWNH1sS7axW">ponso no Tao</a>》（我聽不懂達悟語，只覺得旋律很魔幻）。</p>
<p>Apple TV+ 和 Netflix 都有訂，這兩家是日本少數提供中文字幕的串流。主要看美國和韓國番劇：</p>
<ul>
<li>改變了世界觀：創造安娜 <em>Inventing Anna,</em> 白金業務員 <em>White Gold</em>, 黑錢勝地 <em>Ozark</em>, 小女子 <em>Little Women</em> (All Netflix)</li>
<li>改變了人生觀：愛情不設限 <em>Love &amp; Anarchy</em>, 好萊塢 <em>Hollywood,</em> 碎片人生 <em>Pieces of Her</em>, 非常律師禹英禑, 我們的藍調時光, 塔盧拉 <em>Tallulah,</em> 阿穆的生存之道 <em>Mo,</em> 馬男波傑克 <em>BoJack Horseman</em> (以上 Netflix),  <em>Swan Song</em>, <em>Pachinko</em>, <em>The Morning Show</em> (以上 Apple TV+)</li>
<li>燒腦爽片：<em>Severence</em>, <em>For All Mankind</em> (以上 Apple TV+), 冒牌帝國 <em>Fakes</em>, 毒梟聖徒 <em>Narcos-Saints,</em> 下流正義 <em>The Lincoln Lawyer</em>, 籠中鳥 <em>Inside Man</em> (以上 Netflix)</li>
</ul>
<p>值得一提的是看了《悲慘世界》2012年電影版，整整lag十年！小時候看過1995年演唱會版，所以腦子裡一直跟那個版本比較。那時候中二年紀小不懂事，連劇情都看不懂，老師帶著全班看，我也不懂他在淚推個什麼意思。看了電影版總算是把劇情補上了，才懂這部音樂劇要有點社會經驗才能懂。但總覺得演唱會版本整體上的演唱比較棒，電影版的除了安海瑟薇演的芳婷唱得很有張力，其他的就像在趕進度似的。也注意到主教面熟面熟，一查才知道是1995的尚萬強跑來演主教。我想是一種救贖吧。</p>
<h2 id="man-you">漫遊</h2>
<p>自從 Covid-19 以來我就漸漸養成了散步的習慣，從以前公館-台電大樓都嫌遠，到現在10公里算普通，也是自己的一大改變。東京走起來比台灣舒服，來東京玩過的人應該都知道。可以說2020年是散步元年、到了2021年，即時不看手機地圖也不太會在東京迷路了。</p>
<p>今年的走透透主要是為了在東京買房子，至少要知道哪裡好住哪裡很亂。實際造訪也不是不行，畢竟喜歡走路，也算是探險。但是東京太大了，總不能全部走透透再說要住哪。所以讀了不少東京地理大觀的雜書，大多是日本常見的捏他集，網路上偶爾也會有人整理的那種，不值一提。不過藉此重新發現我對地圖很有興趣，所以讀了今尾恵介的地圖識讀系列：《<a href="https://www.amazon.co.jp/%E5%9C%B0%E5%9B%B3%E5%B8%B3%E3%81%AE%E6%B7%B1%E8%AA%AD%E3%81%BF-%E4%BB%8A%E5%B0%BE-%E6%81%B5%E4%BB%8B/dp/4807164694">地図帳の深読み</a>》、《<a href="https://www.amazon.co.jp/%E5%9C%B0%E5%9B%B3%E5%B8%B3%E3%81%AE%E6%B7%B1%E8%AA%AD%E3%81%BF-100%E5%B9%B4%E3%81%AE%E5%A4%89%E9%81%B7-%E4%BB%8A%E5%B0%BE%E6%81%B5%E4%BB%8B/dp/4807165879">地図帳の深読み 100年の変遷</a>》、《<a href="https://www.amazon.co.jp/%E5%9C%B0%E5%9B%B3%E9%89%84%E3%81%AE%E3%81%99%E3%81%99%E3%82%81-%E6%98%AD%E6%96%87%E7%A4%BE-%E6%97%85%E8%A1%8C%E3%82%AC%E3%82%A4%E3%83%89%E3%83%96%E3%83%83%E3%82%AF-%E7%B7%A8%E9%9B%86%E9%83%A8/dp/4398147691">地図鉄のすすめ</a>》、《<a href="https://www.amazon.co.jp/%E5%9C%B0%E5%9B%B3%E5%B8%B3%E3%81%AE%E6%B7%B1%E8%AA%AD%E3%81%BF-%E9%89%84%E9%81%93%E7%B7%A8-%E4%BB%8A%E5%B0%BE-%E6%81%B5%E4%BB%8B/dp/4807166409">地図帳の深読み 鉄道編</a>》。每個篇章都可以學到新知識，對於閱讀地圖也更有感覺，好比說路的輪廓可以說明哪裡有陡坡、路的形狀可以說明開發的年代、鐵路繞大彎通常是蒸汽火車爬不上去等等。漫遊在東京多了一層樂趣。</p>
<p>其實也許我對歷史的興趣完全是地圖宅的那種興趣也說不定。</p>
<h2 id="she-ying">攝影</h2>
<p>邊散步邊拍照是最近兩年的興趣。為了便攜，今年從 Canon EOS Kiss X10 (Rebel SL3) 換到了 Fujifilm X-S10，試了好幾顆定焦鏡，現在比較喜歡 35mm 畫角（約全片幅的 50mm）。我很喜歡拍青苔，但一般的定焦鏡對焦距離太遠，應該是需要微焦鏡的，不過 Fujifilm X 系統的微焦鏡選項不是很多，最近出的是 30mm，感覺跟現在的鏡頭有點重複，還在觀望。</p>
<p>重新註冊了 Flickr，偶爾會發一些照片。不過最常發圖的地方還是在 <a href="https://www.instagram.com/yorkxin/">Instagram</a> 就是了。</p>
<p><a data-flickr-embed="true" href="https://www.flickr.com/photos/191004964@N02/52574540522" title="Autumn Maple"><img src="https://live.staticflickr.com/65535/52574540522_a18d88651d_z.jpg" width="640" height="427" alt="Autumn Maple"/></a><script async src="//embedr.flickr.com/assets/client-code.js" charset="utf-8"></script></p>
<h2 id="qi-ta">其他</h2>
<p>再度嘗試寫 Blog。自從 2019 年從 Medium 搬家到 Static-Site Generator 之後，就非常沒有寫文章的動力，主要還是覺得寫個文章像在跟 Markdown 編譯器吵架似的，跑得快的編輯器很陽春，功能足的編輯器又慢又像在寫軟體。且 Git 發稿流程太儀式化了，像在發布軟體新版一樣，改個錯字還得 git commit git push git merge。現在用 Ghost 兜了一套 headless CMS，前端依然用 SSG，但後台管理就舒舒服服的，按發布就發布，按存檔就存檔。Ghost 編輯器我用得很開心，要連結有連結，要插圖有插圖，Markdown 拋在腦後了。寫得出這篇文章也是靠 Ghost 後台的。我知道有很多人喜歡純 Git 流程，但我還是比較喜歡 GUI。</p>
<p>電腦軟體全部換成了中文版。以前堅持用原文版，一直覺得電腦軟體的中文翻譯參差不齊，不完美的東西看了就扎眼不如不看，反正看得懂英文和日文。最近覺得還是中文看得順眼，看到英文和日文就想到工作，所以能換繁體中文版的都換了，翻譯品質就算了，不想計較。缺點是在日本開 Google Maps 偶爾會看到奇怪的中文翻譯。</p>
<p>改用 Firefox。不是為了什麼反隱私的意識形態。一開始是工作中要操作多個 AWS 帳號，發現 <a href="https://addons.mozilla.org/en-US/firefox/addon/multi-account-containers/">Multi-Account Containers</a> 很好用。漸漸發現 Firefox 整體都好用，古時候瀏覽器常見的那些微調設定，在 Chromium 被拔掉的那些，Firefox 都還留著，有種新潮的懷舊感。字體我也不再那麼挑剔了，能看就好，我又不是搞設計專業的。</p>
<p>以及開始在 Mastodon 活動了，在 Twitter 開始被蹂躪的時候註冊了帳號，現在看來還是小圈圈比較適合我，也沒有社交關注度的壓力。歡迎關注： <a href="https://g0v.social/web/@yorkxin">@yorkxin@g0v.social</a>。</p>
<p>那麼2022年就這樣吧。還算是有收穫的一年。</p>

            ]]></description>
        </item>
        <item>
            <title>Logi MX Keys Mini for Mac</title>
            <link>https://blog.yorkxin.org/posts/logi-mx-keys-mini-for-mac/</link>
            <pubDate>Tue, 13 Dec 2022 14:12:11 +0000</pubDate>
                
            <guid>63987bc6f5647503814059a8</guid>
            
            <description><![CDATA[
                <p>最近買了這把鍵盤：Logi MX Keys Mini for Mac。用了大約一個月，寫一下心得。</p>
<p>原本用的是 Microsoft Sculpt Ergonomic Keyboard，這把是人體工學設計，左右按鍵區域的中間有個分隔，花了一些時間習慣以後，打起來很舒服，手腕和肩膀的酸痛有比較緩和。當初為了美式佈局還從美國進了水貨到日本。但是因為開始用 JetBrains IDE 工作，它有很多快速鍵是安排在 F 功能鍵，這把鍵盤的 F 區非常難按，像是年久失修的卡帶錄音機。所以開始物色市場上的鍵盤。</p>
<p>首要條件是美式佈局，這在日本的公司貨就選項很少了。第二條件是無線，且得支援多設備連線，因為公司的電腦和私人的電腦都要用。微軟那支本來就只有 2.4GHz USB，Dell U2720Q 可以插在螢幕的 USB 上，透過 USB-C 連到筆電，所以不是問題；市場上的商品大部分是藍芽的，選項不多。第三是不要機械鍵盤，最好是筆電觸感的，鍵程短的，這是我個人喜好。最好沒有數字鍵區。本來還很堅持一定要有編輯鍵，但是看在市場上有的選項真的不太多，只好放棄。基本上我的完美鍵盤是 Logi MX Mechanical Mini 的佈局、Apple Magic Keyboard 鍵盤的觸感。</p>
<p>最後還是選了 Logi MX Keys Mini，而且日本公司貨得要 Mac 版的才有美式佈局。</p>
<p>打字觸感很滿意，比 Apple 的 Magic Keyboard 外接鍵盤再那麼好一點。F 功能鍵也比 MacBook Air (2020) 和微軟那把鍵盤的大很多，好按很多，有種一等公民的感覺。多設備切換我嘗試了藍芽和 Logi Bolt USB 接收器（選購），最後還是懶得切來切去，直接用 USB 了。可惜手邊的 MX Master 3 不能用這顆接收器，且內附的 USB 接收器訊號插在螢幕後面就不如藍芽穩定，不然就真的實現 USB-C 熱插拔 KVM Switch 了。</p>
<p>不過初期還是發現一些挑剔的點，用了才知道的：</p>
<p>第一是媒體鍵區的客製選項很有限。媒體鍵跟 F 功能鍵是一起的，透過 Fn 組合鍵來發動，或是 Fn+Esc 來鎖定。Logi Options+ 工具程式裡面可以選的選項不多，而且Fn+🔊大聲 這個組合是無法單獨設定，例如設定了🔊大聲鍵為 Page Down，那麼按了 Fn+🔊大聲 也依然是 Page Down。</p>
<p>第二是想要設定 CapsLock = Ctrl 得透過 <a href="https://karabiner-elements.pqrs.org/">Karabiner Elements</a>，macOS 的鍵盤設定無效。發現這件事當下還想上拍賣網站放手了，因為我很習慣 Ctrl 在 A 左邊這個位置（可以參考2016年的<a href="https://blog.yorkxin.org/posts/ergonomic-control-key/">拙文</a>）。不過跟微軟那把鍵盤筆記起來，至少 MX Mini 有大寫鎖定燈了。</p>
<p>第三是左 Ctrl 不能指定到別的按鍵。我設定成 CapsLock 發現按了Fn + 🌛 = 鎖屏，竟然會離開應用程式。用 Karabiner Elements 看了鍵盤事件，才發現這把鍵盤的 Fn + 🌛 = 鎖屏功能，實際上是送出 [左 Ctrl] + Cmd + Q 的組合鍵，也就是 macOS 預設的鎖屏快速鍵。所以如果把左 Ctrl 設定成 CapsLock，那麼就會觸發 Cmd + Q = 離開應用程式。當然另一個明顯的解法是把鎖屏快速鍵改成別的，但考慮到我也很少按 CapsLock，就算了。現在我有兩個 Ctrl 呢。</p>
<p>其實沒有編輯區還是不太習慣，現在是把🔊大聲設定成 Page Up，把🌛設定成 Page Down（預設是切換勿擾模式），但還是很懷念 Home 和 End。再考慮要犧牲哪個按鍵好了。</p>
<p>至於文首說的肌肉痠痛問題，可能是無解了，只能認老吧。Logi 有出人體工學鍵盤，但是數字鍵還是太佔空間了。</p>
<p>最後還是想抱怨一下日本市場對美式鍵盤真的很不友善，美式鍵盤幾乎僅限於高級機械鍵盤，筆電除了 Dell XPS、Lenovo E 系列和 Apple 全系列之外，幾乎都只有日文鍵盤，想要美式鍵盤還得進水貨，又說無保固，又說沒有總務省的電波檢驗。</p>
<hr />
<p>有興趣的可以參考〈<a href="https://www.rtings.com/keyboard/reviews/logitech/mx-keys-mini">Logitech MX Keys Mini Review - RTINGS.com</a>〉這篇評測。</p>

            ]]></description>
        </item>
        <item>
            <title>稍微分析了一下我的 Podcast 收聽紀錄 2018–2020</title>
            <link>https://blog.yorkxin.org/posts/podcast-analysis/</link>
            <pubDate>Sat, 09 May 2020 13:00:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2020/05/09/podcast-analysis.html</guid>
            
            <description><![CDATA[
                <p>自從 2015 年開始聽 Podcast 至今也約 5 年了。最近發現我聽 Podcast 的時間和訂閱節目的數量都很多，有點資訊超載的感覺，尤其新節目越來越多（大部分是台灣的），排擠到原本訂閱的節目，跳過了好多單集。</p>
<p>以下是我一直以來的猜想：</p>
<ol>
<li>我訂閱的節目在最近兩三個月變得很多，但是真正去聽的很少，所以有些該退訂。</li>
<li>我聽節目的喜好程度是中國普通話節目 &gt; 台灣國語節目 &gt; 台語節目 &gt; 英語 &gt;&gt; 日語。</li>
<li>一開始都是聽中國的節目，而從某一段期間開始狂聽英語節目。</li>
<li>開始在家工作之後似乎聽的時間變多了，儘管沒有通勤。</li>
</ol>
<p>於是我好奇我的行為有沒有什麼改變。</p>
<p>尤其是「<strong>訂了沒在聽</strong>」的問題，一直困擾著我。我有公開一個我<a href="https://airtable.com/shrPEgq1XSTs41Bor">訂閱中的節目列表</a>。目前是大約 100 檔節目。但是我不可能全部都聽完。</p>
<p>但是數據去哪裏找呢？</p>
<hr />
<p>我用的 Podcast App 叫做 <a href="https://castro.fm/">Castro</a>，裡面有個打星 (Star) 的功能。這個計畫本來是想要做打星列表，公開出來。但是 App 裡面沒有匯出的功能，備份檔裡面雖然有節目和單集的歷史紀錄，但只有內部索引號碼 (UUID)，讀不到節目名稱。去信開發者也只收到罐頭回信。</p>
<p>本來很氣餒，但後來找到有人<a href="https://anh.do/blog/castro-history">透過祕技把資料庫匯出</a>，寄送到自己的 Email 信箱。基本上他做的是把事件灌到另一個紀錄個人嗜好的 App 裡面。</p>
<p>有了原始資料庫就可以做很多事情了，包括收聽行為的分析。拿到了資料庫之後，就用 SQL 和 Google 試算表做簡單的數據分析。剛好遇到日本的黃金週，旅遊計畫也因為武漢肺炎 (COVID-19) 疫情而被取消了，所以我有完整的長假可以玩這些資料。（技術細節請見文末）</p>
<p>以下是一些 Insights。直方圖的數據是根據收聽時間 × 節目主要語言來彙整的，以一週為單位，橫軸是收聽的週（以週日為開始），越往右邊越靠近現在（2020 年 5 月）。</p>
<p>數據最終更新的時間是五月初，所以五月的數據不完整。以及我開始用 Castro 大約是 2018 年 11 月，所以先前的數據是空白的。不過一年半的數據也夠分析了。</p>
<hr />
<h2 id="zong-bo-fang-dan-ji-shu">總播放單集數</h2>
<p>首先最直覺的問題是「我到底都聽了多少單集」。</p>
<p>跟其他 Podcast App 不同，Castro 的基本設計是一個播放清單和收件匣 (Inbox)。Castro 把訂閱 Podcast 節目類比成訂閱電子報，有新內容的時候先進入收件匣，要聽的時候再移入播放清單，且可以自由調整播放順序。我在 Castro 中設定大部分的節目是進入 Inbox，只有少數每日更新，或是乾貨特別多的節目，可以直接進入播放清單。如果你是播客主，Castro 客戶端的下載 +1 就是發生在此刻。</p>
<p>如果用 SEO 的術語來說，就是「閱覽數」(PV)。除了我有信心可以每集必聽的節目之外，還有標題內文吸引我所以點了下載的單集。聽說 SEO 界還有所謂的 clickbait 伎倆——以聳動或嘩眾取寵的標題騙人家點進來看，增加 PV 及增加廣告曝光次數。幸好 Podcast 仍然還在主動訂閱的模式，clickbait 比較難奏效。</p>
<p>以下這張圖就是我自己的 PV 數。</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2020-05-09-podcast-analysis/episodes-weekly.png" alt="/images/2020-05-09-podcast-analysis/episodes-weekly.png" /></p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2020-05-09-podcast-analysis/episodes-weekly-100.png" alt="/images/2020-05-09-podcast-analysis/episodes-weekly-100.png" /></p>
<ul>
<li>雖然華語佔了超過一半，但是英語的佔比也不少。現在大致上中國、台灣跟英語的節目很平均。</li>
<li>很明顯有個趨勢是收聽的英語單集越來越多，甚至一度超越華語節目。但如今大概還是保持在 30~40%。</li>
<li>我訂閱了好幾檔每日出新番的 NPR 英語節目，如 <a href="https://www.npr.org/podcasts/510355/coronavirusdaily">Coronavirus Daily</a>、<a href="https://www.npr.org/podcasts/510325/the-indicator-from-planet-money">The Indicator from Planet Money</a>、<a href="https://www.npr.org/podcasts/510351/short-wave">Short Wave</a>等，所以這也讓英語（黃色區塊）變得比較大片。另外比較積極更新的還有 Vox 的 <a href="https://www.vox.com/reset">Reset</a>、中國的<a href="https://storyfm.cn/">故事 FM</a>等。</li>
<li>2020 年 3 月間，一週播放到 100 個單集似乎是有點太超過了。這段期間是我大量試聽不同的新節目。</li>
<li>2019 年底開始，台灣的華語節目大爆發。可能是為了試聽所以點了許多單集，但是現在佔比又被中國華語吃回來了，這可能跟我自己喜好的節目風格有關。</li>
<li>只要我去外地出遊就幾乎不聽，如 2019 年日本黃金週連假、新年連假。</li>
<li>2019 年 9 月開始聽節目的時間突然變得很多，這是因為我把 PlayStation 4 賣掉了。</li>
</ul>
<h2 id="zong-bo-fang-shi-chang">總播放時長</h2>
<p>上述的播放單集並沒有回答一個問題：我播放了那麼多的單集，加起來到底都花了多少時間在聽 Podcast？</p>
<p>透過統計資料庫裡面的「播放進度」就可以算出每週大約播放了多少小時的節目。當然因為它只有進度（秒），所以它是原始音訊檔的時間長度，忽略快轉跳過的時間、加速播放、自動移除空白而省下的時間，也忽略了重複播放多次的加總時間（只計算最後一次播放到哪裏）。但因為我的習慣通常是不會重複聽第二次，所以不精確的部分不太影響這個指標。</p>
<p>那麼來看圖說故事。縱軸是加總的播放進度，單位是小時：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2020-05-09-podcast-analysis/duration-weekly.png" alt="/images/2020-05-09-podcast-analysis/duration-weekly.png" /></p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2020-05-09-podcast-analysis/duration-weekly-100.png" alt="/images/2020-05-09-podcast-analysis/duration-weekly-100.png" /></p>
<ul>
<li>儘管之前的單集統計說明我聽的節目是中國華語、台灣華語、英語平均，但從總播放時長來看，還是中國華語居多，可能我收聽的中國節目大部分都很長。</li>
<li>2020 年 2 月中開始，因應武漢肺炎疫情，公司要求我們在家上班。從那之後我聽節目的總時長一度下跌，但是後來又漲回來了。我猜測是跟我沒有通勤有關，所以花了一些時間建立新習慣。</li>
<li>2020 年 3 月中曾經一度達到一週 60  小時，相當於平均一天超過 8 小時。原本我以為我程式寫錯，去看了原始資料還真的都有印象。可能是一邊開著一邊工作，當白噪音，其實也沒在認真聽。這種節目就是我該退訂的。</li>
<li>2019 年中以前聽的內容大部分都是中國的節目</li>
<li>儘管台灣的節目變多了，進入我耳朵的聲音大部分還是中國的節目</li>
<li>從 2019 年 7 月開始重拾英語收聽習慣，但最近有被台灣節目排擠的趨勢</li>
<li>2020 年 4 月開始聽了一些台語節目，基本上都是從<a href="https://www.rti.org.tw/radio/programList/program_category_id/2">中央廣播電台的閩南語頻道</a>來的</li>
<li>日語節目我曾經多次嘗試，多次放棄</li>
</ul>
<h2 id="zhui-xin-fan-zhi-shu">追新番指數</h2>
<p>上述的收聽及紀錄不考慮新番舊番，也就是說如果我在 2020 年 5 月選了 2018 年發布的單集收聽，還是算在 2020 年 5 月。如果要解答訂閱的即時收聽量，也就是「新番一定聽」和「訂了沒在聽」的指標，則要考慮發布日期和收聽日期。</p>
<p>下圖計算的方式是每週有給一個節目按「播放」就算 1 次，按多次也只算 1 次。並且只計算發布後 7 天內收聽的節目，所以可以排除去聽老集數的誤差（播客主所謂的「長尾」）。</p>
<p>於是就可以看出每週真正去追的新番數量：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2020-05-09-podcast-analysis/following-shows.png" alt="/images/2020-05-09-podcast-analysis/following-shows.png" /></p>
<ul>
<li>儘管台灣節目大爆發時期我訂了許多台灣節目，但是隨後也退訂了不少，反而新訂了一些中國的節目。再度強調，這還是跟我個人收聽的口味有關。</li>
<li>真正大量追番的時期要從 2019 年 9 月算（賣掉了 PlayStation 4）；在此之前大概都在每週 10 檔節目左右。也是在幾乎同一時期去追了不少英語節目。</li>
<li>目前（2020 年 4 月）是維持在每週追 40~50 檔節目。將來應該會變少。</li>
<li>我所謂「訂了 100 檔節目」真正有持續在追的也只有一半。事實上可能有很大一部分已經停更了，不是我主動跳過的。</li>
</ul>
<h2 id="zhong-cheng-zhi-shu">忠誠指數</h2>
<p>上面的追新番圖表是照語言區分的。我事實上最好奇的是「我需要退訂哪些節目」。</p>
<p>做播客主的人一定會好奇：你的節目真的有人在聽嗎？儘管託管網站後台看得到下載數量，但你卻不知道他們有沒有播放（除非是 Apple Podcasts、Spotify 等平台有第一手數據）。甚至你也不知道這其中有多少是訂了之後沒在聽。</p>
<p>接下來這項統計，就是我對節目的忠誠度。這個數據是如此計算的：</p>
<blockquote>
<p>對於每一檔節目，找到最初我播放的單集。從那之後發布的每一單集，根據月份加總時間長度。然後和我實際收聽的長度相除。</p>
</blockquote>
<p>這樣就能得到一個「<strong>消化率</strong>」：雖然有訂閱，而且它在這個月有發新番，但有多少 % 是我真正去聽了。</p>
<p>這就能看出我對節目的忠誠度，以及看看那些我可以退訂，反正我也沒在聽。</p>
<p>再一次用 SEO 的術語來解釋，前文的單集總數如果是 PV 的話，忠誠指數就是「逗留網站時間」。吸睛的標題可能會吸引我打開網頁，但真正有內容的長文會讓讀者流連忘返，標題卻不見得吸睛。</p>
<p>然而對於 Podcast 來說，花時間製播的內容還是得要有人聽完才有意義。標題和節目筆記只是一種廣告，轉化發生在節目的消化。但這樣的轉化除非是發布端到收聽端整合的大平台（Apple Podcast, Spotify）才能精確計算，不像 YouTube 有製播-回放-統計一條龍的鏈，用 RSS 發布的 Podcast，天生就很難一個平台通吃，一般的泛用型 Podcast App 因為單純是下載檔案，後端只能看到下載數據，無法向播客主回報收聽紀錄等資訊。所以這種數據對播客主來說是夢寐以求的。</p>
<p>以下這張截圖只是一張試算表的一部分。因為 5 月的內容還算新，所以我還沒聽完。但是如果我往下捲，就會發現許多紅色、有發新番但根本沒在聽的節目。截圖中的節目基本上都是我還有固定在追的。</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2020-05-09-podcast-analysis/consumption-rate-monthly-per-show.png" alt="/images/2020-05-09-podcast-analysis/consumption-rate-monthly-per-show.png" /></p>
<p>當然，因為有語言的標籤，所以可以照語言分類：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2020-05-09-podcast-analysis/consumption-rate-monthly-per-lang.png" alt="/images/2020-05-09-podcast-analysis/consumption-rate-monthly-per-lang.png" /></p>
<p>我的積極目標是要讓最新月份的數據盡量在 70% 以上，或是偏綠色，這表示我有訂的都有在聽，盡量降低出一集就 Skip 一集的壓力感。</p>
<hr />
<h2 id="hou-ji-yong-shu-ju-dong-cha-zi-ji-de-chen-mi">後記：用數據洞察自己的沈迷</h2>
<p>運用大數據平台分析個人的「小數據」也不是第一次了。在 2019 年 11 月也做過類似的事情，那時候是<a href="https://blog.yorkxin.org/2019/11/03/twitter-usage-analysis.html">分析 Twitter 發文紀錄</a>。資料量也不是很多，才三萬多條 ，不過已經可以看到一些有趣的趨勢。</p>
<p>今天做這個 Podcast 分析，雖然最初的目標是拉出資料庫來發布我的打星播放清單，但是當我手上拿到一個充滿事件紀錄的資料庫的時候，我就轉而運用我在工作學到的知識來分析數據，以及把流程給自動化。</p>
<p>從 2019 年末開始我有觀察到，每週推出的單集，多過我可以消化的量，但當時不以為然，只覺得 Castro 可以幫我統整單集，我自己決定如何收聽，已經比 Overcast、Pocket Cast 等基於播放列表的 App 來得方便了。但最近看到 Inbox 有 40 個單集，我就感到問題的嚴重性。儘管在家工作的時間變多了，但同時我也開始覺得「聽不完」，那麼 Podcast 作為娛樂工具就失去了它的意義，<strong>不如說是一種沈迷</strong>。</p>
<p>以前我也有類似的感覺。2012 年以前我沈迷 PTT。2016 年以前我沈迷日本動漫，每季都會追新番，看不完。2019 年以前會玩 PS4 的暢銷大作。2019 年中以前會一直逛 Facebook。如今是 Podcasts。</p>
<p>雖稱沈迷，這種沈迷卻不是因為樂在其中，而是來自怕落伍的壓力 (Fear of Missed Out, FOMO)。我只是一味地輸入而沒有咀嚼之後再輸出，為的只是保持自己的資訊跟「大眾」同步。如果讀新聞是為了因應瞬息萬變的世界（瘟疫下更重要），那麼追逐娛樂的新事物，雖然可以消化時間，有時還能改變自己的思考模式，但如果沒有輸出，並無法給自己帶來好處，只是徒增壓力。</p>
<p>我依稀記得 Cal Newport 在<a href="https://www.calnewport.com/books/digital-minimalism/">《Digital Minimalism》</a>一書中闡述一種觀念，就是我們要去尋找對自己最有效用的科技，而非單純別人也在用所以你也用。狂刷 Netflix、社群網站等行為，都不是最有效用的利用法。作者建議讀者去尋找對自己最有效用的科技，並對於任何新科技都保持嚴格檢驗的態度，「我是否確實體會到他對我生活帶來的美好」、「我榨取了它對我最有價值的部分了嗎」而非不假思索地接受新科技。</p>
<p>用台灣人最愛說的 CP 值（性價比，Cost-Performance Rate）來解釋的話，成本（代價）是你花了多少時間在這些娛樂，但最重要的性能（效能）卻是可以自己定義的。它可以是消遣無聊的生活、創作而得到的爽、對世界發表意見的輸出。但如果成效不彰，可能你用的方法不適合你，也可能該工具本來的設計就不是讓你爽，而是讓你看廣告，所以吸引你的注意力。但最重要的是，定義一個效能 P，然後檢視你付出的代價 C 跟實際獲得的 P 有沒有夠高，藉此來檢驗科技是否給自己帶來好的效果。</p>
<p>話說回我自己。如前所述，我放棄了不少興趣，動漫、電動、SNS。近來我跟 Netflix、電子書處於一種我很滿意的平衡，我也找到了我和 Facebook 新的平衡。但眼看 Podcast 好像快要吃垮我自己，現在做這種數據研究，給自己一些未來的指引，可能還不遲。</p>
<hr />
<h2 id="fu-lu-ji-shu-xi-jie-for-geeks">附錄：技術細節 for Geeks</h2>
<p>分析的方式：</p>
<ul>
<li>Google Spreadsheet 樞紐分析表 （TIL 這根本上古神器）
<ul>
<li>原本還用了 <code>=QUERY()</code> 但發現我要的結果直接用樞紐分析表即可</li>
</ul>
</li>
<li>Amazon Athena 寫一些 View Query 倒 CSV 出來
<ul>
<li>目前是手動取代成 TSV 然後貼到 Google 試算表，因為還要 workaround 節目標題裡面有逗號的情形</li>
<li>最終可以從 API Gateway 從 S3 直接出 CSV 檔案，用 <code>=IMPORTCSV()</code> 直接匯入 Google 試算表</li>
<li>最終還可以掛入 Step Functions 來自動下 query 更新 CSV 檔案</li>
<li>學到了 <code>PARTITION OVER</code> 可以得到類似樞紐分析的結果</li>
</ul>
</li>
</ul>
<p>資料彙整的方式：</p>
<ul>
<li>用一支程式跑在 Lambda 抽出 SQLite 資料到 JSON，每個 Query 上限 1000 條，避免一次拉太多導致 Lambda memory bloat；盡量用小規格機器跑。</li>
<li>Schema 寫在 AWS Glue Table 裡面，手動寫，沒有用 Glue Crawler。
<ul>
<li>手寫一來是因為可以直接抄 SQLite 的 schema，二來是原始資料庫有一些欄位可以手動 cast，例如時間戳記原本是 <code>integer</code>，可以 cast 成 <code>timestamp</code>。</li>
<li>Glue Crawler 爬一次要兩分鐘，也是錢。</li>
</ul>
</li>
<li>語言的標籤是我在 AirTable 手動加，然後複製貼上到 Google 試算表的。根據的是節目主要語言，而非單集語言。所以如果一個台灣的華語節目突然有一集在講英語，那算在台灣華語。由於出現的次數很低，不影響整體結果，所以不予區別。</li>
</ul>
<p>下載資料庫檔案的方式：</p>
<ul>
<li>祕技是在 Castro &gt; Settings &gt; Support 那邊長按 Email Support，就可以選擇 Email with Database and Logs。因為我的資料庫有36MB，所以只能用 iCloud Mail Drop 寄送。</li>
<li>用 Zapier Email Parser 抓出 iCloud Mail Drop 的下載地址</li>
<li>讓 Zapier 執行一段 JavaScript 打到一個 API Gateway 執行 Step Functions
<ul>
<li>API Gateway 可以發 API Key 比較方便</li>
<li>Step Functions 不需要讓 client 等 Lambda 結果，直接回 execution ID</li>
<li>Zapier JavaScript Task 的 timeout 是 1 秒</li>
</ul>
</li>
<li>Step Functions 的內容：
<ul>
<li>第一個 Lambda 開 Puppeteer 尋找 iCloud Mail Drop 附件的下載地址；規格比較高</li>
<li>第二個 Lambda 直接用 axios 讀 stream 丟到 S3</li>
<li>因為 Puppeteer 的 Chromium 要下載檔案比較麻煩，無法確定檔案何時下載完成，必須一直 poll 下載資料夾，直到沒有 <code>*.crdownload</code> 為止。</li>
<li>最後一步是啟動另一個 Step Functions 把 S3 裡面的資料庫檔案變成 JSON（如上述資料彙整）。分開是為了讓第二個 state machine 不要跟下載綁在一起，這樣要抽資料就可以直接從 S3 讀現成的資料庫。</li>
</ul>
</li>
</ul>
<p>一些淚：</p>
<ul>
<li>嘗試用 AWS SAM 寫 Node.js 程式，一開始蠻順利，到後來發現 <code>sqlite3</code> 有系統依賴，在 macOS 上面 <code>sam build</code> 的話會編譯成 macOS 用的 binary，無法跑在 Lambda 的 Linux 上面。必須用 <code>sam build -u</code> 跑在 Docker 裡面編譯才行。但是非常慢。
<ul>
<li>既然拿得到下載網址，也許可以直接寫一個 Docker image 跑在 Fargate 然後直接 <code>wget</code> + <code>sqlite3 | jq</code>  + <code>aws s3 sync</code> 就好了。</li>
</ul>
</li>
<li>要寄送 Castro Support Email，iOS 必須有 Mail App。其他第三方 App 不行。</li>
<li>因為要從 iCloud 下載檔案，以及連結 Zapier，服務都開在美區似乎網路延遲比較低。</li>
<li>CloudFormation deploy API Gateway 似乎不會 deploy stage。</li>
</ul>
<p>一些懸念：</p>
<ul>
<li>不知道 Castro 什麼時候會把這個輸出資料庫的功能下架。</li>
<li>結果我還沒做打星播放列表。</li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>稍微分析了一下我的 Twitter 使用量 2008–2019</title>
            <link>https://blog.yorkxin.org/posts/twitter-usage-analysis/</link>
            <pubDate>Sun, 03 Nov 2019 07:00:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2019/11/03/twitter-usage-analysis.html</guid>
            
            <description><![CDATA[
                <p>最近 Twitter 又提醒了我的週年慶。2008 年我註冊 Twitter 帳號，前前後後在 BBS, Plurk, Facebook 之間輾轉來去，現在只剩下 Twitter 和 Instagram 是還有持續在使用的社交網路。</p>
<p>最近受到<a href="https://www.calnewport.com/books/digital-minimalism/">《Digital Minimalism》</a>觀念的影響，理解到過度使用社交網路會影響身心健康及工作效率，於是有意識地拒用社交網路（希望有機會寫一篇文章談談拒用的歷程）。不過因為被 Twitter 提醒週年慶，也開始好奇，11 年來我用 Twitter 的習慣有什麼改變。</p>
<p>結果就是花了半天的時間做出這張圖表：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2019-11-03-twitter-usage-analysis/Screen%20Shot%202019-11-03%20at%2014.12.29.png" alt="" /></p>
<p>Series 的定義如下：</p>
<ul>
<li><code>1.0_regular</code> 單純發文</li>
<li><code>1.1_link_fwd</code> 轉貼連結</li>
<li><code>2.0_retweet</code>  單純轉發別人的推文</li>
<li><code>3.0_reply</code> 在 Thread 中回覆別人</li>
</ul>
<p>其中有幾個明顯的低谷，主因是農曆過年我不太上網。</p>
<p>而 2017 年中起我發文的量變得很低，主因是我換到一個做得很開心的工作，牢騷變少了，自然發文也就變少了。這也反映了我一直都把 Twitter 當牢騷垃圾桶的心態。</p>
<p>最後是 2019 年約 7 月起急劇降低，這是我開始實踐 Digital Minimalism 的時期。不過上個月（10 月）好像快破功了。</p>
<p>一些高峰值我不願意去深究，我猜可能是年輕氣盛跟人家在網上吵架吧。</p>
<p>接著就來講講我是如何做出這張圖表的。</p>
<h2 id="qu-de-twitter-de-zi-liao">取得 Twitter 的資料</h2>
<p>要做分析，首先要有資料。一開始想說是不是要寫機器人去爬 API，但事實上 Twitter 提供個人數據下載服務，在 <a href="https://twitter.com/settings/your_twitter_data">Your Twitter Data</a> 頁面可以索取下載。這服務應該是來自一些國家政府的要求，Facebook 也提供一樣的功能。</p>
<p>Twitter 稱下載回來的資料是 JSON 格式，但實際上卻是封裝在 JavaScript 程式碼裡面。聽不懂？舉個例子：</p>
<p>這是 <code>verified.js</code> 的內容：</p>
<pre data-lang="js" class="language-js "><code class="language-js" data-lang="js">window.YTD.verified.part0 = [ {
  &quot;verified&quot; : {
    &quot;accountId&quot; : &quot;9999999999&quot;,
    &quot;verified&quot; : false
  }
} ]
</code></pre>
<p>這是我期待的 JSON：</p>
<pre data-lang="json" class="language-json "><code class="language-json" data-lang="json">[ {
  &quot;verified&quot; : {
    &quot;accountId&quot; : &quot;9999999999&quot;,
    &quot;verified&quot; : false
  }
} ]
</code></pre>
<p>每個檔案都是長這樣。眼尖的 JavaScript 工程師應該看得出來，前面多了 <code>window.YTD.xxx.part0</code>，你得丟進 JavaScript Runtime 把檔案都 evaluate 一遍才能得到資料，還得事先初始化 <code>window.YTD.xxx</code> object，而且是存在記憶體裡面。</p>
<p>而像我這種每天發牢騷的 Twitter 用戶，至今累積 3.7 萬條推文，下載回來的數據就相當可觀。我的 <code>tweet.js</code> 高達 48 MB。整包丟進 Runtime 再寫程式分析是很不切實際的事情。</p>
<p>不過好在這些檔案除了開頭是一個 assignment 之外就都是 JSON 了，全都是 primitive types，statement 最後面也沒有分號（😉），所以直接取代開頭也就行了：</p>
<pre data-lang="sh" class="language-sh "><code class="language-sh" data-lang="sh">sed &#x27;s&#x2F;^window.YTD.tweet.part0 = &#x2F;&#x2F;g&#x27; &lt; tweet.js &gt; tweet.json
</code></pre>
<h2 id="guan-dao-zi-liao-ku-li">灌到資料庫裡</h2>
<p>為了高效率搜尋和分析，我需要把資料灌到資料庫裡面。當然我有好幾個選項：我可以開一個 ElasticSearch 直接灌進去，也可以刻 Schema 灌到 PostgreSQL 裡面，甚至把所有 JSON structure 都打平丟進 Excel 也可以。</p>
<p>不過以上幾個方法都比不起找一個雲端大數據分析服務來得簡單，雖然很像用牛刀殺雞。我一開始選擇了 Google Cloud 的 BigQuery，而且發現它很符合我當下的需求，所以就沒有研究別的方案了。</p>
<p>首先要灌資料。在 Console 開一個 Data Set 很簡單。灌資料就有點問題了。你可以從本機上傳，但是上限是 10MB。我有 48MB 的 JSON 要傳，只能透過 Google Cloud Storage。然而它又有一個限制：JSON 必須是 newline delimited 的。</p>
<p>這是一般的 JSON Array:</p>
<pre data-lang="json" class="language-json "><code class="language-json" data-lang="json">[
  {
     &quot;a&quot;: 1
  },
  {
     &quot;a&quot;: 2
  },
  {
     &quot;a&quot;: 3
  }
]
</code></pre>
<p>這是所謂的 newline delimited：</p>
<pre data-lang="json" class="language-json "><code class="language-json" data-lang="json">{&quot;a&quot;: 1}
{&quot;a&quot;: 2}
{&quot;a&quot;: 3}
</code></pre>
<p>沒錯，就是一行一個 object，用換行符號 <code>\n</code> 切開。</p>
<p>要把上述的 <code>tweet.json</code> 轉換成 newline delimited 格式，只要用 <a href="https://stedolan.github.io/jq/">jq</a> 即可：</p>
<pre data-lang="sh" class="language-sh "><code class="language-sh" data-lang="sh">jq -c &#x27;.[]&#x27; &gt; tweet.gbq.json &lt; tweet.json
</code></pre>
<p>現在你可以上傳檔案到 Cloud Storage 並匯入資料了。</p>
<p>欸，那 Schema 呢？免煩惱，只要打開自動偵測即可！</p>
<pre><code>Auto detect
☑️ Schema and input parameters
</code></pre>
<h2 id="fen-xi-zi-liao">分析資料</h2>
<p>GCP 的 BigQuery 是可以用 SQL 分析的。大致上跟一般的 RDBMS 一樣，只有 function 之類的不太一樣。以下是我是用的 SQL。</p>
<p>首先是做一個 view 來拉出我分析要用的 metadata。用 Query editor 玩玩看，然後按 Save view 即可：</p>
<pre data-lang="sql" class="language-sql "><code class="language-sql" data-lang="sql">SELECT
id,
parse_datetime(&quot;%a %b %d %X +0000 %Y&quot;, created_at) as timestamp,
starts_with(full_text, &quot;RT @&quot;) as is_retweet,
in_reply_to_user_id IS NOT NULL as is_reply_thread,
ARRAY_LENGTH(entities.urls) &lt;&gt; 0 as has_links,
full_text --- 肉眼參考用，實際分析不會用到
FROM `&lt;table&gt;` --- 這裡取代成你的 project.dataset.table
</code></pre>
<p>長得像這樣：</p>
<table><thead><tr><th><code>id</code></th><th><code>timestamp</code></th><th><code>is_retweet</code></th><th><code>is_reply_thread</code></th><th><code>has_links</code></th><th><code>full_text</code></th></tr></thead><tbody>
<tr><td><code>99999999</code></td><td><code>2018-03-02T12:34:56</code></td><td><code>true</code></td><td><code>false</code></td><td><code>true</code></td><td><code>RT @jack: ...</code></td></tr>
</tbody></table>
<p>幾個注意的點：</p>
<ul>
<li><code>created_at</code> 匯入是 String type，原文如 <code>Wed Mar 02 12:34:56 +0000 2018</code> ，時區固定在 UTC。BigQuery 不會幫你自動轉換成時間，所以自己 parse。</li>
<li>所有的轉推，包括官方轉推，都是 <code>RT @</code> 開頭。但是 <code>entities.urls</code> 裡面會包含原推的超連結。所以 <code>is_retweet</code> 和 <code>has_links</code> 會同時 <code>true</code>。</li>
</ul>
<h2 id="hua-tu">畫圖</h2>
<p>View 弄好之後就可以對它下 Query，例如：</p>
<pre data-lang="sql" class="language-sql "><code class="language-sql" data-lang="sql">select
timestamp,
if(is_retweet, &quot;2.0_retweet&quot;,
  if(is_reply_thread, &quot;3.0_reply&quot;,
    if(has_links, &quot;1.1_link_fwd&quot;, &quot;1.0_regular&quot;)
  )
) as tweet_type
FROM `&lt;view&gt;`
order by id asc
</code></pre>
<p>上文提到 <code>is_retweet</code> 和 <code>has_links</code> 可以同時 <code>true</code>，為了畫圖方便起見，我用了一個有點複雜的 <code>if()</code> 來決定哪個優先。</p>
<p>Query 下好之後你應該會發現有個按鈕叫做 “Explore with Data Studio“。這就是我拿來畫圖的工具。你可以把 Data Studio 想像成 Excel 的圖表工具，只是它的資料源是 Google Cloud Platform 的某個 source。</p>
<p>為了方便肉眼閱讀，我設定了這些：</p>
<ul>
<li>Dimension: Year Month (Show as: YYYYMM)</li>
<li>Break Down Dimension: tweet_type</li>
<li>Break Down Dimension Sort: tweet_type, Ascending（這也是我加了 1.0 等數字的原因）</li>
</ul>
<p>Data Studio 提供了許多圖表可以用，像題圖的 Stack Area：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2019-11-03-twitter-usage-analysis/Screen%20Shot%202019-11-03%20at%2014.12.29.png" alt="" /></p>
<p>或是Stacked Bar， <del>可以看出我沒有 Monday Blue 但有 Thursday Blue</del> ：（Dimension Format 設成 Day of Week ）</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2019-11-03-twitter-usage-analysis/Screen%20Shot%202019-11-03%20at%2015.53.44.png" alt="" /></p>
<p>或是 Pie Chart，證明了我真的是轉貼魔人，近 60% 是轉推和分享連結：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2019-11-03-twitter-usage-analysis/Screen%20Shot%202019-11-03%20at%2015.57.18.png" alt="" /></p>
<h2 id="jie-lun-shu-ju-yao-fen-xi-cai-you-yi-yi">結論：數據要分析才有意義</h2>
<p>現在的工作上會碰到一些 SRE（System Reliability Engineering）的挑戰，需要設事件、做 log pipeline，在大量的雜訊裡找到系統有問題的訊號。我不是負責 SRE，但我需要負責送 event 出去，好讓他們可以分析。如果套到 Twitter 歷史資料來看的話，每一個近況更新（推文）都是一個事件，metadata 自然是有意義的，但拉長遠來看，我也可以得知自己上網習慣的變化。附帶一提，如果透過自然語言分析去處理內文，也可以建立自己想法的模型跟情緒，這也就是為什麼劍橋分析事件中，他們可以針對某特定族群下假訊息的廣告，也是為什麼你應該小心那些臉書小遊戲和算命 app。</p>
<p>雖然這裡展示的工具是 GCP BigQuery + Data Studio，但實際上應該有很多工具可以做到同樣的事情。身為 Web 工程師，SQL 對我來說沒什麼困難，但人工匯入資料建立 schema 是我不太想花時間做的事情，這也是我選擇 BigQuery 的原因：它可以自動偵測 schema。</p>
<p>可惜當初刪除 Plurk 的時候沒有下載備份，現在要分析也沒辦法了。就當作過往雲煙吧。</p>

            ]]></description>
        </item>
        <item>
            <title>中文姓名在日本</title>
            <link>https://blog.yorkxin.org/posts/chungwen-hsingming-tsai-jipen/</link>
            <pubDate>Fri, 05 Jul 2019 13:40:26 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2019/07/05/chungwen-hsingming-tsai-jipen.html</guid>
            
            <description><![CDATA[
                <p>2015 年我寫過一篇〈<a href="https://blog.yorkxin.org/2015/02/02/english-name">驗證在公司裡使用英文名字的迷思</a>〉，當時是任職於美商公司的台灣分公司，基本上還是跟台灣同事工作。事隔多年的現在，我搬到日本了，並且在外國人員工高達六成的公司工作。總共加起來在日本也居住三年了。這次就來說說所謂台灣人的中文名字在日本的情況吧。</p>
<h2 id="bi-ben-ming-zhong-yao-de-ri-yu-fa-yin-jia-ming-zhuan-xie">比本名重要的日語發音假名轉寫</h2>
<p>我初到日本時的居留證裡面並沒有漢字姓名，只有護照上的拼音「Chuang Yu Cheng」，順序比照日本的姓前名後。這就成了我的正式名字，因為日本的政府機關和金融機構只承認居留證上寫的姓名，即使出示護照也不被認可（但是台灣刻的漢字印章被承認了）。真正「取回」漢字姓名的時候，已經是更新在留期限（居留期間）拿到新的居留證的時候了。</p>
<p>但除了漢字或是羅馬字的姓名之外，日本生活中會很常被問到「姓名的日語發音」。實務上是替原本的姓名（不管是什麼文字）加註日文的<a href="https://zh.wikipedia.org/wiki/%E6%97%A5%E8%AA%9E%E5%81%87%E5%90%8D">假名發音</a>，表單上會標記「セイ・メイ」或「フリガナ」等。舉凡銀行開戶（關係到公司的薪水能不能轉到你的戶頭，以及自動扣帳能不能成功）、年金、甚至網上預約餐廳、理髮店等，很多情況下會被同時要求填寫「姓名和假名發音」這樣的組合。</p>
<p>對於日本人來說，看到羅馬字拼寫的姓名，第一反應就是用熟悉的英語來讀。我的拼音姓名照字面用英語讀的話，「Chuang Yu Cheng」就變成了「チュウアン・ユーチェン」。我聽說過有些人會用這種方式開戶、註記戶籍的別名，一方面也可以省很多麻煩，畢竟羅馬字照英語的發音對日本人來說很直覺也不陌生。</p>
<p>然而，畢竟我是和日本人一樣擁有漢字姓名的，就算不被官方承認，心底總是堅持要有一個跟自己漢字姓名有關聯的名字。於是我便給自己取了一個非正式名字，是中文姓名的日語發音。中文名字是「莊育承」，日語<a href="https://zh.wikipedia.org/wiki/%E9%9F%B3%E8%AE%80">音讀</a>裡變成了「そう・いくしょう（Sou Ikushou）」。從台灣或中國來日本的人，有不少人會採用這種發音。如「王」讀如「おう（Oh）」、「陳」讀如「ちん（Chin）」。</p>
<p>而這種規則當然也不是我發明的。在日本的新聞報導中，出現華人姓名的時候，發音大部分是日語音讀。如蔡英文成了「さい・えいぶん（Sai Eibun）」，賴清德成了「らい・せいとく（Rai Seitoku）」，而習近平就成了「しゅう・きんぺい（Shu Kinpei）」，偶爾會讀成「シー・ジンピン（Shee Jinpin）」，但機率不高。也就是說，現代日語中本來就有用日語音讀去讀華人漢字姓名的習慣，我取用日語音讀當姓名發音，應該是合乎文化習慣的。這也很符合漢字文化圈各自用當地發音規則的習慣，畢竟我們台灣人也擅自用國語發音直接讀日本人、韓國人，甚至香港人的名字。</p>
<p>那麼實際上有遇到什麼障礙呢？可以說並沒有太大的障礙。事實上，連銀行都承認了我自己取的日語發音。首先銀行在我開戶時接受了，後來不管是薪資轉帳、辦信用卡、投資帳戶、自動扣繳帳單，只要假名一致，就沒有問題（更精確地說，是被銀行的電腦結清系統受理了）。可以說，假名拼寫的一致性，比姓名是漢字還是羅馬字還重要。</p>
<p>於是「そう・いくしょう」就變成了我的半正式姓名。</p>
<p>而我可以擅自取發音，我認為，這跟日本的姓名文化有關。</p>
<h2 id="ming-cong-zhu-ren-de-ri-ben-xing-ming-fa-yin-yuan-ze">名從主人的日本姓名發音原則</h2>
<p>在日本生活三年來我注意到，日本人對於姓名的漢字該對應什麼發音，並沒有很嚴格的要求。</p>
<p>在台灣的國語裡，一個字基本上只有一個讀音；若有兩個音的話，會被稱為「破音」，也就是說國語重視由官方指定的正確的發音，超過方言發音。舉個例子，小時候我的國語課本裡「滑稽」的「滑」發音是「ㄍㄨˇ」，最近的課本變成了「ㄏㄨㄚˊ」。如果考試寫「ㄍㄨˇ」還會被批錯，可見官方發音的強勢。</p>
<p>然而在日語裡，一個字並不一定只有一個發音；即便是音讀（日語仿漢語的發音），也可能因為自中原傳入日本的時代不同，而有有所差異。如果拿我姓氏「莊」的日文常用漢字「荘」去查的話，會得到「しょう」（莊園、莊嚴）、「そう」（別墅、旅館、也可用在宿舍名）、「チャン」（麻將連莊）等發音。剛剛提到的「滑」，常見的就有「かつ」、「なめ」、「すべる」，而「滑稽」一詞的發音則是「こっけい」。</p>
<p>實際上人名和發音的對照也並不是一對一的。拿「新垣結衣」的名字「結衣」（ゆい）來說，常見的漢字還有「優衣」、「由依」等，但是發音都是相同的。又如「大野智」的名字「智」（さとし），同樣發音的漢字名也有「聡」、「智史」、「里志」等。類似的例子不勝枚舉，但在日本人的名字文化裡是很普遍的。</p>
<p>也就是說，日本人自己的姓名漢字和發音本來就是分開來的，不止正式的文書需要標記發音，連商業名片上都會註記發音。在社交場合，也無法光從發音去猜漢字——通常是拿了名片或加對方好友才發現漢字是如何如何寫；更不會隨便從漢字去猜發音，這恐怕比猜錯漢字還沒禮貌。在不知道漢字的情況下，直接在書信上寫假名其實是可以接受的。</p>
<p>日本人其實對於人名漢字與發音之間的連結並沒有像台灣有那麼嚴格的要求，基本上是尊重個人選擇的漢字發音的對應（當然漢字有<a href="https://ja.wikipedia.org/wiki/%E4%BA%BA%E5%90%8D%E7%94%A8%E6%BC%A2%E5%AD%97">法定的准用字集</a>，但是包括了許多傳統漢字和異體字）。於是我也可以搭順風車，我說「莊育承」讀作「そう・いくしょう」不會有人反對，我說「Chuang Yu Cheng」讀作「そう・いくしょう」連銀行都同意了。這也讓我有辦法把在身分證件沒有漢字名的情況下，硬是把我的名字和漢字名連結起來。</p>
<h2 id="zun-zhong-ren-ming-bu-ying-zuo-ren-he-jia-she">尊重人名，不應做任何假設</h2>
<p>上述的文化現象，有時候對於非日本文化出身的外國人，可能會遇到困擾。初到日本的外國人得先把自己姓名的日文發音和寫法背起來，即使她不需要日語能力就能夠工作生活。我遇到最大的一次麻煩，就是開辦證券帳戶時，系統規定漢字的姓、名最多加起來八個字。當時我的居留證還沒有漢字姓名，只能輸入羅馬字母。超過八個字的部分怎麼辦呢？該券商還提供了一個問答項目，解釋外國人該如何填寫（答案是名寫頭文字，姓寫到七個字滿，如 “CHUANG Y”），這顯然是系統設計當初只考慮日本人的問題。過兩天券商來電詢問我的全名，結果還是用我自己取的假名開戶了。</p>
<p>當然在台灣開發軟體系統可能也會遇到類似的問題。別以為每個人的姓名都是三個字、頂多四個字，2018 年中華民國行政院的發言人「<a href="https://zh.wikipedia.org/wiki/%E8%B0%B7%E8%BE%A3%E6%96%AF%C2%B7%E5%B0%A4%E9%81%94%E5%8D%A1">谷辣斯・尤達卡</a>」就有六個字呢，更別說著名的「<a href="https://zh.wikipedia.org/wiki/%E9%BB%83%E5%AE%8F%E6%88%90%E5%8F%B0%E7%81%A3%E9%98%BF%E6%88%90%E4%B8%96%E7%95%8C%E5%81%89%E4%BA%BA%E8%B2%A1%E7%A5%9E%E7%B8%BD%E7%B5%B1">黃宏成台灣阿成世界偉人財神總統</a>」那可是他的正式名字。</p>
<p>曾經看過一篇使用者體驗的技術文章說，做人名輸入的 UI，除非法定要求，最好是一個很長的輸入框即可。不要分姓和名，不要假設全家都同一個姓，不要假設每個人都有姓氏（日本皇室沒有姓），因為對自己最直覺的人名輸入框，可能只適用於自己的文化，而在那成千上萬不同的文化裡，總有你不知道的人名文化。擅自設計各種人名輸入的限制，只是徒增困擾而已。再說，人名怎麼寫，真的對自己的業務有影響嗎？</p>
<h2 id="yusan">「ユーさん」</h2>
<p>說了這麼多，實際上同事、朋友們又會有另一套叫法。除了最常見的禮貌叫法「そうさん」（姓+さん）、外國人居多的情況叫「ユーチェンさん」（讀如英語 /You Chain sang/），有一段期間我還被叫做「ユーさん」，來源是羅馬字的「Yu-Cheng」的「Yu」。</p>
<p>似乎有不少人以為有連字符的名字是可以分開的，固然西方文化裡面有這種姓名用法，但台灣人的名字連字符寫法，單純只是把兩個漢字的發音分開而已。中國漢人的姓名並不流行用連字符，整體看起來就是一個單字。在我觀察到連字符問題之後，我也就學中國人用「Yucheng」這個寫法了。只是有些人會覺得連字符是一種台灣人的特徵——或說「民國遺風」吧——拿掉連字符似乎就失去了台灣人的文化。當然這就是見仁見智了。</p>
<hr />
<p>如果我護照上的名字是台語發音，又會是怎麼樣的風格呢？</p>

            ]]></description>
        </item>
        <item>
            <title>日本新年號的資訊系統問題：合字</title>
            <link>https://blog.yorkxin.org/posts/japanese-era-name-ligature/</link>
            <pubDate>Fri, 05 Apr 2019 20:48:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2019/04/06/japanese-era-name-ligature.html</guid>
            
            <description><![CDATA[
                <p>日本因<a href="https://zh.wikipedia.org/wiki/%E6%98%8E%E4%BB%81%E5%A4%A9%E7%9A%87%E9%80%80%E4%BD%8D">天皇讓位</a>，將自 2019 年 5 月 1 日起採用新的年號，稱為「改元」。同年 4 月 1 日頒布了新的年號為「令和」。日常生活中使用和曆（日本年號紀元）而非西元的地方非常多，因此日本經濟產業省發表了一系列的「<a href="https://www.meti.go.jp/policy/it_policy/kaigen/kaigen_taiou.html">改元所伴隨的企業資訊系統修正之對策</a>」，例如紙本表單若是來不及修改，則可以用橡皮章修正；政府機關也容許「平成 33 年」這種未來的年份存在無法修正的文件上，如駕照期限。</p>
<p>但與 1989 年昭和改平成不同的是，21 世紀的生活大量依賴電腦資訊系統，尤其是商業活動、政府和銀行，因此作業系統和應用軟體將不得不支援新的年號，堪稱「日本的 Y2K 問題」。在經產省的網頁上，可以找到一份<a href="https://www.meti.go.jp/policy/it_policy/kaigen/2019ms.pdf">日本微軟的技術說明會簡報</a> (PDF)，裡面提及了不少他們將如何支援新年號的問題，使用者可能會遇到的問題，以及哪些舊軟體將不支援新的年號。例如，Excel 自動填滿日期的時候應該要在 2019/5/1 換新的年號、官方採用「元年」而非「1 年」。某些本地化模組也將支援西元和和曆的自動轉換。</p>
<p>其中我比較關注的是合字的文字編碼問題，在這裏特別拿出來講一下。</p>
<h2 id="nian-hao-de-he-zi">年號的合字</h2>
<p>日本的年號其實是有<a href="https://zh.wikipedia.org/wiki/%E5%90%88%E5%AD%97">合字 (ligature)</a> 的。什麼叫做合字呢？基本上就是多個字合起來變成一個字。在活字印刷時代，會把常用的組合鑄成一個活字，例如 fi 有一個 ﬁ 的合字。Unicode 裡面也收錄了不少人類文明史上的合字，包括日本明治以來的年號合字：</p>
<ul>
<li>㍾ (U+337E) = 明治</li>
<li>㍽ (U+337D) = 大正</li>
<li>㍼ (U+337C) = 昭和</li>
<li>㍻ (U+337B) = 平成</li>
</ul>
<figure>
  <img alt="" src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2019-04-06-japanese-era-name-ligature/1*09IfADp6K8oSO702Z3YW1g.png" title="" />
  <figcaption>明治、大正、昭和、平成四個日本年號的合字在 Unicode 的碼位。圖為 macOS 內建的 Character Viewer。</figcaption>
</figure>
<p>因應新年號實施，Unicode 當然也要給這個合字一個碼位。不過「㍻」前面的 U+337A 已經被指定了，「㍾」後面的 U+337F 也被指定了（㍿），只能選一個別的區段，最後 Unicode 決定<a href="http://blog.unicode.org/2018/09/new-japanese-era.html">保留 U+32FF 給新年號的合字</a>用，但是無法立刻發布新版本。因為 <strong>Unicode 編纂合字時，會一併紀錄合字拆開來是哪些字</strong>，這稱為正規化 (Normalization)。例如<a href="https://www.fileformat.info/info/unicode/char/337b/index.htm">「㍻」的拆解</a>就是<code>&lt;square&gt; 5E73 6210 [ ‌平 ‌成 ]</code> 。他們的計畫是在年號公布之後儘快<a href="https://www.unicode.org/versions/Unicode12.1.0/">發佈 Unicode 新版本</a>。</p>
<p>此外字型也需要支援這個字符。在上述的微軟技術簡報中指出，Windows 內建字型，包括從別的廠商買來的授權，將近 30 套字型需要支援這個合字。並且還有直排橫排要支援。而 Adobe 動作也很快，在新年號公布之後，已經開始<a href="https://www.watch.impress.co.jp/docs/news/1177681.html">更新 Adobe Fonts 的字型</a>了。</p>
<p>在作業系統內有安裝新版字型的情況下，應該可以看到如下的合字：</p>
<ul>
<li>㋿ (U+32FF) = 令和</li>
</ul>
<p>或是參考這個示意圖：</p>
<figure>
  <img alt="" src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2019-04-06-japanese-era-name-ligature/1*SLxlZhd4DPxh-i_W693i7A.png" title="" />
  <figcaption>「令和」合字模擬示意圖。用繪圖軟體縮放的兩個字，筆畫不成比例請見諒。</figcaption>
</figure>
<h2 id="nian-hao-he-zi-de-lai-yuan">年號合字的來源</h2>
<p>這些合字是怎麼來的呢？為了寫這篇文章我上網搜尋了一下。細節可以參考這篇 <a href="https://weblabo.oscasierra.net/shift_jis-windows31j/">Shift_JIS 編碼名稱的故事</a> 和 <a href="https://blogs.adobe.com/CCJKType/2019/03/era-name-ligature-history.html">Adobe 官方的日本年號合字簡史</a>。</p>
<p>1982 年，日本微軟和當時的一些系統廠商制定了 Shift_JIS 碼表來在 16 位元 OS 裡表示日文（關於制定的歷史可以看一下<a href="https://ja.wikipedia.org/wiki/Shift_JIS">維基百科</a>）。在 MS-DOS 裡面稱為 <a href="https://ja.wikipedia.org/wiki/Microsoft%E3%82%B3%E3%83%BC%E3%83%89%E3%83%9A%E3%83%BC%E3%82%B8932">CP932</a>。微軟開放了其他 OEM 廠可以任意擴張字元，於是 NEC 和 IBM 就各自加入了自己的字符。年號的合字就是 NEC 加進去的。</p>
<p>1993 年 Windows 3.1 問世時，微軟禁止其他廠商任意擴張字元，並把 NEC 和 IBM 自己加入的字符都統一了。這個新的碼表叫做 Windows-31J，在 MS-DOS 系統內部依然是 CP932，直到今天。</p>
<p>有趣的是，上述的微軟技術簡報中明確指出，<strong>Shift_JIS 將不支援新年號「令和」的合字</strong>。也就是說，如果文件是採用 Shift_JIS 編碼，又剛好用了「㍻」等合字，那麼新年號是鐵定無法支援的，除非貼圖（像前文的示意圖）、自行造字，或是放棄合字，改用兩個漢字。</p>
<h2 id="guan-yu-geng-huan-nian-hao-de-shi-cheng">關於更換年號的時程</h2>
<p>根據現行日本國憲法，天皇無權干政，而頒布年號是政府的職權而非天皇本人，並僅限於皇位繼承。（承襲自明治時代所規定的<a href="https://ja.wikipedia.org/wiki/%E5%85%83%E5%8F%B7%E6%B3%95">一世一元</a>制度，以前日本也像中原傳統一樣，會因天災人禍甚至君主心情而改年號）。</p>
<p>跟以往因天皇駕崩而在當日改元有所不同，這次政府希望盡量降低社會成本，而打算提早公布。不過政府內的<a href="https://www.asahi.com/articles/ASL5K5GMBL5KUTFK00L.html">保守派基於避免兩個權威的原則</a>，希望可以當日公布並實施。最後協調的結果是登基一個月前的 4/1 公布新的年號。</p>
<blockquote>
<p>政府は当初、改元の準備期間を長くとるため今夏ごろの公表を検討。しかし、新元号の発表によって天皇陛下と新たに即位する皇太子さまという「二重権威」が生じるとの懸念が強まり、公表時期をできるだけ即位日に近づける方向となった。</p>
<p>——<a href="https://www.asahi.com/articles/ASL5K5GMBL5KUTFK00L.html">《</a><a href="https://www.asahi.com/articles/ASL5K5GMBL5KUTFK00L.html">朝</a><a href="https://www.asahi.com/articles/ASL5K5GMBL5KUTFK00L.html">日新聞》 2018年5月17日 大久保貴裕</a></p>
</blockquote>
<p>至於為什麼是 4/1 呢？據說是微軟向政府要求的。原本政府想要在 4/10，天皇在位 30 年紀念活動隔日，即 4/11，公布新的年號，但 Windows 有固定的更新時程，是每個月的第二個星期三。2019 年 4 月是 4/10。如果不在那之前發布更新檔（註冊表），就只能等到 5/8 了。</p>
<blockquote>
<p>同（マイクロソフト）社は毎月１回、第２水曜日に全世界統一でソフトの更新を行うが、４月は１０日、５月は８日となる。政府は当初、４月１０日に開かれる天皇陛下ご在位３０年の「お祝いと感謝の集い」の翌１１日に新元号の公表を検討していたが、１１日ではソフト更新に向けた改修作業が次の５月８日まで行うことができず、同月１日の改元には間に合わない。</p>
<p>——<a href="https://www.sankei.com/life/news/190104/lif1901040027-n1.html">《産経新聞》2019年1月4日 小川真由美</a></p>
</blockquote>
<p>如果屬實的話，微軟果然是世界的巨頭啊，連日本年號的發布日期都受他們的影響😂</p>
<p>但話說回來這次平成的天皇明仁生前退位，也是在 2016 年他老人家發表了「<a href="http://www.kunaicho.go.jp/page/okotoba/detail/12">關於象徵天皇之務的想法</a>」的演講，說他身體撐不下去了，但是依法他無法干政，也無法自行宣布退位，懇請各位國民體諒。言下之意就是拜託政府立個法讓我讓位吧。他也許也預想到自己若自然辭世，必然會重演 1989 年當時社會無法因應新年號的問題。這次可以平順地過度到新的年號，我認為是他給日本社會最大的禮物之一。</p>
<h3 id="nian-hao-yu-xi-yuan-de-zhuan-huan">年號與西元的轉換</h3>
<p>日本作為世界上少數採用自有年號的國家，外界可能不太清楚年號是怎麼在日常生活使用的。事實上<strong>日本的年號是以日為單位的</strong>。</p>
<p>例如<a href="https://zh.wikipedia.org/wiki/%E5%B9%B3%E6%88%90">昭和換平成</a>的時候是 1989/1/7 駕崩，1/8 起採用新的年號。也因此在正式的文書裡面，記載和曆生日是需要看日期的。1989/1/7 以前出生的人是昭和 64 年，1/8 以後出生的則是平成元年。這也反映到了網上金融系統，如線上申請信用卡的網站，會同時有昭和 64 年和平成元年的選項。</p>
<p>而年度的區分則還是照陽曆年末年初的規則，所以令和元年是從 2019/5/1 到 12/31，到 2020/1/1 就是令和 2 年了。</p>
<p>前大日本帝國最後一次更改年號是 <a href="https://zh.wikipedia.org/wiki/%E6%98%AD%E5%92%8C">1926/12/25 大正改昭和</a>，而 1926/12/25 既是大正 15 年也是昭和元年。該從幾點幾分切開我沒找到，但大概也不重要了吧。真要探究的話，當初日本帝國還有兩個時區（<a href="https://ja.wikisource.org/wiki/%E6%A8%99%E6%BA%96%E6%99%82%E3%83%8B%E9%97%9C%E3%82%B9%E3%83%AB%E4%BB%B6_%28%E5%85%AC%E5%B8%83%E6%99%82%29">台灣屬於 GMT+8 西部標準時</a>），那麼當地幾點幾分開始算昭和呢？也是個會讓人想破頭的問題吧。</p>

            ]]></description>
        </item>
        <item>
            <title>把 API 效能提高近 10 倍的故事</title>
            <link>https://blog.yorkxin.org/posts/how-i-improved-json-performance-in-rails/</link>
            <pubDate>Mon, 18 Mar 2019 03:01:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2019/03/18/how-i-improved-json-performance-in-rails.html</guid>
            
            <description><![CDATA[
                <p>最近在調公司專案的 HTTP API 效能。背景是 Ruby on Rails 和 RESTful API，第一天就上 <a href="https://github.com/flyerhzm/bullet">bullet</a> 防 SQL N+1，所以主要問題不在 N+1。API Spec 使用 OpenAPI 寫的，測試全部都通過 OpenAPI 的 JSON Schema validator（詳見<a href="https://speakerdeck.com/chitsaou/web-development-with-openapi-spec-and-fast-testing?slide=26">我的演講簡報</a>），所以可以放大膽 refactoring。</p>
<p>最初用了 Rails 推薦的 <a href="https://github.com/rails/jbuilder">JBuilder</a>，沒考慮到效能，QA 也不覺得慢，就沒注意。結果 APM 統計出來數字很難看，某特定 endpoint 的 rps &lt; 2（requests per second），就開始認真調查了。</p>
<p>首先是發現 SQL 花的時間很長，極端狀況下甚至會 timeout。仔細一看才發現是 schema 的 nesting 太深，四五層左右，還有重複內容的 array。這是 OpenAPI Schema 互相嵌套的潛在問題，錯在當初 review API spec 沒仔細審 schema。這種四五個表格互相 join 我是不會調 SQL 效能啦。但實際上看前端 UI 也沒用到，就跟前端工程師協調把那些 array 直接放空，終於可以拉到 rps 3 以上了。</p>
<p>但還是很慢，就想說改用別的 JSON Serializer。首先選的是 <a href="https://github.com/rails-api/active_model_serializers">ActiveModel::Serializer</a>，速度立刻翻倍。但 AMS 的 API 有點醜，bug 也不少，他們自己甚至把整個 gem 砍掉重練；我的話是要在 root 加料的時候很麻煩。但看在速度的份上還是用下去了。這樣子 rps 拉到了 5 以上。直接擺脫 ActionView 的 Template 果然是正確的選擇 — — JBuilder 似乎每 render 一次 partial 就會重新開一次檔案，時間都浪費在 disk I/O；而 class 的話一啟動就存在記憶體裡面了。</p>
<p>不過在找 JSON Serializer 的時候，固然發現到其他 gem。其中一個叫做 <a href="https://github.com/yosiat/panko_serializer">panko_serializer</a>，是用 C 寫的，號稱有特別的優化（關於 type casting）。效能是 AMS 的兩倍，該當 endpoint 的 rps 拉到了 10 左右，要在 root 加料也很簡單。缺點是嚴重依賴 db column 的 raw 值，只能輸出 UTC 時間（除非改用 method 叫），而且有隱藏的「如果不是 db column 就會變成 <code>nil</code>」的問題。加上專案本身還很新，感覺維護成本很大，就放棄了。</p>
<p>最近試的是 <a href="https://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-as_json">ActiveModel::Serialization::JSON#as_json</a>，就是真正內建在 Rails 裡面的那個。搭配 <a href="https://github.com/ohler55/oj/blob/master/pages/Rails.md">Oj encoder 的 Rails 優化功能</a>的話，速度只差 panko_serializer 一點點（5% 左右）。該 endpoint 測出來大概 8 rps 而已。</p>
<p>原本覺得他的 API 很難看，沒有 Serializer 好讀；但是仔細看了一下其實也沒什麼難懂，基本上就是事先用 hash 寫靜態的 structure。指定 association 的方式也很簡單，也因此比較容易發現多曾嵌套問題。</p>
<p>panko_serializer 的 attribute-only 問題他也有，但是 API 文件寫的比較好讀，事前指定 call instance method 就行了。缺點是不能設定 attribute alias，必須 method alias 寫在 Model 裡面，然後當作 method 去拉。</p>
<p>此外沒有對 association 加工的功能（前文提到我需要把某個深深的 array 清空），其他 serializer 或 Jbuilder (view) 都可以寫 if-else 來處理。不過好在 <code>as_json</code> 拉出來就是 hash，反正對這個 hash 用 <code>yield_self</code>或 <code>tap</code> 加料就好了。</p>
<p>另一個隱藏的問題是，如果 association 是 nil，那麼他連 key 都不會寫進去，JSON Schema validator 就在唉了。我不知道這<a href="https://github.com/rails/rails/blob/5-2-stable/activemodel/lib/active_model/serialization.rb#L186">是 bug 還是 feature</a> 啦，總之是 hash 嘛，對他加料即可。</p>
<p>測試的工具是 <a href="https://github.com/wg/wrk">wrk</a>，之前還用過 <a href="https://en.wikipedia.org/wiki/ApacheBench">Apache Bench (ab)</a>，但覺得 wrk 直接指定單線程刷 n 秒看 rps 比較直觀。都不喜歡的話可以參考 <a href="https://github.com/denji/awesome-http-benchmark">awesome-http-benchmark</a> 這個列表找自己喜歡的即可。</p>
<p>至於最近很紅的 Netflix 的 <a href="https://github.com/Netflix/fast_jsonapi">fast_jsonapi</a>，我沒用它，因為有個要件：你的 API response 必須是 <a href="https://jsonapi.org/">JSON:API</a> 格式（有固定的 meta attributes 要套上）。我們的 API 很不巧並不是 JSON:API 格式，所以不能直接上。曾經我參加過一個 workshop 號稱改用 fast_jsonapi 就可以效能翻不知道幾倍，結果我看他的 code 根本就一堆 N+1 問題，修掉 N+1 就翻倍再翻倍了。更別說原本他的 API 也不是 JSON:API 規格，效能翻倍之前先逼死前端吧。</p>
<p>以上就是我把 API 效能提高近 10 倍的故事。結論是</p>
<ol>
<li>API Spec 的 schema 多層嵌套是 red flag</li>
<li>直接刷 API endpoint 測效能</li>
<li>慎選 JSON Generator</li>
<li>效能不佳，先解 SQL 效能</li>
<li>世界上沒有 silver bullet。別人的場景跟你不見得相同</li>
</ol>

            ]]></description>
        </item>
        <item>
            <title>失去的歷史和留下的歷史</title>
            <link>https://blog.yorkxin.org/posts/heritage/</link>
            <pubDate>Sun, 22 Jul 2018 22:25:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2018/07/23/失去的歷史和留下的歷史.html</guid>
            
            <description><![CDATA[
                <p>日前參觀<a href="http://www.suidorekishi.jp">東京都水道歷史館</a>，裡面從四百年前江戶幕府時代的上水（從鄰近的湖泊或河川引水）開始講，到明治時代的上下水道現代化，到關東大地震缺水，到二戰期間水道系統被炸爛，到修復到可以生飲，到戰後團地住宅興起，家戶有浴缸，都是跟民生用水有緊密關聯。</p>
<p>看一個都市的歷史總是有許多東西可以學習跟比較的。例如民生用水在技術上是怎麼引進都市的，在公共水井取水的婦人會趁機會交換八卦（<a href="https://ja.wikipedia.org/wiki/%E4%BA%95%E6%88%B8%E7%AB%AF%E4%BC%9A%E8%AD%B0">井戶端會議</a>；日文維基百科的社群討論區也叫做<a href="https://ja.wikipedia.org/wiki/Wikipedia:%E4%BA%95%E6%88%B8%E7%AB%AF">井戶端</a>）。以及一些重要的地名：</p>
<ul>
<li><a href="https://zh.wikipedia.org/wiki/%E4%BA%95%E4%B9%8B%E9%A0%AD%E6%81%A9%E8%B3%9C%E5%85%AC%E5%9C%92">井之頭</a>：神田上水的源頭，玉川上水開通前最主要的水源</li>
<li><a href="https://zh.wikipedia.org/wiki/%E6%BA%9C%E6%B1%A0%E5%B1%B1%E7%8E%8B%E7%AB%99">溜池</a>：曾有天然湧水池，也是江戶城外濠的一部分</li>
<li><a href="https://zh.wikipedia.org/wiki/%E6%B0%B4%E9%81%93%E6%A9%8B%E7%AB%99">水道橋</a>：跨河道的引水渠道</li>
</ul>
<p>有些竟然還有浮世繪，可以更具體地想像當時是長什麼樣子的。</p>
<p>但回家路上反覆咀嚼時，我感受到一件事：我喜歡在日本看歷史博物館，也許是為了尋找他們跟台灣的關聯。</p>
<p>在台灣，要得知正確的歷史很困難，由於政權更迭、文件佚失、故意隱瞞等原因，現代充斥著許多穿鑿附會、以訛傳訛、張冠李戴的假歷史。你跟他們指正嘛，也不見得會認真對待，甚至回以不要那麼認真、見仁見智之類的幹話。最有名的大概就是<a href="https://mapstalk.blogspot.com/2015/07/blog-post.html">台北瑠公圳</a>了。</p>
<p>目前最可信的產業史還是日本時代遺留下來的產業遺跡，我想是因為不少在日本其實都可以找到類似的東西。例如在明治時代的東京，曾經有<a href="https://ja.wikipedia.org/wiki/%E6%B7%80%E6%A9%8B%E6%B5%84%E6%B0%B4%E5%A0%B4">淀橋淨水場</a>（今東京都廳，當地也是 Yodobashi Camera 的發祥地），類似的東西就是<a href="https://zh.wikipedia.org/wiki/%E8%87%BA%E5%8C%97%E6%B0%B4%E9%81%93%E6%B0%B4%E6%BA%90%E5%9C%B0">公館水源地</a>的淨水廠，而水源市場的名字也是來自當時的水源地。</p>
<p>在我看來，台灣人是一個庶民文化長期被殖民國忽視的族群，今天佔主流的中華文化還是某種程度上鄙視庶民文化。日本有浮世繪紀錄庶民生活，台灣的文化卻有不少是缺乏可信紀錄的。日本經歷過政治動盪，但是沒有被征服過；台灣經歷了許多次的政治清洗。失去的東西固然很難找回來，但至少可以在別國找到一點線索。</p>
<p>也就是之前在推特看到的說法：喜歡去日本玩的台灣人，有些也許是羨慕日本人還保留了他們自己的歷史文明。</p>
<p>應該反省為什麼會失去、該如何保留這些僅存的文化遺產。我想這是歷史博物館教我最多的東西。</p>

            ]]></description>
        </item>
        <item>
            <title>我為何撤銷了大部分網站的 Facebook 帳戶連結</title>
            <link>https://blog.yorkxin.org/posts/signing-out-from-facebook-login/</link>
            <pubDate>Wed, 21 Mar 2018 02:46:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2018/03/21/signing-out-from-facebook-login.html</guid>
            
            <description><![CDATA[
                <p>如果定期更換密碼是傳統上網的密碼安全守則，那麼定期清查並撤回社群網站登入及資料使用權，便是當代上網的安全守則。</p>
<p>身為現役 Web 工程師，資訊安全不只教條，還是必須在生活中實踐的原則。近來 Facebook 的帳號安全事件，給了我動力去清查所有用 Facebook 登入的第三方網站，並且積極把 Facebook 登入方案從其他網站刪除，或加上密碼登入。本文分享我的動機，以及實踐後的結果。本文雖然針對 Facebook，但也適用於 Twitter、Google 等社群網站帳號登入，只是我用 Facebook 登入的網站特別多，所以以此為例。</p>
<h2 id="mei-you-yong-yuan-de-facebook-zhang-hao">沒有永遠的 Facebook 帳號</h2>
<p>從 2007 年開立帳號以來，見證了 Facebook 從默默無名的美國留學生通訊錄，到取代無名小站，躍身為台灣兩大社群服務（另一個是 LINE），大企業行銷不能忽略他的廣告平台，靠他的快速登入和廣告集客力起家的小新創更不知幾許。商業之外，其快速發酵的效果，與昔日的網路論壇 PTT 互相呼應，也促成台灣在 21 世紀的第一個大型民主運動。可以說，Facebook 已經成為台灣現代流行文化的一部分了。</p>
<p>然而 2018 年初，Facebook 帳號安全問題頻傳。台灣國內有批評中國政府便被刪文甚至封帳號而逃到 Twitter 的臉書難民，在其本營美國則有 Facebook 疏於保護個人資訊任其流竄的醜聞。儘管 Facebook 是來自言論自由的國度、來自世界資訊科技首都矽谷，無論是政策上跟安全技術上都值得信任，但就像矽谷新創九死一生（字面意義），世界上沒有永遠的公司，沒有永遠安全的系統，當然也沒有永遠流行的網路服務。</p>
<p>這裏就有了一個動機：<strong>萬一我的 Facebook 帳號被封，或是 Facebook 倒閉，至少希望其他網站不能受到影響。</strong></p>
<h2 id="mei-you-yong-yuan-po-bu-liao-de-wang-zhan">沒有永遠破不了的網站</h2>
<p>大家都愛「Facebook 登入」。使用者喜歡他簡單好用，產品經理喜歡他有效提升轉換率。但如果你不是 Web 工程師，首先要知道的是，在一個網站上按下「Facebook 登入」後會發生什麼事情。</p>
<p>當你從「Facebook 登入」完成註冊之後，技術上可以做到，你不需要輸入個別的帳號密碼就可以登入那個網站。網站會拿到一個 Facebook 帳戶的識別號碼，大部分情況下還會拿到 Email 地址，網站可以透過這兩個東西來驗證你就是你，因為可以透過 Facebook 驗證。</p>
<p>但你有沒有注意到，某些網站會同時要求你提供「好友名單」來幫你配對已經在同一個網站上註冊的好友。既然要配對，就需要取得好友名單，這很合理。但要知道的是，就算你沒有打開該網站，網站還是可以自動去下載好友名單來配對。</p>
<p>此外連生日、學經歷、照片等等，甚至你發表在塗鴉牆跟社團裡面的內容，都可以取得，端看你當時允許了網站使用那些資訊。這有好的用途，也有壞的用途。理論上網站必須要盡量減少取得的資訊，但有些初級程式設計師沒有資訊安全概念，寫出會取得所有資訊的程式，這是問題，然而最大的問題不在於取得多少資訊，而在於「如何安全保存這些資訊」。</p>
<p>要知道，「網站資料外洩」這種事情是很常發生的；通常是網站資料庫遭駭客入侵而整個被下載，所以俗稱「拖庫」。如果連製作網站的工程師都有可能因為疏於管理而發生拖庫等資安事件，作為一般使用者的我們，又怎麼能夠相信網站上的資料不會被駭客拖走呢？即使軟體資訊安全上沒有問題，那麼監守自盜的問題呢？</p>
<p>這裏就有了另一個動機：<strong>為了避免網站隨意取得我的私密資料</strong>。</p>
<h2 id="bu-zai-you-gong-yong-mi-ma-dai-lai-de-feng-xian">不再有共用密碼帶來的風險</h2>
<p>我以前會積極使用 Facebook 登入的主要原因是，通常不需要設定密碼。我也開發過這種程式，很清楚這是怎麼設計的，而且並不只是不需要，而是「沒必要」設定密碼。不想設定密碼的原因是我無法記住不同網站的密碼。大家都知道，共用密碼是很危險的。一個網站被拖庫，你不知道他的密碼是明文還是單向加密，萬一是明文，那麼你就等著其他網站的帳號都被駭吧。所以，以前凡是遇到小型網站或是新創公司的服務，我都盡量用 Facebook 登入。另一方面也是方便。</p>
<p>不過理論上，只要不跟其他網站共用密碼，這個風險就不存在了。然而在沒有密碼金庫軟體的時代，這幾乎是不可能的事情，畢竟我的記憶力沒有好到可以記住數百個不同的字串。但今天，瀏覽器甚至作業系統已經內建密碼金庫，我也已經用 <a href="https://1password.com/">1Password</a> 好多年了，新註冊的網站幾乎都是直接採用隨機密碼，然後存入金庫。能開通多重認證的就都盡量開通，再有什麼網站被駭，都可以高枕無憂。</p>
<p>既然當初積極使用 Facebook 登入的誘因已經有更好的解決方法，那麼何必再使用 Facebook 登入呢？這是第三個動機。</p>
<h2 id="zhu-yi-qing-cha-facebook-deng-ru-zhang-hu">逐一清查 Facebook 登入帳戶</h2>
<p>要知道你授權了那些網站可以取得你的 Facebook 資料很簡單，打開<a href="https://www.facebook.com/settings?tab=applications">「應用程式設定」</a>就可以了。此前我隱約記得有登入過很多網站，但沒想到竟然超過一百個。下圖是我已經刪了一半時才想起來要截的圖，之後這些應用程式有一半被我刪掉了，可見當初真的太隨便就按登入。</p>
<figure>
  <img alt="" src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2018-03-21-signing-out-from-facebook-login/1*bitHIrfk_pdq5znicVkY5A.png" title="" />
  <figcaption>你授權了多少網站可以取得你的 Facebook&nbsp;個人資料呢？</figcaption>
</figure>
<p>逐一點開每一個應用程式的圖示，就可以知道該程式可以取得那些資料。大部分都是個人基本資料（姓名、性別、大頭貼、是否成年，無法拒絕）以及 Email、生日。有些會要求現居城市，有些要求了好友名單，求職相關的會要求學經歷等等。但如果仔細看看這些網站的內容，會發現他們不少都會要求過多的資料。例如並非基於地理資訊的服務，也要求提供居住城市；並未基於年齡或生日促銷，卻要求提供生日；並未提供強社交功能，卻要求提供好友名單等等。</p>
<figure>
  <img alt="" src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2018-03-21-signing-out-from-facebook-login/1*HyGJy2e4yNoJdxQPtH8-Lw.png" title="" />
  <figcaption>要求過多的個人資料一例：蝦皮購物</figcaption>
</figure>
<p>這裏最麻煩的在於「逐一點開」。Facebook 的這個管理頁面並不提供讓你一眼就看出那些應用程式有什麼權限的功能。如果你像我一樣有一百個，那就得老老實實地按一百次。</p>
<p>然後你就有機會發現有些應用程式幾乎可以看光你的資料呢：</p>
<figure>
  <img alt="" src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2018-03-21-signing-out-from-facebook-login/1*xIpiSYT66XyM94jo88PQHg.png" title="" />
  <figcaption>PlayStation Network 要求的資料竟然是全部。我當初怎麼會同意呢。附帶一提 Anobii&nbsp;也是。</figcaption>
</figure>
<h2 id="shan-chu-na-xie-bu-zai-xiang-tou-guo-facebook-deng-ru-de-wang-zhan">刪除那些不再想透過 Facebook 登入的網站</h2>
<p>綜上所述，本次清查 Facebook 登入的目的在於：</p>
<ul>
<li>避免登入被限制為 Facebook 導致一家死全部死的問題（單點故障）</li>
<li>朕的資料是朕的資料，朕不給的你不能拿（停損）</li>
<li>實踐一個網站一個密碼的獨立密碼</li>
</ul>
<p>在逐一清查的過程中，我發現有太多當初為了省事而直接點 Facebook 登入的網站，而且有太多是我無法相信他們的系統是安全的。所以本次清查，理想的情況下要達到以下目標：</p>
<ul>
<li>對於不再使用的網站，刪除帳戶；若無法刪除，至少撤回資料使用權</li>
<li>對於頻繁使用的網站，設定 Facebook 以外的登入方式，最好是密碼登入</li>
</ul>
<p>然而事情不是那麼簡單。</p>
<h3 id="xiang-huan-mi-ma-deng-ru-qing-zhao-hou-men">想換密碼登入，請找後門</h3>
<p>雖然有些網站可以讓你設定密碼，但帳號管理畢竟不是主要的業務功能。至今遇到這幾種情況，根據難易度排序，最簡單的在上：</p>
<ol>
<li>在透過社群登入的情況下，可以設定密碼，之後允許刪除社群登入連結</li>
<li>在透過社群登入的情況下，可以設定密碼，但無法刪除社群登入連結</li>
<li>在透過社群登入的情況下，也只能更改密碼，但我並沒有「舊」密碼，只能透過「忘記密碼」來設定新密碼</li>
<li>在透過社群登入的情況下，無法設定密碼，但是可以透過「忘記密碼」來設定密碼</li>
<li>在透過社群登入的情況下，無法設定密碼，也無法透過「忘記密碼」來設定密碼</li>
</ol>
<p>看到沒？這真的是很罕見的需求呢，連常見的帳戶管理程式庫都沒有好的支援。其中 1 跟 2 算是最容易處理的，就算無法刪除社群帳號連結，還是可以從 Facebook 應用程式管理頁面直接吊銷，杜絕網站今後取得更多資料。</p>
<p>3 跟 4 是產品經理該負的責任，但畢竟可以繞一圈設定密碼，雖然不直觀。</p>
<p>5 完全是鎖死的的情況，你在這個網站只能用 Facebook 登入。要是你的 Facebook 帳號被刪除，那麼你就跟這個網站無緣了。因為這些網站很不對用戶負責任，我只好列在這裡：</p>
<ul>
<li>Expedia （找不到設定密碼的方式）</li>
<li>Funliday（不提供密碼登入）</li>
<li>Product Hunt （僅提供 Facebook / Google / 母公司 AngelList 的登入）</li>
<li>TAAZE 讀冊生活（因為是 Facebook 帳號，無法設定密碼）</li>
<li>旅行酒吧（因為是 Facebook 帳號，無法設定密碼；去信客服手動更換）</li>
<li>蝦皮購物（需要台灣手機號碼接收認證碼，尚未核實）</li>
</ul>
<h3 id="mei-you-yong-yuan-de-yong-hu">沒有永遠的用戶</h3>
<p>有些網站當初註冊是抱著嚐鮮的心態，現在沒在用了，最好能刪就刪。不少日本網站都直接提供「刪除帳號」（退会）的服務，這當然輕鬆許多，至於真的刪除與否，姑且信之。但大部分其實是不提供的，尤其是美國跟台灣的新創網站，只好如前文所述直接吊銷 Facebook 連結。</p>
<p>此外還有些竟然把 Facebook 登入的程式整個移除了，讓我想了一下才知道如何透過「忘記密碼」來登入。</p>
<h3 id="mei-you-yong-yuan-de-wang-zhan">沒有永遠的網站</h3>
<p>網站還活著的還能處理，但有些竟然是倒閉了，或是工程師做的玩具網站，沒在維護，上網搜尋也找不到，不知道該從哪裡登入。</p>
<p>仔細想想，這種情況下才更應該用獨立密碼登入，畢竟是玩具網站，安全性跟隱私原則都是不明的，其中一個竟然還是未加密的 HTTP。</p>
<p>其中印象最深刻的是 aNobii，這個曾經的愛書人的集散地，現在還有人在用嗎？這個服務據說數年前被義大利的出版商買下了。現在你再去登入，就會發現網站翻譯做的很糟糕，混合了義大利文(?)跟繁簡中文，連密碼重設信件都是我看不懂的文字，得靠網址才能猜得出來。</p>
<p>最嚴重的問題在於 aNobii 的 Facebook 登入是授權「所有資料」，跟上述的 PlayStation Network 一樣，是非常要不得的。要不是這次做檢查，大概也不知道是這種危險的情況。</p>
<h2 id="jie-yu-bu-xiang-gei-ren-kan-de-dong-xi-jiu-bie-shou-quan-gei-ren-kan">結語：不想給人看的東西就別授權給人看</h2>
<p>我花了一整天清查一百多個 Facebook 登入的現況，最後還保留的只剩下 27 個。有那些會偷偷在後台下載「最新資料」，又有那些會拿這些資料去做各種所謂的調研，我無從得知。在 Facebook 成為台灣第一大公眾社群網站的現在，我們不知不覺在 Facebook 上寫了許多，你以為沒價值，但對於某些人來說很有價值的資料，也在不知不覺中，透過這種與外部網站的連結，流到第三方的手上。很多時候我看到有人在玩占卜遊戲，都覺得資訊安全真的要從小教育，但應用軟體的世界變化太大，該怎麼讓非專業人士的大眾了解，真的非我能力可及。</p>
<p>最後我竟然開始認同日本人消極使用 Facebook 的心態，感覺那是一種自我保護的行為。</p>

            ]]></description>
        </item>
        <item>
            <title>Things I learned from migrating a Chrome extension to Firefox using WebExtensions</title>
            <link>https://blog.yorkxin.org/posts/things-i-learned-from-migrating-a-chrome-extension-to-firefox-using-webextensions/</link>
            <pubDate>Mon, 25 Sep 2017 02:01:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2017/09/25/things-i-learned-from-migrating-a-chrome-extension-to-firefox-using-webextensions.html</guid>
            
            <description><![CDATA[
                <p>I've been developing a browser extension called Copy as Markdown (<a href="https://chrome.google.com/webstore/detail/copy-as-markdown/fkeaekngjflipcockcnpobkpbbfbhmdn">Chrome</a>, <a href="https://addons.mozilla.org/en-US/firefox/addon/copy-as-markdown/">Firefox</a>) for many years. It was started when I was blogging on Jekyll, a Markdown-based blogging system. Recently I rewrote that extension to support the upcoming Firefox 57, which <a href="https://support.mozilla.org/en-US/kb/firefox-add-technology-modernizing">deprecates the add-on SDK</a> I used to build my extension on.</p>
<p>The new SDK on Firefox 57 implements <a href="https://developer.mozilla.org/en-US/Add-ons/WebExtensions">WebExtensions</a>, a <a href="https://browserext.github.io/">W3C Standard</a> for web browser extensions. WebExtensions is mostly based on Chrome’s Extension API. I don’t know much about <a href="https://blog.mozilla.org/addons/2016/09/13/webextensions-and-parity-with-chrome/">the history</a> of the standard war, but the old SDK on Firefox, <a href="https://developer.mozilla.org/en-US/Add-ons/SDK">Jetpack</a>, in my opinion is easier to learn than Chrome’s.</p>
<p>Although I already have a Chrome version which is mostly based on the WebExtensions-like API, it doesn’t mean that the same source code works on Firefox. There are some pitfalls I encountered during the re-development. In fact, this project eventually grew into a practice on how to write once and build extensions for different browsers.</p>
<p>The full source code is available at <a href="https://github.com/yorkxin/copy-as-markdown">chitsaou/copy-as-markdown</a>. Please check the code if you’re interested, and feel free to give me any feedbacks.</p>
<h2 id="callback-vs-promise">Callback vs Promise</h2>
<p>If you check MDN’s <a href="https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Chrome_incompatibilities">WebExtensions Chrome compatibility</a> article, the first thing you’ll notice is the difference of function signatures: <strong>Chrome uses callback, and Firefox uses Promise</strong>.</p>
<p>For example, the <code>[chrome|browser].runtime.sendMessage</code> API, which sends message from popup window to the process the extension is running, has different usage between two browsers:</p>
<pre data-lang="js" class="language-js "><code class="language-js" data-lang="js">&#x2F;&#x2F; In Chrome
chrome.runtime.sendMessage(payload, (response) =&gt; {
  &#x2F;&#x2F; ...
})

&#x2F;&#x2F; In Firefox
browser.runtime.sendMessage(payload).then(response =&gt; {
  &#x2F;&#x2F; ...
})
</code></pre>
<p>Although Firefox accepts callback on functions that supports Promise, I still feel it easier to do things with Promise. But Chrome does not return Promise on those function calls. What’s worse, Chrome uses <code>chrome</code> as the global API entrypoint, while Firefox uses <code>browser</code> but supports <code>chrome</code>.</p>
<p>Fortunately Mozilla provides a polyfill library to do all the dirty jobs: <a href="https://github.com/mozilla/webextension-polyfill">mozilla/webextensions-polyfill</a>. As the name states, it makes Chrome to support <code>browser</code> object, and make all the functions calls return Promise.</p>
<p>Now I can just write <code>browser.*</code> and use the nice Promise-based APIs that Firefox recommends. Perfect!</p>
<p>By the way, if you submit an add-on with this polyfill library, Firefox Add-On developer dashboard will warn you about unnecessary library. But it can still be published without removing the library.</p>
<h2 id="clipboard-access">Clipboard Access</h2>
<p>Copy as Markdown helps you generate Markdown code for links and images, <em>and</em> copy it to the clipboard, so clipboard access is the most important part.</p>
<p>Unfortunately, if you want to do copy in the extension environment, for now there is no API allows you to do so with a single function call. The only way is this:</p>
<pre data-lang="js" class="language-js "><code class="language-js" data-lang="js">&#x2F;&#x2F; create a &lt;textarea&gt;
let textbox = document.createElement(&quot;textarea&quot;)
document.body.appendChild(textbox)

&#x2F;&#x2F; set content
textbox.value = &quot;text you want to copy&quot;

&#x2F;&#x2F; select all texts and execute &#x27;copy&#x27;
textbox.select()
document.execCommand(&#x27;Copy&#x27;)
</code></pre>
<p>Further more, in Firefox, you can only do this in content script or popup window. The background of this limitation is that, in order to create a <code>textarea</code>, it must be attached to a document. In WebExtensions, the core code of extension is running in a “Background Page”, a web page that get started when extension is loaded. If you open Task Manager in Chrome, you’ll see each extension running as a process, and they’re actually the Background Page of each extension.</p>
<p>There are many limitations on what you can do in Background Page, and some are browser-specific. In Firefox, one thing you can’t do is <code>document.execCommand()</code> , because the event must be dispatched from a user interaction, as the MDN article <a href="https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard"><em>Interact with the clipboard</em></a> says. So say you want to make an extension that shortens the current tab, the easiest way to do so is using a Browser Action Popup.</p>
<p>So how can we solve this? The answer is <a href="https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Content_scripts">Content Script</a>. Even you cannot copy text in Background Page, you can still do so if you inject a Content Script into the current page. The only downside of this solution is, you cannot inject Content Script into certain pages for security reason, such as Firefox Add-on website, and all the Firefox internal <code>about:*</code> pages. Therefore, Copy as Markdown for Firefox sometimes doesn’t work if your current tab is a Firefox-restricted page.</p>
<h3 id="permission">Permission</h3>
<p>In addition, to enable clipboard copy in Firefox, a permission is required in <code>manifest.json</code>:</p>
<pre data-lang="js" class="language-js "><code class="language-js" data-lang="js">&quot;permissions&quot;: [
  &quot;clipboardWrite&quot;,
],
</code></pre>
<p>This permission is required if you want to copy something in Firefox, but not required for Chrome — you can copy text in Chrome even if this permission is not specified — . In fact, when I (<a href="https://github.com/yorkxin/copy-as-markdown/issues/50">accidentally</a>) released the first beta for Chrome, some user reported that there are “extra permissions required”, because I added <code>clipboardWrite</code> in Chrome build. Although there is nothing more I requested for the new version, it still scared the users. So then I decided to remove it from <code>manifest.json</code> . After all we don’t need that.</p>
<p>Now in the code I can do such switch: (see full <a href="https://github.com/yorkxin/copy-as-markdown/blob/master/src/background/lib/clipboard-access.js#L24">source code here</a>)</p>
<pre data-lang="js" class="language-js "><code class="language-js" data-lang="js">import copyByBackground from &quot;..&#x2F;..&#x2F;lib&#x2F;clipboard.js&quot;

function copyByContentScriptWithTabWrapping(text, tab) {
  if (!tab) {
    return browser.tabs.getCurrent()
      .then(tab =&gt; copyByContentScript(text, tab))
  } else {
    return copyByContentScript(text, tab)
  }
}

let copyText = null;

if (ENVIRONMENT.CAN_COPY_IN_BACKGROUND) {
  copyText = copyByBackground
} else {
  copyText = copyByContentScriptWithTabWrapping
}
</code></pre>
<p>As you can see there is a <code>ENVIRONMENT.CAN_COPY_IN_BACKGROUND</code>. How do I feature-detect if the browser supports copy in Background Page or not? Well, I don’t. Instead I build separate targets and specify their behavior, as described in “Webpack” section below.</p>
<h2 id="native-popup-style">Native Popup Style</h2>
<p>In Firefox Jetpack, there was no popup window that you can display when user clicks a button on toolbar. — Well, at least there was no such thing when I was building my first Jetpack add-on. — But in WebExtensions framework, there is a popup, and there <em>is</em> a CSS framework for you to make it look native. All you have to do is enable it in <code>manifest.json</code> :</p>
<pre data-lang="js" class="language-js "><code class="language-js" data-lang="js">&quot;browser_action&quot;: {
  &quot;browser_style&quot;: true
}
</code></pre>
<p>The style it applies is basically the same as <a href="http://design.firefox.com/StyleGuide/">Firefox Style Guide</a>. I use samples from <a href="http://design.firefox.com/StyleGuide/#/navigation">Navigations</a> to make menu items in the popup look native.</p>
<figure>
  <img alt="" src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2017-09-25-things-i-learned-from-migrating-a-chrome-extension-to-firefox-using-webextensions/1*sQxWBcRdhD0GEIVwhqGpZQ.png" title="" />
  <figcaption>Copy as Markdown shows different popup styles on Chrome (left) and Firefox&nbsp;(right)</figcaption>
</figure>
<p>For Chrome, there is no such CSS framework you can use, but I’ve been using a custom style for a while. The first thing I have to do is to change some CSS selectors. But the hard part is how to load my custom CSS in Chrome. Fortunately, we can use a common practice from the web development world: load the CSS file anyways, namespace the CSS selectors, and append <code>body</code>-level class conditionally.</p>
<p>First, the CSS file is always loaded, no matter what browser it is. It’s loaded locally, so doesn’t cost network bandwidth. (Conditionally inserting <code>&lt;style&gt;</code> to load CSS dynamically does not re-draw the page, though.)</p>
<p>In CSS, namespace everything so that it won’t match until the top-level element has some class:</p>
<pre data-lang="css" class="language-css "><code class="language-css" data-lang="css">.custom-popup-style .panel-list-item {
  &#x2F;* ... *&#x2F;
}

.custom-popup-style .panel-list-item:hover {
  &#x2F;* ... *&#x2F;
}
</code></pre>
<p>In popup page’s JavaScript, change the <code>&lt;body&gt;</code>'s class according to a environment variable:</p>
<pre data-lang="js" class="language-js "><code class="language-js" data-lang="js">if (!ENVIRONMENT.SUPPORTS_POPUP_BROWSER_STYLE) {
  document.body.classList.add(&quot;custom-popup-style&quot;)
}
</code></pre>
<p>That’s all. Thanks to all the responsive web designers for this tip.</p>
<p>The <code>ENVIRONMENT.SUPPORTS_POPUP_BROWSER_STYLE</code> is also done by Webpack, which will be covered below.</p>
<h2 id="webpack-for-multiple-targets">Webpack for Multiple Targets</h2>
<p>As I mentioned in the previous sections, Copy as Markdown is now built by Webpack. Although you can develop an extension without Webpack, it still brings some benefits such as <strong>module loading</strong> and <strong>separating environments for browsers</strong>.</p>
<h3 id="module-loading">Module Loading</h3>
<p>Separating code by module makes it easier for maintenance. All you need to do is pack scripts into single files, such as <code>background.js</code> , <code>popup.js</code> and <code>content-script.js</code>, and reference them in <code>manifest.json</code>.</p>
<p>You may heard that ES6 module is available <a href="https://medium.com/dev-channel/es6-modules-in-chrome-canary-m60-ba588dfb8ab7">in the latest Chrome</a>, but unfortunately it is not available in Chrome Extension environment, because it requires HTTP server to return <code>application/javascript</code> MIME Type, but Chrome extension does not do so for JavaScript files (yet?).</p>
<p>But even if ES6 module is fully supported, there are still some other benefits we can get from Webpack.</p>
<h3 id="defining-environments"><strong>Defining Environments</strong></h3>
<p>As mentioned above, the extension needs to know whether the browser supports two features: copy in background page, and popup style.</p>
<p>At first I thought it makes more sense to do a real feature detection, like:</p>
<pre data-lang="js" class="language-js "><code class="language-js" data-lang="js">function canCopyInBackground() {
  try {
    &#x2F;&#x2F; ... setup textbox
    document.execCommand(&#x27;copy&#x27;)
    return true
  } catch {
    return false
  }
}
</code></pre>
<p>But it doesn’t work, because <code>document.execCommand</code> does not throw error when it fails. If I instead validate it by checking the clipboard content, that brings critical privacy issues.</p>
<p>Now because this extension is only shipped to 2 platforms: Firefox and Chrome, and I can lock supported browser versions during publishing, why not just build two packages for those two browsers?</p>
<p>With Webpack it’s easy to do so, just use <a href="https://webpack.js.org/plugins/define-plugin/">webpack.DefinePlugin</a> and load different environment files according to command line environment variable.</p>
<h3 id="separate-manifest-json">Separate <code>manifest.json</code></h3>
<p>Besides, we can also separate <code>manifest.json</code> for different browsers. As mentioned above, in <code>manifest.json</code> there are some different keys and values between two browsers. Some keys trigger warnings, some would even trigger permission alert to users. My solution is make two files <code>manifest.firefox.json</code> and <code>manifest.chrome.json</code> , and conditionally copy one of them to the target directory, with <a href="https://github.com/kevlened/copy-webpack-plugin">copy-webpack-plugin</a>.</p>
<p>Although it may make more sense to use shared <code>manifest.json</code> and modify the JavaScript object dynamically during build process, I didn’t find a good plugin doing this well, and I feel that maintaining two manifest files is a good solution for now. I’ll leave the issue there until there is a 4th browser to support.</p>
<p>One of the benefits of doing so is that, Chrome Web Store and Firefox Add-On have different rules on version numbers and names (described below). This fact makes it hard to use the same version number between Chrome version and Firefox version. Separating <code>manifest.json</code> makes it easier to set different version numbers that are compatible to different publishing platforms.</p>
<h2 id="publishing-on-stores">Publishing on Stores</h2>
<h3 id="firefox-keep-the-original-add-on-id">Firefox: Keep the Original Add-on ID</h3>
<p>Once you finished switching to WebExtensions, one thing you need to do is to keep the original Add-on ID from previous version. To find it, just visit the add-on dashboard. Then specify it in the new add-on’s manifest file:</p>
<pre data-lang="js" class="language-js "><code class="language-js" data-lang="js">&quot;applications&quot;: {
  &quot;gecko&quot;: {
    &quot;id&quot;: &quot;jid1-xxxxxxxxxxxxxxxx@jetpack&quot;
  }
}
</code></pre>
<p>Those <code>jid</code> and <code>jetpack</code> terms are from previous Add-on SDK Jetpack. You have to keep to same ID so that Firefox Add-on can recognize it as the same extension.</p>
<p>However, this key is invalid in Chrome, so, another reason to use two different manifest files.</p>
<h3 id="version-name-incompatibility">Version Name Incompatibility</h3>
<p>These issues are not directly related to framework switching, but a problem that exists for a long time. If you’re new to Chrome Web Store, then this section may be helpful.</p>
<p>On Chrome Web Store, all versions <em>must</em> contain only digits. For example <code>1.2.3</code> is valid, but <code>1.2.3rc1</code> is invalid. On Firefox Add-ons, they accept beta versions like <code>1.2.3rc1</code> or <code>1.2.3beta</code>, and it will recognize these as “Development Version”, so you can ask your friends to test it before release.</p>
<p>For Chrome, there is another <code>version_name</code> to display a different version name in Chrome’s Extensions page. While I am doing public testing, the version I used for Firefox Add-ons is <code>"version": "2.0.0rc1"</code> , but for Chrome I have to use <code>"version": "2.0.0", "version_name": "2.0.0rc1"</code> . Firefox does not allow <code>version_name</code> manifest key, though.</p>
<p>What’s worse, Chrome strictly check if newly uploaded extension is versioned greater than the previous one. Say you want to publish a beta version, you cannot publish a <code>2.0.0rc1</code> . Instead you can use <code>1.99.0</code> . If you accidentally published <code>2.0.0</code> as a development version, like me, the next version can only be <code>2.0.1</code> or “greater”.</p>
<p>That’s why my final release version becomes <code>2.1.0</code>. Even if I want to publish a “stable release of 2.0.0” I can never do so because I have already uploaded a <code>2.0.0</code> to Chrome Web Store.</p>
<h3 id="beta-testing">Beta Testing</h3>
<p>Finally, there is no “Beta Testing Channel” in Chrome Web Store. Well, there is, but it’s too complex for me to figure out how to setup a test user group.</p>
<p>On Firefox Add-ons, there is a Development Channel for beta testers. If the add-on contains version with <code>beta</code> or <code>rc</code> it will be recognized as development version, and published into Development Channel. This is a big plus to add-on developers.</p>
<p>In fact, I <a href="https://github.com/yorkxin/copy-as-markdown/issues/49">accidentally released</a> a broken version <code>2.0.0</code> on Chrome Web Store, which I thought it won’t be published due to misleading copies. There is no way to take down a version, and no way to “revert” to older version. All I could do is check out to older version in Git repository, <a href="https://github.com/yorkxin/copy-as-markdown/commit/3b11f60648d2022858adac55c1b96555dcd16372">increase version number</a> to <code>2.0.1</code> (due to strict restriction on incremented version number), set <code>version_name</code> to something like <code>reverted to older version</code>, zip it, and upload to Chrome Web Store. Sounds crazy, huh?</p>
<p>There are many things that Chrome Web Store could be improved. I just heard that they’re rebuilding the developer dashboard recently. Looking forward to see the new platform.</p>
<h2 id="conclusion">Conclusion</h2>
<p>WebExtensions is a utopia for browser extension developers. Write once, run everywhere. But different browser vendors may have different preferences on things like API design, features and security concerns. I tried to run my extension on Edge, unfortunately it failed to even get loaded.</p>
<p>This is another example of browser diversity, but it’s much harder to develop something on it, due to limitation of web API supports, and tools / libraries we can use. I believe that in the foreseeable future, browser vendors can accomplish the ideal goal, but for now, we still need to handle many browser-specific issues.</p>
<p>By the way, when will Safari support WebExtensions?</p>

            ]]></description>
        </item>
        <item>
            <title>With APFS, Latin with Accent and Japanese Kana are Finally Docker-Friendly</title>
            <link>https://blog.yorkxin.org/posts/apfs-docker-unicode/</link>
            <pubDate>Thu, 06 Apr 2017 19:41:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2017/04/07/apfs-docker-unicode.html</guid>
            
            <description><![CDATA[
                <p><strong>Apple File System</strong>, or APFS, has been a hot topic since last year. Mac users and developers are eagerly looking forward to its final release. And in fact, APFS also solves a problem that we have now on HFS+: the exchangeability problem of file names with the Accented Latin (like <em>é</em> as in <em>café</em>), and Japanese Kana.</p>
<h2 id="accented-latin-and-kana-file-names-finally-work-as-expected-in-apfs">Accented Latin and Kana File Names Finally Work as Expected in APFS</h2>
<p>To try these out, you can download a sample code from <a href="https://github.com/yorkxin/apfs-docker-unicode-poc">chitsaou/apfs-docker-unicode-poc</a>.</p>
<p>Consider you have the following files in your Docker project:</p>
<pre><code>$ cat café.txt
I like Moonbucks Coffee!

$ cat ありがとう.txt
ありがとう means Thank You!
</code></pre>
<p>And a simple Dockerfile:</p>
<pre><code>FROM alpine:latest
COPY . &#x2F;
</code></pre>
<p>Put these files under an HFS+ disk. Build the image and run it:</p>
<pre><code>$ docker build -t test-image .
$ docker run test-image cat café.txt
cat: can&#x27;t open &#x27;café.txt&#x27;: No such file or directory
$ docker run test-image cat ありがとう.txt
cat: can&#x27;t open &#x27;ありがとう.txt&#x27;: No such file or directory
</code></pre>
<p>The file will not be accessible if you type its name with the keyboard in your terminal or source code. (To type <code>é</code>, press <code>Opt-E</code> then <code>e</code>)</p>
<p>However, if you build the image from an APFS disk, it will work. First, create an APFS image and mount it:</p>
<pre><code>$ hdiutil mount foo.sparseimage -mountpoint &#x2F;Volumes&#x2F;apfs-image
$ cd &#x2F;Volumes&#x2F;apfs-imagehdiutil create -fs APFS -size 1GB foo.sparseimage

</code></pre>
<p>Copy the files above into it, and try again:</p>
<pre><code>&#x2F;Volumes&#x2F;apfs-image $ docker build -t test-image .
&#x2F;Volumes&#x2F;apfs-image $ docker run test-image cat café.txt
I like Moonbucks Coffee!
&#x2F;Volumes&#x2F;apfs-image $ docker run test-image cat ありがとう.txt
ありがとう means Thank You!
</code></pre>
<p>It works! ✨</p>
<p>So if you have to handle file names with accented Latin characters, or even Japanese characters in Docker, you can simply wait until the APFS’s final release, or convert to ascii-only representation like <code>cafe</code> or Romaji. You can also avoid HFS+ by building the images on a Linux-based CI server, instead of macOS.</p>
<p>The rest of this post will briefly describe the reason why it doesn’t work on HFS+, with some Unicode knowledges.</p>
<h2 id="problem-of-file-name-with-combined-chars-in-hfs">Problem of File Name with Combined Chars in HFS+</h2>
<p>Say that we have a file named <code>café.txt</code>, and we know each char’s Unicode code point as well as it’s UTF-8 code (note that <code>é</code> char is a two-byte <code>C3 A9</code> in UTF-8; <a href="http://ratfactor.com/utf-8-to-unicode">read this</a> if you want to know why):</p>
<pre><code>   Unicode     UTF-8
c   U+0063      0x63
a   U+0061      0x61
f   U+0066      0x66
é   U+00E9      0xC3A9
.   U+002E      0x2E
t   U+0074      0x74
x   U+0078      0x78
t   U+0074      0x74
</code></pre>
<p>Now let’s see how it’s represented in HFS+:</p>
<pre><code>$ touch café.txt
$ ls *.txt | od -t x1 -c
0000000    63  61  66  65  cc  81  2e  74  78  74  0a
           c   a   f   e    ́  **   .   t   x   t  \n
</code></pre>
<p>We can see that the 4th byte is <code>0x65</code> (<code>e</code>) followed by <code>0xCC 0x81</code>. What exactly is this <code>CC 81</code>? If we <a href="http://www.ltg.ed.ac.uk/~richard/utf-8.cgi?input=cc+81&amp;mode=bytes">reverse it from UTF-8</a> we’ll find that it’s <code>[U+0301](http://www.fileformat.info/info/unicode/char/0301/index.htm)</code> <a href="http://www.fileformat.info/info/unicode/char/0301/index.htm">Combining Acute Accent</a>.</p>
<p>On the other hand, in <a href="https://docs.docker.com/engine/userguide/storagedriver/aufs-driver/#images">Docker AUFS</a>:</p>
<pre><code>$ docker run -it alpine sh
# touch café.txt
# ls *.txt | od -t x1 -c
0000000   c   a   f 303 251   .   t   x   t  \n
         63  61  66 c3  a9   2e  74  78  74  0a
</code></pre>
<p>This time we get a <code>C3 A9</code> code sequence, which matches the UTF-8 code of <code>é</code> char.</p>
<p>And if we try it in APFS:</p>
<pre><code>&#x2F;Volumes&#x2F;apfs-image $ touch café.txt
&#x2F;Volumes&#x2F;apfs-image $ ls *.txt | od -t x1 -c
0000000    63  61  66  c3  a9  2e  74  78  74  0a
           c   a   f   é  **   .   t   x   t  \n
</code></pre>
<p>This time we also get a <code>C3 A9</code> sequence, the same as that in a Docker AUFS disk.</p>
<p>So why is there such difference?</p>
<h2 id="unicode-nfd-how-hfs-represents-e-and-ga">Unicode NFD: How HFS+ Represents é and が</h2>
<p>As shown above, the representation of <code>é</code> in HFS+ is <code>65 CC 81</code> rather than UTF-8’s <code>C3 A9</code> . This form is called <strong>Unicode Normal Form Canonical Decomposition, or NFD</strong>, while the <code>C3 A9</code> form is called Normal Form Canonical Composition, or NFC. With NFD, <code>é</code> will be represented as <code>e</code> followed by ◌́ (U+0301).</p>
<p>You can read more on Wikipedia’s “<a href="https://en.wikipedia.org/wiki/Unicode_equivalence#Normal_forms">Unicode equivalence</a>” article.</p>
<p>In HFS+, a file name will be encoded in Unicode NFD form. It seems that when Docker builds an image, the file names are copied as-is. So if we have a file named with characters that were converted to NFD on HFS+, and copy it to Docker, it will not be accessible via the most commonly used NFC form.</p>
<p>But it will work if the file is copied from an APFS disk. Since APFS does not convert file names into NFD, the file names are the same as what we have typed into the terminal or source code.</p>
<blockquote>
<p><strong>How does Apple File System handle filenames?</strong></p>
<p>APFS has case-sensitive and case-insensitive variants. The case-insensitive variant of APFS is normalization-preserving, but not normalization-sensitive. The case-sensitive variant of APFS is both normalization-preserving and normalization-sensitive. Filenames in APFS are encoded in UTF-8 and aren’t normalized.</p>
<p>HFS+, by comparison, is not normalization-preserving. Filenames in HFS+ are normalized according to Unicode 3.2 Normalization Form D, excluding substituting characters in the ranges <code>_U+2000_</code>–<code>_U+2FFF_</code>, <code>_U+F900_</code>–<code>_U+FAFF_</code>, and <code>_U+2F800_</code>–<code>_U+2FAFF_</code>.</p>
<p><em>Source:</em> <a href="https://developer.apple.com/library/content/documentation/FileManagement/Conceptual/APFS_Guide/FAQ/FAQ.html#//apple_ref/doc/uid/TP40016999-CH6-DontLinkElementID_3"><em>Apple File System FAQ</em></a></p>
</blockquote>
<h3 id="voiced-kana-daku-on">Voiced Kana (Daku-on)</h3>
<p>In fact, this NFD conversion also happens in Japanese.</p>
<p>There are 2 kinds of scripts in Japanese: <a href="https://en.wikipedia.org/wiki/Kana">Kana</a> (仮名) and Kanji (漢字). Kana, with around 100 characters, are mostly used for the Japan-origin phrases like ありがとう (thank you), or loan words like ハンバーガー (hamburger), while Kanji is a set of characters shared with the <a href="https://en.wikipedia.org/wiki/East_Asian_cultural_sphere">East-Asian culture sphere</a>.</p>
<p>Some Kana chars have two sounds: 清音 (sei-on, means voiceless) and 濁音 (daku-on, means voiced). For example, コ /ko/ is sei-on, and ゴ /go/ is daku-on. The way daku-on is represented in Japanese is by adding a double-dotted <a href="https://en.wikipedia.org/wiki/Dakuten_and_Handakuten">dakuten mark</a> at the right-top corner of a sei-on Kana. Such contract is similar to the <a href="https://en.wikipedia.org/wiki/Voice_%28phonetics%29">unvoiced and voiceless contrast</a> in English, for example “coat” /kot/ vs “goat” /got/, where /k/ sound is unvoiced, and /g/ sound is voiced.</p>
<p>In most cases, sei-on and daku-on are individual characters in Unicode, for example, <code>コ</code> is U+30B3, and <code>ゴ</code> is U+30B4. However, it can also be represented in NFD form, so <code>ゴ</code> becomes <code>コ</code> followed by ◌゙ (<a href="https://codepoints.net/U+3099">U+3099 Combining Katakana-Hiragana Voiced Sound Mark</a>). Now let’s go back to our original issue…</p>
<h3 id="voiced-kana-representation-in-hfs">Voiced Kana Representation in HFS+</h3>
<p>Say we have a file named <code>ありがとう.txt</code> . The third character が is the one with voiced sound mark. Their Unicode representations are as follows (Mind that chars other than <code>.txt</code> are all encoded as 3-byte sequences in UTF-8, so you can find it later in the byte sequence):</p>
<pre><code>    Unicode UTF-8
あ  U+3042  0xE38182
り  U+308A  0xE3828A
が  U+304C  0xE3818C
と  U+3068  0xE381A8
う  U+3046  0xE38186
.   U+002E      0x2E
t   U+0074      0x74
x   U+0078      0x78
t   U+0074      0x74
</code></pre>
<p>Now let’s inspect it in HFS+: (try this in <code>sh</code> rather than <code>bash</code>)</p>
<pre><code>$ touch ありがとう.txt
$ ls *.txt | od -t x1 -c
0000000    e3  81  82  e3  82  8a  e3  81  8b  e3  82  99  e3  81  a8  e3
          あ  **  **  り  **  **  か  **  **    ゙  **  **  と  **  **  う
0000020    81  86  2e  74  78  74  0a
          **  **   .   t   x   t  \n
</code></pre>
<p>You can see <code>が</code> is encoded as <code>E3 81 8B E3 82 99</code> , while <code>E3 81 8B</code> is U+304C <code>か</code>, and <code>E3 82 99</code> is U+3099 ◌゙ , the voice mark.</p>
<p>Let’s try again in <a href="https://docs.docker.com/engine/userguide/storagedriver/aufs-driver/#images">Docker AUFS</a>:</p>
<pre><code>$ docker run -it alpine sh
# touch ありがとう.txt
# ls *.txt | od -t x1 -c
0000000 343 201 202 343 202 212 343 201 214 343 201 250 343 201 206   .
        e3 81 82 e3 82 8a e3 81 8c e3 81 a8 e3 81 86 2e
0000020   t   x   t  \n
        74 78 74 0a
</code></pre>
<p>This time we get a <code>E3 81 8C</code> code sequence, which matches the UTF-8 code of <code>が</code> character.</p>
<p>And if we try it in APFS:</p>
<pre><code>&#x2F;Volumes&#x2F;apfs-image $ touch ありがとう.txt
&#x2F;Volumes&#x2F;apfs-image $ ls *.txt | od -t x1 -c
0000000    e3  81  82  e3  82  8a  e3  81  8c  e3  81  a8  e3  81  86  2e
          あ  **  **  り  **  **  が  **  **  と  **  **  う  **  **   .
0000020    74  78  74  0a
           t   x   t  \n
0000024
</code></pre>
<p>The same <code>E3 81 8C</code> sequence as in Docker AUFS.</p>
<p>Again, if we build a Docker image and copy the file from a HFS+ disk, it won’t be accessible, because when you type ありがとう it is usually in NFC form, not NFD. But if we do so from an APFS disk instead, it will work.</p>
<p>That is, the accented Latin problem happens during Docker build, will also happen in the Japanese file names.</p>
<h2 id="alternative-solution-convmv">Alternative Solution: <code>convmv</code></h2>
<p>There is another solution that you can use: converting file names from NFD to NFC during build process, but adding such workaround just for macOS (development machine) spends too much effort with no much return, and makes it harder to maintain. In my case, I eventually renamed all Japanese file names to ASCII-only names (<a href="https://en.wikipedia.org/wiki/Romanization_of_Japanese">Romaji</a>, romanization of Japanese). After all, Japanese people are familiar with Romaji.</p>
<p>You can take <a href="https://gist.github.com/JamesChevalier/8448512">https://gist.github.com/JamesChevalier/8448512</a> as an example. But as far as I tried, it doesn’t work well for the Japanese Kana.</p>
<h2 id="one-more-thing-atom-git-status">One More Thing: Atom Git Status</h2>
<p>I’ve been using Atom for more than 1 year. There was an issue annoyed me for some time, but has been resolved during my research of the Unicode file name thing.</p>
<p>Suppose you have a Git repository, and there is a file named <code>café.txt</code> .</p>
<p>In Atom’s tree view, it is always displayed as new (green), even if <code>git status</code> is clean.</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2017-04-07-apfs-docker-unicode/1*8Q7Exig8ndxwXsNKcosU5g.png" alt="" /></p>
<p>But if the repository exists in an APFS disk, it won’t be displayed as new after checked into the repository.</p>
<hr />
<h2 id="conclusion">Conclusion</h2>
<p>Understanding how texts are encoded in the computer system is always fun. I remember that when I was young, I learned that BIG-5 (encoding system used by Traditional Chinese) is double-byte, and there is a famous 許功蓋 (Hsu-Kong-Kai) problem, in which these characters uses <code>\</code> as the second byte, causing a string to be recognized as escape character like <code>\“</code> and crash a compiler or a database system. When I was studying in the college, I learned how UTF-8 encodes Unicode efficiently while keeping the compatibility with ASCII. They must be very smart to come up with such idea.</p>
<p>I studied this problem by Google and try-n-error, found no good explanation so I wrote this. If you found anything wrong or you’d like to add more, please point out in the comments!</p>
<h2 id="see-also">See also</h2>
<ul>
<li><a href="https://developer.apple.com/library/content/documentation/FileManagement/Conceptual/APFS_Guide/FAQ/FAQ.html#//apple_ref/doc/uid/TP40016999-CH6-DontLinkElementID_3">Apple File System — Frequently Asked Questions</a></li>
<li><a href="http://stackoverflow.com/questions/6153345/different-utf8-encoding-in-filenames-os-x">Different utf8 encoding in filenames os x</a></li>
<li><a href="http://www.taishukan.co.jp/kokugo/webkoku/series003_03.html">日本の文字とUnicode 第3回 | 大修館書店 WEB国語教室</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>用 rubyzip 壓內含中文檔名的 Zip 檔給 Windows</title>
            <link>https://blog.yorkxin.org/posts/rubyzip-encoding-windows/</link>
            <pubDate>Sun, 05 Feb 2017 17:45:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2017/02/06/rubyzip-encoding-windows.html</guid>
            
            <description><![CDATA[
                <p>再也不麻煩了，向煩死人的 Big5 告別吧。</p>
<p>以往為了應付 Windows 而必須先把檔案名稱轉換成 Big5 才能讓他們在 Windows 正常解壓縮。這種做法很可能會遇到 Unicode 轉換到 Big5 失敗的問題，例如游錫堃的「<a href="http://www.unicode.org/cgi-bin/GetUnihanData.pl?codepoint=%E5%A0%83">堃</a>」在 Big5 裡面就沒有，這時候就需要想很多 workaround。</p>
<p>但如果你面向的使用者很少，例如公司內部用的系統，解法會比較簡單。<a href="https://github.com/rubyzip/rubyzip">rubyzip</a> 不知到哪時候開始提供了一個可以讓新版的 Windows 直接認出 Unicode 檔名的方法：</p>
<p><a href="https://github.com/rubyzip/rubyzip/wiki/Files-with-non-ascii-filenames" title="https://github.com/rubyzip/rubyzip/wiki/Files-with-non-ascii-filenames"><strong>files with non ascii filenames — rubyzip/rubyzip</strong>
_rubyzip - Offical Rubyzip repository_github.com</a><a href="https://github.com/rubyzip/rubyzip/wiki/Files-with-non-ascii-filenames"></a></p>
<p>簡單來說，這樣設定就行了：（Rails 的話開一個 initializer 即可）</p>
<pre data-lang="rb" class="language-rb "><code class="language-rb" data-lang="rb">Zip.unicode_names = true
</code></pre>
<p>凡 Windows 8 以上的版本都沒問題。檔案總管內建的解壓縮工具就可以了。</p>
<p>至於如果你要支援 Windows 7 以下，或使用者不固定⋯⋯Good luck 😂</p>
<hr />
<p>同樣的問題本來我已經 6 年沒處理了，但最近來到日本，又要應付 Shift_JIS 的問題。看來 legacy encoding 真的是到哪裡都很煩的問題。</p>

            ]]></description>
        </item>
        <item>
            <title>諸君！把 Control 和 Caps Lock 交換吧</title>
            <link>https://blog.yorkxin.org/posts/ergonomic-control-key/</link>
            <pubDate>Mon, 26 Dec 2016 21:04:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2016/12/27/ergonomic-control-key.html</guid>
            
            <description><![CDATA[
                <p>先說結論：<strong>推薦各位程式設計師把 Control 設定到美式鍵盤 A 鍵的左邊</strong>。</p>
<figure>
  <img alt="" src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2016-12-27-ergonomic-control-key/1*iF5EEAbLrNkadpz50fbDqQ.jpeg" title="" />
  <figcaption>Control 在 A 左邊的 Realforce 鍵盤佈局</figcaption>
</figure>
<p>作為一名全職程序猿，為了讓自己維持效率和健康，無時無刻都在思考如何改善自己的工作環境跟工具。以往我在乎的是工具的好壞，好比說電腦一定就是 MacBook Pro 配 16GB RAM、鍵盤就是 <a href="http://www.topre.co.jp/products/elec/keyboards/">Realforce</a>、用 <a href="http://www.apple.com/magic-accessories/">Magic Trackpad</a> 而不用滑鼠、螢幕首選 Dell、有錢有空間的話就敗一台 <a href="http://www.hermanmiller.com/products/seating/performance-work-chairs/aeron-chairs.html">Aeron</a> 全功能椅，搭配 Bose <a href="https://www.bose.com/en_us/products/headphones/over_ear_headphones/quietcomfort-25-acoustic-noise-cancelling-headphones-samsung-and-android-devices.html">QC25</a> 抗噪耳機、<del><a href="http://fripside.net">fripSide</a> 全專輯</del>來讓我不會被外界干擾。</p>
<p>當我還在台灣有那些閒錢的時候，都會儘量自我滿足奢侈的工具慾。然而當人到了日本之後——或說已經決定不續留台灣之後——大部分的東西都成為累贅。大件的東西都在離開台灣之前處理掉了，真正帶來日本的只有 MacBook Pro（SpoonRocket 遺產）、Realforce 鍵盤和 QC25。</p>
<p>然而有一天我突然發現 Realforce 鍵盤有一個 switch 可以交換 Control 和 Caps Lock，也附了兩種尺寸的鍵帽，又同時發現我自己在輸入大寫字母的時候，幾乎都是按 Shift + 英文字母了。Caps Lock 對我來說就像 <a href="https://zh.wikipedia.org/wiki/%E8%8F%9C%E5%8D%95%E9%94%AE">Application Key</a> 那樣毫無用處。</p>
<p>再加上聽說過 Unix 鍵盤最早的時候 Control 是在 A 左邊的，作為一個每天使用 Terminal 的程序猿，<code>^C</code> <code>^K</code> 每天按，長年受到左手小指發麻的困擾。也聽說有不少人為了這件事所以把兩個鍵交換。於是我乾脆也試著把這兩個鍵給交換，結果的確<strong>大幅改善了左手小指發麻的症狀</strong>。</p>
<p>具體來說，把 Control 移到 A 左邊之後，這些習慣改變了：</p>
<ul>
<li>按 Control 不需要再大幅移動小指頭 → 最常按的 <code>^C</code> 可以很輕鬆就按到</li>
<li>更強迫自己用 Shift 去輸入大寫 → 發現幾乎只有文案的句首才需要大寫，程式碼的話直接打小寫靠自動補完 → 根本不需要 Caps Lock</li>
</ul>
<p>然後也開始學習<a href="https://support.apple.com/en-us/HT201236">新的快速鍵</a>：</p>
<ul>
<li>由於更懶得離開打字區，開始學習用 <code>^A</code> 和 <code>^E</code> 移動游標。不過這主要是基於 macOS 的 Home / End 是控制捲軸而不是移動游標的問題。</li>
<li>發現可以用 <code>^N</code> 和 <code>^P</code> 上下移動游標</li>
<li>為了將來適應 MacBook Pro with Touch Bar 開始學習 <code>^[</code> = Esc（誤）</li>
</ul>
<p>最後是開始覺得沒有方向鍵的 <a href="http://www.pfu.fujitsu.com/hhkeyboard/">HHKB</a> 似乎也可以習慣了 🤔</p>
<hr />
<p>至於單用 MacBook 或鍵盤沒有兩鍵交換功能的時候怎麼辦呢？</p>
<p>有在用外接鍵盤的大概都知道，macOS 有個功能是修改鍵盤 Modifier Keys 的對應，在那裡把 Control 跟 Caps Lock 對調就行了。這樣做的話，一樣可以達到保養左手小指的效果。</p>
<figure>
  <img alt="" src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2016-12-27-ergonomic-control-key/1*ajjOQnYHyb_fMGLsznbgWg.png" title="" />
  <figcaption>System Preferences &gt; Keyboard &gt; Modifier&nbsp;Keys…</figcaption>
</figure>
<p>用 Windows 的話，這個問題可能會更加困擾，因為 Windows 大部分的快速鍵都是 Control 按出來的。不過我自己沒在用 Windows，實際上不知道該怎麼處理。</p>
<hr />
<p>Read more:</p>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Caps_lock#Caps_Lock_versus_Control_key">Caps lock § Caps Lock versus Control key</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>Amazon S3 的全資料夾轉址</title>
            <link>https://blog.yorkxin.org/posts/s3-redirection-for-dir/</link>
            <pubDate>Mon, 16 Feb 2015 13:30:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2015/02/16/s3-redirection-for-dir.html</guid>
            
            <description><![CDATA[
                <p>最近剛把 blog 搬到了 Amazon S3，前面用 CloudFront 擋，速度變快了不少。</p>
<p>不過遇到了一個忘記處理的問題：原本的網站會自動把文章網址從 / 轉到 /posts/，但是我忘記處理了，導致 Google Webmaster Tool 裡面噴一堆 404。</p>
<p>既然是 S3 轉址，我就回去看了之前的<a href="http://blog.yorkxin.org/posts/2013/04/08/middleman-cdn-1-s3/">筆記</a>，從 Webmaster Tool 把 404 列表下載回來之後，拼湊成轉址表，結果 S3 告訴我最多只能有 50 條規則，行不通了。</p>
<p>不過爬了一下<a href="http://docs.aws.amazon.com/AmazonS3/latest/dev/HowDoIWebsiteConfiguration.html">文件</a>才發現還有一個轉址規則是  <code>ReplaceKeyPrefixWith</code>，簡單來說就是幫你 replace matched path prefix，官方的範例是你要把全站的網址從 <code>/docs</code> 搬到 <code>/documents</code> 那麼就是這樣設定：</p>
<pre data-lang="xml" class="language-xml "><code class="language-xml" data-lang="xml">  &lt;RoutingRules&gt;
    &lt;RoutingRule&gt;
    &lt;Condition&gt;
      &lt;KeyPrefixEquals&gt;docs&#x2F;&lt;&#x2F;KeyPrefixEquals&gt;
    &lt;&#x2F;Condition&gt;
    &lt;Redirect&gt;
      &lt;ReplaceKeyPrefixWith&gt;documents&#x2F;&lt;&#x2F;ReplaceKeyPrefixWith&gt;
    &lt;&#x2F;Redirect&gt;
    &lt;&#x2F;RoutingRule&gt;
  &lt;&#x2F;RoutingRules&gt;
</code></pre>
<p>如此就有 301 轉址了。</p>
<p>但是要注意的是這個轉址是在 S3 設定的，所以如果前面有掛自訂 domain name 的 CDN 例如 CloudFront，那麼就要另外加上 <code>HostName</code> 設定才會跑到正確的網址。</p>
<p>所以我的 blog 是這樣設定的：</p>
<pre data-lang="xml" class="language-xml "><code class="language-xml" data-lang="xml">&lt;?xml version=&quot;1.0&quot;?&gt;
&lt;RoutingRules&gt;
  &lt;RoutingRule&gt;
    &lt;Condition&gt;
      &lt;KeyPrefixEquals&gt;2007&#x2F;&lt;&#x2F;KeyPrefixEquals&gt;
    &lt;&#x2F;Condition&gt;
    &lt;Redirect&gt;
      &lt;HostName&gt;blog.yorkxin.org&lt;&#x2F;HostName&gt;
      &lt;ReplaceKeyPrefixWith&gt;posts&#x2F;2007&#x2F;&lt;&#x2F;ReplaceKeyPrefixWith&gt;
    &lt;&#x2F;Redirect&gt;
  &lt;&#x2F;RoutingRule&gt;
  &lt;RoutingRule&gt;
    &lt;Condition&gt;
      &lt;KeyPrefixEquals&gt;2008&#x2F;&lt;&#x2F;KeyPrefixEquals&gt;
    &lt;&#x2F;Condition&gt;
    &lt;Redirect&gt;
      &lt;HostName&gt;blog.yorkxin.org&lt;&#x2F;HostName&gt;
      &lt;ReplaceKeyPrefixWith&gt;posts&#x2F;2008&#x2F;&lt;&#x2F;ReplaceKeyPrefixWith&gt;
    &lt;&#x2F;Redirect&gt;
  &lt;&#x2F;RoutingRule&gt;
  &lt;RoutingRule&gt;
    &lt;Condition&gt;
      &lt;KeyPrefixEquals&gt;2009&#x2F;&lt;&#x2F;KeyPrefixEquals&gt;
    &lt;&#x2F;Condition&gt;
    &lt;Redirect&gt;
      &lt;HostName&gt;blog.yorkxin.org&lt;&#x2F;HostName&gt;
      &lt;ReplaceKeyPrefixWith&gt;posts&#x2F;2009&#x2F;&lt;&#x2F;ReplaceKeyPrefixWith&gt;
    &lt;&#x2F;Redirect&gt;
  &lt;&#x2F;RoutingRule&gt;
  &lt;RoutingRule&gt;
    &lt;Condition&gt;
      &lt;KeyPrefixEquals&gt;2010&#x2F;&lt;&#x2F;KeyPrefixEquals&gt;
    &lt;&#x2F;Condition&gt;
    &lt;Redirect&gt;
      &lt;HostName&gt;blog.yorkxin.org&lt;&#x2F;HostName&gt;
      &lt;ReplaceKeyPrefixWith&gt;posts&#x2F;2010&#x2F;&lt;&#x2F;ReplaceKeyPrefixWith&gt;
    &lt;&#x2F;Redirect&gt;
  &lt;&#x2F;RoutingRule&gt;
  &lt;RoutingRule&gt;
    &lt;Condition&gt;
      &lt;KeyPrefixEquals&gt;2011&#x2F;&lt;&#x2F;KeyPrefixEquals&gt;
    &lt;&#x2F;Condition&gt;
    &lt;Redirect&gt;
      &lt;HostName&gt;blog.yorkxin.org&lt;&#x2F;HostName&gt;
      &lt;ReplaceKeyPrefixWith&gt;posts&#x2F;2011&#x2F;&lt;&#x2F;ReplaceKeyPrefixWith&gt;
    &lt;&#x2F;Redirect&gt;
  &lt;&#x2F;RoutingRule&gt;
  &lt;RoutingRule&gt;
    &lt;Condition&gt;
      &lt;KeyPrefixEquals&gt;2012&#x2F;&lt;&#x2F;KeyPrefixEquals&gt;
    &lt;&#x2F;Condition&gt;
    &lt;Redirect&gt;
      &lt;HostName&gt;blog.yorkxin.org&lt;&#x2F;HostName&gt;
      &lt;ReplaceKeyPrefixWith&gt;posts&#x2F;2012&#x2F;&lt;&#x2F;ReplaceKeyPrefixWith&gt;
    &lt;&#x2F;Redirect&gt;
  &lt;&#x2F;RoutingRule&gt;
  &lt;RoutingRule&gt;
    &lt;Condition&gt;
      &lt;KeyPrefixEquals&gt;2013&#x2F;&lt;&#x2F;KeyPrefixEquals&gt;
    &lt;&#x2F;Condition&gt;
    &lt;Redirect&gt;
      &lt;HostName&gt;blog.yorkxin.org&lt;&#x2F;HostName&gt;
      &lt;ReplaceKeyPrefixWith&gt;posts&#x2F;2013&#x2F;&lt;&#x2F;ReplaceKeyPrefixWith&gt;
    &lt;&#x2F;Redirect&gt;
  &lt;&#x2F;RoutingRule&gt;
  &lt;RoutingRule&gt;
    &lt;Condition&gt;
      &lt;KeyPrefixEquals&gt;2014&#x2F;&lt;&#x2F;KeyPrefixEquals&gt;
    &lt;&#x2F;Condition&gt;
    &lt;Redirect&gt;
      &lt;HostName&gt;blog.yorkxin.org&lt;&#x2F;HostName&gt;
      &lt;ReplaceKeyPrefixWith&gt;posts&#x2F;2014&#x2F;&lt;&#x2F;ReplaceKeyPrefixWith&gt;
    &lt;&#x2F;Redirect&gt;
  &lt;&#x2F;RoutingRule&gt;
&lt;&#x2F;RoutingRules&gt;
</code></pre>
<p>btw 我有做一個搬家程式叫做 <a href="https://github.com/yorkxin/hikkoshi">hikkoshi</a>，可以處理泛 Jekyll 的 blog 搬家，以及 Ghost。如果你也有搬家的需求可以用用看。</p>
<hr />
<p>參考：</p>
<ul>
<li><a href="http://docs.aws.amazon.com/AmazonS3/latest/dev/HowDoIWebsiteConfiguration.html">Configure a Bucket for Website Hosting - Amazon Simple Storage Service</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>驗證在公司裡使用英文名字的迷思</title>
            <link>https://blog.yorkxin.org/posts/english-name/</link>
            <pubDate>Mon, 02 Feb 2015 15:29:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2015/02/02/english-name.html</guid>
            
            <description><![CDATA[
                <p>我一直很排斥用英文名字。現在加入了美國公司 SpoonRocket，終於有機會可以試試中文拼音名字了。</p>
<p>以下是半年來的效果：</p>
<ul>
<li>用 Yu-Cheng 美國人照樣叫，發音不一定會對，他們會用英語去發音，但是初次見面的時候會當面確認。雖然還是念成 <code>/you chain/</code>。英語裡面沒有「<a href="http://zh.wikipedia.org/zh-hant/%C3%9C">ü</a>」（ㄩ）的音，所以我也不太強求了。</li>
<li>美國人分不太清楚 cheng（ㄔㄥ）和 chen（ㄔㄣ），公司裡面有另一個 yu-chen，美國同事有時候會搞混，但是也可能是 auto complete 我在後面的緣故（？）</li>
<li>跟其他廠商聯絡的時候，會有人以為我的名字是 Yu Cheng Chuang，稱呼我的時候叫我 Yu。</li>
<li>基本上不會有人叫你的姓氏，處理 given name 就好了。</li>
</ul>
<p>現在公司裡面是外國人的只有我用拼音名字，美國那邊的人雖然是移民後代但是都有英文名字，或是母語名但是叫得出名字。這種困擾大概只有我有而已。</p>
<p>本來我還有個江湖名叫做 Ya Qi（鴨七），但是考慮到這個拼音要讀正確跟 Yu Cheng 大概也是一樣的難度（會讀作 <code>/yah khi/</code>）。當初開帳號的時候美國主管的看法是 "as long as US staff can pronounce it"，所以就不使用了。</p>
<p>結論是如果你在美國公司，很在乎人家唸對你的名字，就考慮換一個名字吧，但是不一定必須是美國人的名字，江湖名好唸也可以，好比說某台灣同事就用了一個日本風格的名字，大家也很開心。</p>
<p>至於在台灣的公司，之前大家都叫我鴨七，出去外面行走江湖（？）也都用這個名字，也是相安無事，同事們各自喜歡被怎麼叫就怎麼叫，原則上就是名從主人而已。說實話真的沒有非要英文名字不可。</p>
<p>不過聽說有些台灣公司會強迫取英文名字，我真的覺得走火入魔啊。</p>

            ]]></description>
        </item>
        <item>
            <title>日本動畫在台灣收視的困境（下）：動漫迷專屬的影音平台</title>
            <link>https://blog.yorkxin.org/posts/genuine-anime-in-taiwan-2/</link>
            <pubDate>Wed, 19 Nov 2014 13:24:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2014/11/19/genuine-anime-in-taiwan-2.html</guid>
            
            <description><![CDATA[
                <p><a href="http://blog.yorkxin.org/posts/2014/11/19/genuine-anime-in-taiwan-1">上一篇</a>說明了台灣現有可以看到正版日本動畫的平台，表面上的問題是這些：</p>
<ul>
<li>畫質與音質參差不齊</li>
<li>作品數量不足</li>
<li>無法正常以大螢幕觀看</li>
</ul>
<p>表面上，只要他們改善這些問題，便能吸引動漫迷，然而我仍然認為不夠。</p>
<p>在說明為什麼我有這種感覺之前，先說明動漫迷怎麼觀賞日本動畫的。</p>
<span id="continue-reading"></span>
<h2 id="dong-man-mi-de-zhui-fan">動漫迷的「追番」</h2>
<p><a href="http://zh.wikipedia.org/wiki/%E6%97%A5%E6%9C%AC%E5%8A%A8%E7%94%BB">電視動畫</a>（テレビアニメ）是日本重要的娛樂產業，與一般大眾認知的卡通不一樣，動漫迷所稱的動畫是指日本的深夜電視動畫，這種動畫的目標族群並非兒童，而是青少年乃至於成年人，主題廣泛，包括科幻、懸疑、奇幻冒險等等，當然也有輕鬆的後宮、戀愛喜劇等等。每一季（三個月）會有至少 30 部在日本的電視播放，其中賣座的大概也是十幾、二十部左右。不過因為作品數量多，短短 1 年，累積起來的「口耳相傳的佳作」就有 30 部以上，傳頌五年、十年的好作品也所在多有。</p>
<p>當然，這麼多作品不可能每個人都是每部看的，我將動漫迷看動畫的方式粗略分為三類：</p>
<ul>
<li><strong>重度追新番</strong> - 每季都追一堆新番，幾乎是日本播完隔天就看，這樣才能跟上朋友的話題。</li>
<li><strong>輕度追新番</strong> - 每季追新番，但只挑評價好的，等人家評測文出來才決定要看哪些。有的人會在一季播完之後再一次看完。</li>
<li><strong>補舊番</strong> - 往回看舊的作品，當然是評價好的才會去看。不要說《新世紀福音戰士》這麼久的，光是 2009 年的《科學超電磁砲》現在就還有人會去追。</li>
</ul>
<p>這三種觀賞的方式，分別需要三種不同的策略來滿足：</p>
<ul>
<li><strong>重度追新番</strong> - 上架要快，比盜版快就贏了。國外的正版平台甚至有一小時後上架的策略。</li>
<li><strong>輕度追新番</strong> - 作品要足，至少熱門一點的不要漏掉，上架也不要落後超過兩週。</li>
<li><strong>補舊番</strong> - 也是作品要多，熱門到四、五年的盡量留在架上，並且盡可能提供 HD。</li>
</ul>
<h2 id="dong-man-xu-yao-zhuan-shu-de-ping-tai">動漫需要專屬的平台</h2>
<p>綜觀目前的情況，多數動漫迷會在網路上找盜版，不論是新番還是舊番，原因有這些：</p>
<ul>
<li><strong>正版新番同步太慢</strong> - 正版可以慢到兩週，盜版大概一天之內就會出現翻譯版</li>
<li><strong>正版畫質太差</strong> - 盜版畫質其實也不算好，但目前還是有正版平台畫質低於盜版</li>
<li><strong>設備不支援</strong> - iPad 不是人人都有，MOD 不是家家可裝、能不能裝的控制權不見得在你手上</li>
</ul>
<p>也因此「用電腦上網抓盜版看動畫」甚至比上網點隨選視訊還好、還快、還方便。</p>
<p>一個好的動漫迷專屬平台，至少要有這些：</p>
<ul>
<li><strong>作品要多</strong> - 找不到就會去找盜版了。而且動漫迷真的會去爬五年前的舊作品來看。舊作上架後最好不要下架。</li>
<li><strong>同步要快</strong> - 比盜版快就贏了，盜版會差到一天，正版最好即時到一小時。</li>
<li><strong>畫質要好</strong> - 要有 HD，要比盜版好，不要糊，要可以用大螢幕觀看，而非小小的平板</li>
<li><strong>月租便宜</strong> - 一個月 $150 ~ $200 是國際趨勢，只要品質好，我甚至 300 都可以接受</li>
</ul>
<p>還有一些不應該發生的問題：</p>
<ul>
<li><strong>音質要正常</strong> - 128kbps 是一般人可以接受的底線；事實上我對於現在還有人壓低音質感到不可思議，要知道動漫迷之中可是有很多人對音質很講究的</li>
<li><strong>上架時間要久</strong> - 不要讓人有「怕它會下架」的恐懼</li>
</ul>
<p>以上的需求，在 2014 年的今天，台灣仍沒有一個網路影音平台可以符合的。</p>
<p>簡單來說，如果你問一個動漫迷，家裡沒裝 MOD 想看《進擊的巨人》可以去哪裡看？人家會推薦你的服務，而不是什麼電視頻道，或是盜版平台，那就成功了。而目前，台灣仍然沒有一個平台值得推薦的。</p>
<h3 id="yin-si-shi-zhong-hua-dian-xin-mod-de-zhi-ming-shang">「隱私」是中華電信 MOD 的致命傷</h3>
<p>前一篇文章我一直捧中華電信的 MOD，但其實這種機上盒服務，有一個很大的問題，就是「隱私」。</p>
<p>一般來說，MOD 是接在家庭的電視機，這種配置就無法滿足動漫迷。</p>
<p>要知道，電視機通常是擺在客廳的，闔家觀看的作品當然可以在客廳看，但近年來熱門的動畫都或多或少有色色的畫面，比較敏感的家庭會對於這種作品很感冒，所謂「需要恥力」就是在說這種情況。而電腦螢幕是私人佔有的，自然不會這種煩惱。這類作品可以說是近年來熱門動畫的大宗，一般戀愛喜劇（ラブコメ）都有這種情節。若要滿足動漫迷而不考慮這些因素，那是非常困難的。</p>
<hr />
<p>說了這麼多，世界上真的有這種夢幻平台嗎？答案是有的。</p>
<h2 id="crunchyroll-zhen-zheng-zhuan-wei-dong-man-da-zao-de-ping-tai">Crunchyroll - 真正專為動漫打造的平台</h2>
<p>在美國，<a href="http://www.crunchyroll.com/">Crunchyroll</a> 是最大的日本動畫平台，每一季都會進<a href="http://en.wikipedia.org/wiki/List_of_US_anime_simulcasts">大量的新番同步</a>，並且是在日本<strong>播出一小時後就上架</strong>，還附英文、西班牙文字幕。日本播出時是深夜，在美國西岸就已經是早上八、九點了，剛好可以搶先看。</p>
<p>它的 <strong>Player 簡單好用</strong>、也有 <strong>1080p 高畫質</strong>。Premium 會員收費則是<strong>每個月 $7</strong> 美金，折台幣大概 $210，怎麼算都很便宜。此外還有免費會員，是比 Premium 會員晚一週看到新番，並且會有廣告。</p>
<p>它有真正意義上的<a href="http://www.crunchyroll.com/devices">跨平台</a>，別說 Apple Device / Windows，就連 PS4 都有支援。</p>
<p>雖然大部份的作品在台灣無法打開，但還是有極少數是全球授權的。2014 秋季的話，就是<a href="http://www.crunchyroll.com/terraformars">《Terra Formars》</a>和<a href="http://www.crunchyroll.com/i-cant-understand-what-my-husband-is-saying">《旦那が何を言っているかわからない件》</a>。未登入會員可以試看 3 分鐘，試過一次你就知道什麼叫<strong>真正好用的網路影音平台</strong>。</p>
<p>不只是上架快，它還有「加入待看列表」、「評論」等功能，不只可以追番還可以吐槽，可以說是完全以動漫迷為中心來打造的平台。</p>
<p>並且，快速同步是真正可以有效打擊盜版的，根據 <a href="http://gigazine.net/news/20120322-anime-crunchyroll-taf2012/">2012 年在 Tokyo Anime Fair 的演講</a>，《火影忍者》新番同步使得<strong>盜版下載量減少 70%</strong>。</p>
<p>至於怎麼樣可以做到一小時同步的？根據一則 <a href="http://ascii.jp/elem/000/000/468/468874/">2009 年的專訪</a>，在 4 到 8 天前就會從版權主那邊拿到腳本及畫面，著手翻譯。影片本身則沒有提到什麼時候取得，但相信只要有了翻譯，要上字幕、轉檔上架也是很快的事，而這正是字幕組辦不到的事。</p>
<p>Crunchyroll 的共同創辦人 Kun Gao 曾經<a href="http://www.reddit.com/r/IAmA/comments/2b26ou/im_kun_gao_the_cofounder_and_ceo_of_crunchyroll/">在 Reddit 開過 AMA</a>，有興趣的話可以去上面爬爬看。</p>
<p>附帶一提，創辦人是 U.C. Berkeley 出身的，果然矽谷出好產品啊…</p>
<p>除了 Crunchyroll 之外還有 <strong><a href="http://www.funimation.com/">Funimation</a></strong>，也是強調新番同步，跟 Crunchyroll 的作品不太有重疊，強檔舊番也很多。這家好像還有兼 BD / DVD 代理進口。</p>
<p>但無論如何，這兩個網站都同時符合上述的所有「專為動漫迷打造的平台」的需求。</p>
<h2 id="daisuki-shen-mi-de-guan-fang-se-cai-de-ping-tai">DAISUKI：神秘的官方色彩的平台</h2>
<p><a href="http://daisuki.net/">DAISUKI</a> 這個平台很神奇，似乎是<a href="http://av.watch.impress.co.jp/docs/news/20130227_589623.html">日本的動畫發行商共同營運的網站</a>。作品很少，但一樣是當日同步，720p HD，附英文字幕。台灣看得到的，主要是台灣還沒代理的，如《Aldnoah/Zero》、《女神異聞錄 4 黃金版》、《M3〜ソノ黒キ鋼〜》（有中文字幕），但竟然有台灣已經有代理版權的《刀劍神域 II》，更早之前還有《魔法少女小圓》，但日前已公告因為授權到期而全站下架。</p>
<h2 id="zhong-guo-de-zheng-ban-dong-hua-ping-tai">中國的正版動畫平台</h2>
<p>一般人會以為中國的網路影音都是盜版的，但實際上，最近兩年來中國政府很積極在把盜版網站轉正，目前在中國大陸，實際上是可以看到正版新番同步的。有在同步新番的網站非常多，如樂視網、百度愛奇藝（合併 PPS）、PPTV、優酷土豆網、騰迅視頻、搜狐等，最近甚至連 bilibili 也買了 Fate/stay night (2014) 的版權來播。通常都是買獨播權，也因此每季都會有版權戰爭。對岸有個網站「<a href="http://acgdb.com/">次元碉堡 ACGDB</a>」專門在搜集這類的資訊。</p>
<p>但畫質我就沒辦法驗證了，因為台灣這裡看不到，而且就算翻牆過去，也會受限於網路速度而無法確認品質如何。</p>
<p>有趣的是，版權的來源，有些是跟台灣的代理商在中國大陸的分公司買的，多數的情況下，同一個作品，會同時有台灣、香港和中國大陸的版權代理，例如木棉花上海、曼迪香港（有中國大陸業務），授權的作品都跟台灣非常接近。先前中國發生過《中二病也要談戀愛！戀》在中國的<a href="http://acgdb.com/52cd06561d41c84c646dd560">搜狐視頻不慎偷跑提前播出</a>，使得代理商暫時被日本方面取消版權，進而影響到 2014 年初的台北漫畫博覽會，該作品的<a href="http://www.mightymedia.com.tw/news/bnews_detail.asp?bnewsid=1351">所有周邊商品都臨時停售</a>，便可以佐證版權代理是同時在兩岸三地之間運作的。</p>
<p>不過，雖然說有新番連載，但先前又傳出中國政府要限制境外節目都<a href="http://www.setn.com/news.aspx?PageGroupID=8&amp;NewsID=46939">必須先審後播</a>，新番從物理上就是不可能先審後播的，對於日本動畫在中國播出應該是會有一定的影響，但具體怎麼影響還要再看下去。</p>
<hr />
<h2 id="jie-lun-shut-up-and-take-my-money">結論：Shut up and take my money!</h2>
<p>「Shut up and take my money!」的意思是「錢拿去，我很滿意你的服務」，常用來稱讚一個服務很棒，使用者甘願付錢的情況。今天仍沒有一個真正可以讓我說出這句話的平台出現，我非常失望。</p>
<p>我認為最大的問題在於，<strong>目前營運的平台中，沒有一家是真正從動漫迷的需求出發的</strong>。MOD 以傳統電影租借的方式來提供娛樂戲劇的服務，是水火不容。KLand 以內容來說可以算是有，但卻往行動設備發展，這方向便是與傳統動漫迷的需求相違。myVideo 和 HiChannel 技術落後，有內容而沒有品質。<strong>台灣真正需要的是像 Crunchyroll 這樣的平台，而不只是用現有的平台來提供服務</strong>。</p>
<p>在音樂界我們已經有 <a href="http://www.kkbox.com">KKBOX</a>，一個月 $150 就可以無限暢聽，以 KKBOX 的說法就是<a href="http://www.inside.com.tw/2014/10/23/kkbox-interview">「自來水般的服務」</a>。對動漫迷來說，動畫的確也像是自來水一般，而目前在台灣卻仍沒有真正為動漫迷打造的平台。</p>
<p>Update: 完稿後，有朋友傳了一個巴哈姆特的問卷，是問<a href="http://user.gamer.com.tw/notice_detail.php?sn=495">若有動漫月租型服務，你願意付多少錢</a>。這麼說，又有新的平台要出現了嗎？非常期待。</p>
<hr />
<p>p.s. 我知道不只日本動畫在台灣有嚴重的盜版問題，美劇、日劇，甚至漫畫都有一樣的問題，不過我只看動畫，剩下的就歡迎大家發表。</p>

            ]]></description>
        </item>
        <item>
            <title>日本動畫在台灣收視的困境（上）：台灣現有的正版管道</title>
            <link>https://blog.yorkxin.org/posts/genuine-anime-in-taiwan-1/</link>
            <pubDate>Wed, 19 Nov 2014 13:21:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2014/11/19/genuine-anime-in-taiwan-1.html</guid>
            
            <description><![CDATA[
                <p>我是一個日本動漫迷（俗稱的宅），從兩年前開始，看動畫成為我一個很重要的娛樂，現在是下班回家就會看至少一個小時。</p>
<p>一般來說，動漫迷取得動畫的管道主要都是盜版網站，iTunes App Store 和 Google Play Store 上面也一大堆盜版平台，而且一直位居排行榜的前面。但我做為一個社會人士，既有工作（所以知道任何東西都有代價），又有經濟基礎，自然認同為娛樂付出金錢是理所當然的，因此，近一年來我一直在找尋正版動畫的管道。</p>
<p>而事實上，台灣也的確有正版管道，但我仍然感到不滿意，並且深刻感到在台灣要收視日本動畫的困難。以下，先介紹目前在台灣有的正版管道，接著提出為什麼不滿意，以及我認為應該怎麼解。</p>
<hr />
<span id="continue-reading"></span>
<p>台灣有進口正版動畫，主要的版權代理商是<a href="http://www.e-muse.com.tw">木棉花</a>、<a href="http://www.my-cartoon.com.tw">群英社</a>、<a href="http://www.mightymedia.com.tw">曼迪</a>、<a href="http://www.prowaremedia.com.tw">普威爾</a>、<a href="https://www.kadokawa.com.tw">台灣角川</a>（近年開始）。每一季都會引進約三到五部動畫，在動漫博覽會都可以看到他們的身影。</p>
<p>不過版權代理商辦得到的只有版權代理和翻譯，至於播放則要靠電腦技術，目前（2014 末）可以看到日本電視動畫的台灣本土平台有這些：</p>
<table><thead><tr><th>平台名稱</th><th>廠商</th><th>畫質</th><th>缺點</th></tr></thead><tbody>
<tr><td><a href="http://mod.cht.com.tw">中華電信 MOD</a></td><td>中華電信</td><td>HD（舊作品 SD）</td><td>上架期間短、綁機上盒</td></tr>
<tr><td><a href="http://www.kland.com.tw/">KLand 動畫</a></td><td>春水堂科技</td><td>無電腦版，無從比較</td><td>音質差，限平板、手機</td></tr>
<tr><td><a href="http://www.myvideo.net.tw/">myVideo</a></td><td>台灣大哥大</td><td>劣，低於 SD</td><td>畫質差，UI 爛</td></tr>
<tr><td><a href="http://hichannel.hinet.net">HiChannel</a></td><td>HiNet</td><td>低於 SD</td><td>畫質差，UI 爛</td></tr>
</tbody></table>
<p>MOD 是目前花錢可以買到最好的，但考慮到價錢的話，CP 值就不算高。以作品量來說，MOD 雖然上架期間短，但代理商卻是最齊全的。KLand 作品數量還算多，但只發展行動裝置，而且音質很差，目前還不支援電腦版，只有木棉花和群英社的作品。myVideo、HiChannel 作品數量比 KLand 多一些，但操作非常痛苦，號稱 HD 畫質卻只有 DVD 甚至更低。</p>
<p>以上四個平台都有<strong>動畫吃到飽</strong>的服務，都是最近一年才開始出現的，價錢差不多都在每月 $100 ~ $200 之間。</p>
<p>此外，從這四個平台可以觀察到，木棉花和群英社是最積極在網路平台上架的代理商，除了新番同步之外還會上 HD 畫質的舊番，尤其木棉花，在上述四個平台都有。曼迪則看起來很保守，以前曾經上架過「動漫集」（見文末），但目前只有在 MOD 上架，而且不見得有 HD；但也有自己的頻道播新番。曼迪、角川似乎都只在 MOD 上架，其他平台則沒有。普威爾則是還有在 myVideo 上架一些作品，目前看到的不是很多。</p>
<p>以下大概介紹一下各平台我用起來的心得。</p>
<hr />
<h2 id="zhong-hua-dian-xin-mod">中華電信 MOD</h2>
<p><a href="http://mod.cht.com.tw">中華電信 MOD</a> 最近開始推<a href="http://mod.cht.com.tw/comic/">動漫館 $199 吃到飽</a>，因為此我自己也訂了。以前我是沒考慮裝 MOD 的，因為很少在看一般的電視頻道，又以前 MOD 的隨選動畫是要隨選計費的，單點一次節目是綁 2 集或 3 集，要價 50 元，以一套動畫通常 12 集，有些大作 24 集，這種價格是非常昂貴的。所以動漫 $199 吃到飽對我來說誘因非常大，因此訂了。</p>
<h3 id="you-dian-gao-pin-zhi-pin-dao-ji-shi-tong-bu">優點：高品質、頻道即時同步</h3>
<p>MOD 隨選視訊的優點是：</p>
<ul>
<li><strong>1080i HD</strong>，畫質與音質普遍優於盜版。</li>
<li>有<strong>新番同步</strong>，不過不一定很即時，通常在一週或兩週內。</li>
<li>一個月<strong>只要 $199</strong>，綁某些專案甚至只要 $149，非常便宜。</li>
<li>翻譯品質不在話下，不會有看不懂的大陸用語。不過這應該是基本要求。</li>
</ul>
<p>新番若有同步上架，通常在一週到兩週後會上架，舊番的話，近三年的作品多數有 HD，舊一點的就只有 SD，好比說《科學超電磁砲》第一季 (2009) 就只有 SD，第二季《科學超電磁砲 S》 (2013) 就有 HD。不過雖然說是 SD，也是有直接看 DVD 的品質，沒有盜版那種一放大就糊得亂七八糟的問題，離螢幕遠一點倒是會有一種畫質比盜版好的錯覺，不知道為什麼突然就可以接受了。</p>
<p>除了隨選之外，還有三個代理商自己開設的頻道：</p>
<ul>
<li>木棉花的頻道「<a href="http://www.i-funtv.com">i-Fun 動漫台</a>」</li>
<li>群英社的頻道「<a href="http://www.my-cartoon.com.tw/my101/">My 101</a>」</li>
<li>曼迪的頻道「<a href="http://mod.cht.com.tw/channel/epginfo.php?chid=095">曼迪日本台</a>」</li>
</ul>
<p>播放的內容主要是該代理商新購入的日本同步作品，以及拿自家有版權的舊作品來播。同步的作品會有<strong>實時播放</strong>，例如《刀劍神域 II》、《Fate/stay night [UBW]》(2014) 就是與日本同日即時同步的。畫質方面跟隨選差不多，但音質就不一定了，曼迪的頻道會特別把人聲強化，使得整個聲音變得很詭異。</p>
<p>畫質的話，只要是 HD 就是非常完美，沒得挑剔，從來沒看過在盜版會出現的壓縮馬賽克現象。根據<del>我自己發明的</del>明體字幕檢查法，大部份都是 1080i 的 FullHD。</p>
<p>內容方面也很尊重原作。隨選視訊有些比較煽情的畫面也不打碼，在頻道播出時甚至會強調「一刀未剪」，也會為此調整播出時段，但還是有像《進擊的巨人》血腥畫面打霧的情況就是了。僅管如此，還是看得出來代理商很努力在尊重原作與滿足觀眾之間取得平衡。</p>
<h3 id="que-dian-ang-gui-bu-zheng-chang-de-shang-xia-jia-ji-zhi">缺點：昂貴、不正常的上下架機制</h3>
<p>但說到價錢，事實上除了頻道訂購費之外，還要再加上光世代專線，每個月要<strong>付出將近 $500 元</strong>。雖然以我的收入用來支付這筆錢並不會痛，但考慮到它的缺點，並沒有物超所值的感覺：</p>
<p><strong>上架期間很短，約半年會下架</strong>。給我一種「趕著把它看完」的焦慮感。我認為這是因為系統原本是設計給電影用的，遇到動漫這種「會去追舊作品」的觀眾反而無法適應。此外，聽說還有「因為中華電信的容量不足，所以代理商沒辦法上架」這種情況，總覺得很莫名其妙。</p>
<p><strong>舊番上架很慢</strong>。有的作品雖然已經是一兩年前的作品了，要上架到隨選的時候，還是會一週一集慢慢上。一般來說追舊番都是一口氣看完的，很少人會當新番在追的。</p>
<p><strong>新番同步很慢</strong>。即使有頻道會同步首播，還是大約一週至兩週後才會在隨選上架。以網路時代的速度，三天就算慢了，何況是一週。中國和美國的正版新番同步是當日同步的，日本播完後一、兩個小時就上架，</p>
<p><strong>品質良莠不齊</strong>。我在 MOD 平台看了四到五部動畫，包括頻道，部份舊動畫只有 SD，甚至有 2013 的動畫仍是 SD 的情況（這種現象以曼迪為主）。音質問題的話，目前遇到的就有爆音和異常的環境效果音（主要是木棉花和角川），或是特別 tune 過強化人聲導致聲音不對勁（曼迪的頻道）的問題。群英社和普威爾的隨選則是一向品質很好，而且字幕字體好讀，值得稱讚。</p>
<p>此外還有小問題，像是早期上架的影片通常都是一片綁 2 集或 3 集，如果你跟我一樣是習慣一天看一集的，遇到這種就很困擾了，因為租期是一日，若第一集看完了按暫停，隔天不在期滿之前再重新按播放，則觀看記錄會消失，在系統上會認定你重新租一次（不會另外加錢），要用快轉才能看到下一集。不過，最近上架的好像都偏向一片 1 集，所以這種問題會比較少。</p>
<p>又，因為 MOD 開始推「動漫館」、「戲劇館」、「電影館」這種吃到飽服務，變成如果你沒有訂吃到飽就沒辦法點進裡面的任何一個隨選，這其實是開倒車，因為 MOD 一開始就是隨選隨看、單集計價的，我如果只訂動漫館，即使我願意付單集的錢，也沒辦法看《新世紀福爾摩斯》，因為它被歸在戲劇館裡面。</p>
<h3 id="jie-lun-gan-za-qian-de-hua-mod-shi-mu-qian-hua-qian-ke-yi-mai-dao-zui-hao-de">結論：敢砸錢的話，MOD 是目前花錢可以買到最好的</h3>
<p>不得不說 MOD 雖然貴，但要攻頂的話就是這個了，沒得選。雖然下架聽起來很不爽，但其實也還蠻多作品的，考慮到一批作品下架之後還會再上另一批作品，也就沒什麼了，自己安排補舊番行程便是。若不知道要看什麼，也可以看頻道的節目。</p>
<hr />
<h2 id="kland-dong-hua">KLand 動畫</h2>
<p><a href="http://www.kland.com.tw/">KLand 動畫</a> 的營運公司是<a href="http://zh.wikipedia.org/wiki/%E6%98%A5%E6%B0%B4%E5%A0%82%E7%A7%91%E6%8A%80">春水堂科技</a>，若你的網齡有 10 年以上，應該會記得 2000 年左右曾經紅極一時的台灣原創卡通「阿貴」，沒錯，就是這家公司。我不知道為什麼這家公司開始做日本動畫平台，但總之就是有這麼一個平台。</p>
<h3 id="you-dian-zong-zhi-hen-bian-yi-xing-dong-zhuang-zhi-zhuan-yong-you-xin-fan-tong-bu">優點：總之很便宜，行動裝置專用，有新番同步</h3>
<p>基本上就是用手機或平板打開，然後裡面有一堆動畫，你可以點開來看，30 天只要 $89 元，非常非常便宜。它有一些是第一話免費的，不必加入會員就能看，可以試試。</p>
<p>除了有新番同步之外，也有舊番可以追，但目前是以木棉花和群英社為主。同步的速度的話，因為我不是每週追的那種人，所以不確定有多快，但以 2014 新番同步的節目來看，應該是一週左右。</p>
<h3 id="que-dian-zuo-pin-liang-shou-xian-pin-zhi-chai">缺點：作品量受限、品質差</h3>
<p>但考慮到它的缺點，我不認為它有物超所值：</p>
<p><strong>音質很差</strong>。直接用手機或平板的內建喇叭聽不太出來，但是一接上耳機就可以聽出它的音質奇差無比，就是壓縮過頭的那種。這是我不能忍受的一點。反應了好幾次都還是這樣子。</p>
<p><strong>沒有電腦版</strong>。所以想用大螢幕爽爽看也沒辦法。此外，iOS 版本據說是可以串流到 Apple TV 的，但我手邊沒有 Apple TV 所以也沒得試。</p>
<p><strong>只有木棉花和群英社</strong>。這大概只能覆蓋 40% ~ 50% 的作品，剩下的在上面則沒有。</p>
<p>其他的小問題像是 Login 方式不安全（要你輸入 Facebook 帳密，這在我這種 Web 技術人員的眼裡是很匪夷所思的）、選第幾話的 UI 很難用之類的。</p>
<h3 id="jie-lun-hen-bian-yi-dan-jiu-shi-bian-yi-jia-qian-de-na-ge-deng-ji-mei-ban-fa-tui-jian">結論：很便宜，但就是便宜價錢的那個等級，沒辦法推薦</h3>
<p>適合在手機或平板上看，而且如果你不在意音質，只用手機或平板的內建喇叭，那麼很適合你。</p>
<p>但對我這種就是要大螢幕爽爽看的人，是沒辦法接受的。</p>
<hr />
<h2 id="myvideo">myVideo</h2>
<p><a href="http://www.myvideo.net.tw/">myVideo</a> 是<a href="https://www.taiwanmobile.com/">台灣大哥大</a>推出的影音平台，看起來是要跟 HiChannel 競爭的，但操作界面也就只有 HiChannel 那種檔次而已。</p>
<h3 id="you-dian-bu-duan-chong-shi-zuo-pin-jia-ge-bian-yi">優點：不斷充實作品，價格便宜</h3>
<p>myVideo 也有推月租制，<a href="http://www.myvideo.net.tw/TWM_Video/Portal/servlet_main.jsp?classID=TWM01138123&amp;menuType=StoreService&amp;menuId=cartoon">一個月 $150</a>，現在推廣價是 $99，還蠻便宜的。<a href="http://www.myvideo.net.tw/TWM_Video/Portal/servlet_main.jsp?isGroup=Y&amp;classID=New_Animation&amp;menuType=HotGroup&amp;menuId=cartoon">新番跟播</a>的作品還算多，當然這是以國內有代理的為準，至少 2014 秋番《Selector spread WIXOSS》在中華電信 MOD 隨選是沒有的（MOD 的木棉花頻道有當週同步，KLand 也有上架）。舊番的話，一眼望過去，大概就是木棉花為主，有一些普威爾的，還沒有看到群英社的。</p>
<h3 id="que-dian-pin-zhi-di-lie-ui-nan-yong">缺點：品質低劣、UI 難用</h3>
<p>聽起來不錯，但為什麼不推薦呢？問題主要都是在技術問題。</p>
<p><strong>電腦版的撥放器很爛</strong>。為了 DRM，使用了 Silverlight，效能很差。不是說 Sliverlight 效能就不好，而是它的播放器要開始播必須等超過 30 秒，整個瀏覽器就停在那邊，就為了那個 DRM 授權。</p>
<p><strong>畫質不穩</strong>。它的情況是這樣：一打開會先是很糊，然後有時候清晰，有時候又糊，有時候又中斷進入 buffering mode。不像 YouTube，你無法要求它固定在 HD，它會自動調整，但顯然自動調整很笨，用起來很不爽。我不知道是不是因為我用 HiNet 光世代所以活該連去 myVideo 很慢，但這種體驗顯然是不及格的。也就是說，即使影片標示 HD，我也從來沒有看過 HD 畫質。</p>
<p><strong>嚴格限制授權的設備</strong>。我不懂為什麼要限一台電腦、一台平板、一支手機，然後一台要綁 1 個月。看起來是一種把君子當小人的心態。都還沒有人要偷你的東西就擔心別人偷你的東西。</p>
<h3 id="jie-lun-cp-zhi-de-p-tai-di-bu-yu-tui-jian">結論：CP 值的 P 太低，不予推薦</h3>
<p>UI 太醜、Player 太爛我都還可以忍，但我最重視的「品質」太低，因此，就算是 $150 這種價格，這種低品質的產品，我依然無法接受。</p>
<hr />
<h2 id="hichannel">HiChannel</h2>
<p><a href="http://hichannel.hinet.net">HiChannel</a> 這個名字從我小時候就聽到過，算是還有點歷史（？）的平台。</p>
<h3 id="you-dian-you-xin-fan-gen-bo">優點：有新番跟播</h3>
<p>老歸老，商業模式是有在進化，除了新番上架之外，最近還有推動漫吃到飽，但可惜是各代理商自己的吃到飽，所以如果你想全看，就得要各自買。現在到底多少錢我也不知道，我已經找不到廣告頁了。</p>
<h3 id="que-dian-zong-zhi-jiu-shi-gui-hua-zhi-hen-chai-cao-zuo-hen-tong-ku">缺點：總之就是貴，畫質很差，操作很痛苦</h3>
<p>說它是老牌，它更像是「不思長進」的平台。整個系統的設計就是停留在我小時候的印象，完全沒有長進。</p>
<p>這個平台跟上述其他三個不同的是有單點計價，但很貴，一集 $25 或 $30 元，也可以買一套的，之前有看到一季 $200 元的。 不過這種方式，對於「只是想要看一下《魔法少女小圓》來稍微跟一下流行」的人來說算是還可以的價格。</p>
<p>至於畫質，雖然標示著 HD 但事實上<strong>畫質只有 DVD 等級</strong>。我本來以為是因為我用 Mac 才有這種問題，但我也在 Windows 確認過了，就是 DVD 等級。</p>
<p>UI 方面，Web 界面就是那種舊時代的土味，播放器的 Sliverlight 在 Mac 必須另外開外掛才能使用，不過 buffering 比起 myVideo 來說快太多了。</p>
<h3 id="jie-lun-lao-pai-bu-si-zhi-shi-zhu-zu-bu-qian">結論：老牌不死，只是駐足不前</h3>
<p>我不知道是前公營事業都有這種情況還是怎麼樣，總之雖然商業模式一直在進步，內容也有在充實，但整個網站用起來就是土味很重，很難用，看起來也沒有想要改進。一個扶不起的平台，要我怎麼推薦。</p>
<hr />
<h2 id="yi-xiao-shi-de-ping-tai">已消失的平台</h2>
<p>除了上述現存的平台之外，以前還有一個原<a href="http://zh.wikipedia.org/wiki/%E5%A3%B9%E7%B6%B2%E6%A8%82">壹網樂</a>手機版變成的「NxTomo動畫館」，後改名為「動漫集」，似乎是把原壹網樂有授權的作品繼續在手機和平板上播，但最近該 App 把這些動畫全部下架了，或許是授權到期或商業政策變化也說不定。</p>
<p>不過它卻是開啟我動漫入門的啟蒙（？）者。2012 年底，網樂通收掉一兩個月後，我興致一來打開 iPad 的「動漫集」來看看，發現裡面有一些日本動畫，於是便開始看，做為娛樂戲劇來說我是很喜歡，而且因為是 iPad 所以畫質也不太在乎，不要讓我看到糊就好了。後來愈看愈多，便開始看盜版的 P＊S，那時候還沒有被中國政府整肅，所以其實我在上面看了不少盜版動畫。</p>
<p>此外還有曾經在 2013 年中在台灣實驗的「Docomo Anime Store」，原本是日本的手機看動畫服務，到了台灣變成在電腦上播放。上架的作品不算多，畫質大概是 720p HD 那個等級，不過四個月後就結束營業了，應該是來試水溫而已。</p>
<hr />
<h2 id="zong-jie-mei-you-ren-he-yi-ge-ping-tai-shi-man-yi-de">總結：沒有任何一個平台是滿意的</h2>
<p>講到這裡，你可以知道我在意的點是什麼了：</p>
<ul>
<li><strong>畫質與音質</strong> - 以上四個平台，除了中華電信 MOD 之外，都沒有畫質、音質可言，雖然 MOD 還是會遇到渣音質，但普遍 OK。</li>
<li><strong>作品數量</strong> - 上架雖然是在商言商，但有辦法網羅各家代理商的卻只有 MOD。以上架廣度而言，是木棉花 &gt; 群英社 &gt; 普威爾 &gt;&gt; 曼迪 = 角川，這樣的分佈。更別說 MOD 是有奇怪的上下架機制。</li>
<li><strong>大螢幕觀看</strong> - 日本電視動畫本來就是在電視上播放的，上述四平台，除了 MOD 之外，都沒辦法用大螢幕正常觀看。</li>
</ul>
<p>表面上看來，這些平台若改善了以上缺點，便有辦法滿足動漫迷，但事實上，除了這些表面上的原因，還有更多問題是讓「動漫迷」無法滿足的。詳細請接著看<a href="http://blog.yorkxin.org/posts/2014/11/19/genuine-anime-in-taiwan-2">下一篇</a>。</p>
<hr />
<p>p.s. 有一些不值得一提的平台：遠傳影城和台灣好 App。前者是看起來只是做做樣子消耗預算的，隨便上架一些然後就擺著，一如死水；後者是 App 很難用，我直接跳過了。</p>

            ]]></description>
        </item>
        <item>
            <title>228 期間的台中民軍與二七部隊</title>
            <link>https://blog.yorkxin.org/posts/228-taichung-civil-army/</link>
            <pubDate>Wed, 29 Oct 2014 16:59:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2014/10/30/228-taichung-civil-army.html</guid>
            
            <description><![CDATA[
                <p>前幾天無意間在 Wikipedia 翻到一個條目，連結的標題叫做「臺中人民政府」，歸類在「中華民國時期的其他地方政權」之下。這麼奇怪的標題吸引了我的目光。點進去轉到<a href="http://zh.wikipedia.org/wiki/%E4%BA%8C%E4%B8%83%E9%83%A8%E9%9A%8A">「二七部隊」</a>，裡面是這麼寫的：</p>
<blockquote>
<p>二七部隊，又稱台灣民主聯軍，乃台灣台中地區於二二八事件發生後，於1947年3月6日由謝雪紅、鍾逸人、蔡鐵城等多人所共同領導的反抗國民政府的武裝民兵（或稱民軍、人民軍）組織。二七部隊是二二八事件當時，規模最大、維持最久的反抗勢力組織，同時也是當時台灣民眾口耳相傳關注的焦點。</p>
</blockquote>
<p>維基百科也引用了<a href="http://museum.228.org.tw/largeeventinfo.aspx?v=2AE0DDB4E4B40054">二二八紀念館的經緯</a>，不過跟維基百科的有一點微妙的不同，兩相比較會有很明顯的感覺，所以以下主要是從二二八紀念館的紀事整理出來的。</p>
<hr />
<span id="continue-reading"></span>
<p>2 月 28 日，前一天的查菸事件傳到台中之後，跟其他都市一樣，台中的民眾也聚集到警察局抗議。至此還不成組織，直到 3 月 2 日，推舉了謝雪紅做為主席舉辦市民大會，一番議論後，早上十時即出門遊行示威。在下午組成了一個叫做「台中地區時局處理委員會」的組織，並組織治安隊，但因為聽說陳儀要派兵打台中，所以治安隊就解散了。</p>
<p>3 月 3 日，謝雪紅將前一天解散的治安隊再度擴大，並在市參議會成立「台中地區治安委員會作戰本部」，這便是俗稱「民軍」的「人民大隊」。至 3 月 4 日，便佔領了台中市多數政府機關以及水湳的空軍飛機工廠（飛機工廠是談判到手的）。該日下午，<quote>「台中地區士紳及人民團體代表五百多名，齊集市政府禮堂，重新組織台中地區時局處理委員會」</quote>，而作戰則交由「保安委員會」來負責。惟謝雪紅不同意此決定（不願交出軍權），兩邊的意見愈來愈懸殊，謝雪紅遂在 3 月 6 日成立「二七部隊」，謝雪紅自任總指揮，由四百餘人青年、學生，以及在地的八個部隊組成，其成員甚至還有前台籍日軍。正式成軍則是在 3 月 7 日。「二七」一名是取自 2 月 27 日查菸事件。</p>
<blockquote>
<p>保安委員會除設置副官團與參謀團外，還置有情報、通信、軍需、兵器、、等各部。處委會並推選吳振武擔任民軍總指揮，從而軍權乃由謝雪紅移轉至吳振武手中。惟謝雪紅不服此決定，拒絕將作戰本部的民軍編入保安委員會。保安委員會成立後，吳振武即在台中師範學校重新編組部隊，停止供應武器給中南部民軍，於是台中地區同時出現兩個步調不一的武裝系統。但保安委員會雖另組部隊，收集不少武器，但因機構過大，意見分歧，難以決定具體行動。惟謝雪紅的作戰本部仍然活躍，對各地的軍火供應亦未間斷。</p>
<p>── <em>（引用自<a href="http://museum.228.org.tw/largeeventinfo.aspx?v=2AE0DDB4E4B40054">二二八紀念館網站</a>）</em></p>
</blockquote>
<p>是人所以會有意見分歧，「時局處理委員會」也分成保守與激進派，意見分歧，莫衷一是。3 月 8 日，一直有傳聞國民黨會派援軍，人心惶惶，「時局處理委員會」許多委員也紛紛辭職。這時候只剩下二七部隊還在作戰，根據維基百科，甚至去支援嘉義和南投的反抗軍。</p>
<p>3 月 9 日陳儀解散二二八事件處理委員會，以軍隊在台北肆意鎮壓。消息傳到台中，人心惶恐，終於在 3 月 11 日崩潰了，委員會開始燒燬文件，下午宣佈解散「台中地區時局處理委員會」，重新推舉原台中市長黃克立復職。台中市的一支民軍就此消滅。到了 3 月 12 日，甚至有<q>「林獻堂、黃朝清等士紳，則在市區沿街勸募，準備製作彩坊歡迎國軍。」</q>（嗯…）</p>
<p>而二七部隊考量到不要讓戰鬥波及市民，在 3 月 12 日便撤守南投埔里，並改稱「台灣民主聯軍」。半路上還派一支學生部隊去搶軍倉庫的武器和糧食，囤在埔里國民學校。之後 3 月 13 日國民黨軍隊掃盪台中，但沒有遇到二七部隊，是直到 3 月 14 日才開始在埔里發生戰鬥，稱為「埔里之戰」。戰鬥的過程可以看二二八網站的「埔里之戰」一節，或「<a href="http://zh.wikipedia.org/wiki/%E7%83%8F%E7%89%9B%E6%AC%84%E4%B9%8B%E5%BD%B9">烏牛欄之役</a>」這則條目。總之雖然二七部隊一開始取得勝利，但最後還是敗在經驗不及正規軍隊、又缺乏彈藥，乃在 3 月 16 日深夜「埋藏武器後解散」。3 月 17 日，國民黨軍隊得知民軍解散後，便進入埔里。</p>
<hr />
<p>這是二二八期間的一段勇猛但充滿血淚的歷史，這些反抗軍之後是什麼下場呢？雖然二二八紀念館的網站上沒有提到，但還是可以想像。有一些在<a href="http://www.wufi.org.tw/%E4%BA%8C%E4%BA%8C%E5%85%AB%E4%BA%8B%E4%BB%B6%E7%9A%84%E6%9C%80%E5%BE%8C%E4%B8%80%E6%88%B0%E2%94%80%E7%83%8F%E7%89%9B%E6%AC%84%E6%88%B0%E5%BD%B9/">二二八事件的最後一戰─烏牛欄戰役</a>這個網站有提到。</p>
<p>看完之後，心裡的感覺是台中人好勇猛，民主果然還是鮮血換來的。今日的台灣頂多有太陽花學運這樣的和平抗爭，若是真的遇到當年那樣引起眾怒的事件，台灣人還站得出來嗎？</p>
<hr />
<p>不過謝雪紅何人呢？維基百科<a href="http://zh.wikipedia.org/wiki/%E8%AC%9D%E9%9B%AA%E7%B4%85">謝雪紅</a>的條目裡面也有寫到：</p>
<blockquote>
<p>謝雪紅，原名謝阿女（1901年10月17日－1970年1月15日），台灣彰化人，日治時期的<a href="http://zh.wikipedia.org/wiki/%E5%8F%B0%E7%81%A3%E5%85%B1%E7%94%A2%E9%BB%A8">台灣共產黨</a>（日本共產黨臺灣民族支部）創始黨員之一、中國共產黨黨員、台灣民主自治同盟首任主席，是中華人民共和國八大民主黨派的參政政治人物之一。謝雪紅後來陸續出任中共中央華東局軍政委員、中華全國婦女聯合會副主席，1954年當選全國人大台灣省代表。謝雪紅於1970年被迫害致死，病逝北京。</p>
<p>她被稱為是台灣社會主義革命先驅，因而被譽為台灣第一位女革命家。也是二二八事件中堅持對國民政府採取武力抵抗之台中二七部隊的參與組織者。二七部隊抵抗國民黨軍隊失敗之後，轉赴廈門，後赴香港。</p>
</blockquote>
<p>也就是說這個人有共產黨的背景。</p>
<p>看到這裡，再加上謝雪紅一心想要領導自己的軍隊，我又覺得，要是真的給她成功的話，說不定中共就直接來台灣了。</p>
<p>不過，也不能說這支二七部隊就是中共的同路軍，我想應該是在當時社會氛圍之下，受到鼓動而出來武裝反抗，而剛好有共產黨背景的人士嗅到了這個良機，出來組織反抗軍，這樣而已。</p>
<p>此外還有另一則條目提到她：<a href="http://zh.wikipedia.org/wiki/%E5%8F%B0%E6%B9%BE%E6%B0%91%E4%B8%BB%E8%87%AA%E6%B2%BB%E5%90%8C%E7%9B%9F">台灣民主自治同盟</a>，這是謝雪紅在台灣反抗失敗、逃到香港之後成立的，之後在 1955 又跑到了北京。此後一直是以一個「中華人民共和國的政黨」活動著，其黨章（？）就是以統一台灣為目的的。</p>
<p>不過謝雪紅在文革時期也免不了落到被批鬥致死的下場就是了。</p>
<hr />
<p>這段歷史在中學課本裡面隻字未提，至少這幾個名詞對我來說都是聞所未聞的。在 Google 上面找也大都是中國學者的研究，當然免不了帶一點政治傾向。</p>
<p>現任民進黨立委<a href="http://zh.wikipedia.org/wiki/%E6%9E%97%E4%BD%B3%E9%BE%8D">林佳龍</a>，曾在 2010 年 2 月 27 日在自由時報投過一篇社論<a href="http://news.ltn.com.tw/news/opinion/paper/375903">「二七部隊 台中精神」</a>來歌頌那時代的反抗軍中的一些重要人物。</p>
<blockquote>
<p>而當時已幾乎全盤控制台中市警察、營區、行政機關的二七部隊，為免台中市陷入戰火、生靈塗炭，於三月十二日毅然決定撤離台中，井然有序的轉進埔里。黃金島隊長並在烏牛欄（愛蘭橋附近），與國府軍隊展開激烈槍戰，以率領的三、四十位民兵，對抗七、八百位正規軍，雙方死傷慘重，直到彈盡援絕才棄守。</p>
</blockquote>
<p>以這則社論的調調來說的話，的確當時的人們只是想要反抗國民黨而已，倒不見得是存心要以社會主義統治台灣。然而根據二二八紀念館的事紀，要說起當年民軍的故事，二七部隊應該只是其中的一部份，而不是主角。</p>
<p>不過林佳龍的社論裡也提到一套書叫做<a href="http://www.books.com.tw/products/0010455022">《辛酸六十年》</a>，共有三本，是由當年的部隊長鐘逸人在八十九高齡時所著的書。此外還有另一本書是由當時烏牛欄戰役的第一線指揮官黃金島口述的<a href="http://www.books.com.tw/products/0010279006">《二二八戰士黃金島的一生》</a>，我想我應該買來看看，畢竟我現在可以找到的資料實在太片段了，妄下結論不是什麼好事。</p>
<hr />
<p>p.s. 以上的論點是我翻過少數幾篇網路資料之後才做出來的個人心得，若是有對您或您的家人朋友有不敬之處，此非我本意，我向您致歉，也歡迎您在留言中指正我。我並非歷史研究專業，若有不盡之處，尚祈海涵。</p>
<hr />
<p>參考資料</p>
<ul>
<li><a href="http://zh.wikipedia.org/wiki/%E4%BA%8C%E4%BA%8C%E5%85%AB%E4%BA%8B%E4%BB%B6">二二八事件 - 維基百科，自由的百科全書</a></li>
<li><a href="http://zh.wikipedia.org/wiki/%E4%BA%8C%E4%B8%83%E9%83%A8%E9%9A%8A">二七部隊 - 維基百科，自由的百科全書</a></li>
<li><a href="http://museum.228.org.tw/largeeventinfo.aspx?v=2AE0DDB4E4B40054">二二八國家紀念館- 二二八大事記</a></li>
<li><a href="http://zh.wikipedia.org/wiki/%E7%83%8F%E7%89%9B%E6%AC%84%E4%B9%8B%E5%BD%B9">烏牛欄之役 - 維基百科，自由的百科全書</a></li>
<li><a href="http://zh.wikipedia.org/wiki/%E8%AC%9D%E9%9B%AA%E7%B4%85">謝雪紅 - 維基百科，自由的百科全書</a></li>
<li><a href="http://zh.wikipedia.org/wiki/%E5%8F%B0%E7%81%A3%E5%85%B1%E7%94%A2%E9%BB%A8">台灣共產黨 - 維基百科，自由的百科全書</a></li>
<li><a href="http://zh.wikipedia.org/wiki/%E5%8F%B0%E6%B9%BE%E6%B0%91%E4%B8%BB%E8%87%AA%E6%B2%BB%E5%90%8C%E7%9B%9F">台灣民主自治同盟 - 維基百科，自由的百科全書</a></li>
<li><a href="http://news.ltn.com.tw/news/opinion/paper/375903">〈自由廣場〉「二七部隊 台中精神」 - 言論 - 自由時報電子報</a></li>
<li><a href="http://www.wufi.org.tw/%E4%BA%8C%E4%BA%8C%E5%85%AB%E4%BA%8B%E4%BB%B6%E7%9A%84%E6%9C%80%E5%BE%8C%E4%B8%80%E6%88%B0%E2%94%80%E7%83%8F%E7%89%9B%E6%AC%84%E6%88%B0%E5%BD%B9/">二二八事件的最後一戰─烏牛欄戰役 | 台灣獨立建國聯盟</a></li>
<li><a href="http://blog.roodo.com/skydaughter/archives/2881719.html">豆腐魚聽自言自語:拜訪烏牛欄戰役第一線指揮官黃金島先生 - 樂多日誌</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>台灣日光節約時間之考據</title>
            <link>https://blog.yorkxin.org/posts/dst-in-taiwan-study/</link>
            <pubDate>Fri, 11 Jul 2014 09:06:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2014/07/11/dst-in-taiwan-study.html</guid>
            
            <description><![CDATA[
                <p>去年（2013）我寫過一篇文章是〈<a href="http://blog.yorkxin.org/posts/2013/08/26/time-zone-in-taiwan/">台灣時區變換的八卦</a>〉，當時寄信去 IANA TZ Database 的 mailing list 提 patch，一年後終於改了，原因是我給的 link 裡面有 non-ASCII 文字（非英文數字的文字），為了這個，所以 Time Zone Database 必須修改成 UTF-8-compatible 的格式（可以容納非英文數字的文字），所以才拖到現在。</p>
<p>因為最近收到回信，所以我又去研究了一下台灣的時區，這次重點是放在日光節約時間。</p>
<span id="continue-reading"></span>
<p>簡單來說，中央氣象局網站上大家<a href="http://www.cwb.gov.tw/V7/knowledge/astronomy/cdata/summert.htm">常引用的</a>，以下的這個日光節約時間的資訊，不是完全正確的。</p>
<table><thead><tr><th>年代</th><th>名稱</th><th>起訖日期</th></tr></thead><tbody>
<tr><td>民國34年至40年（西元1945-1951年）</td><td>夏令時間</td><td>5月1日至9月30日 <strong>← 這是錯的</strong></td></tr>
<tr><td>民國41年（西元1952年）</td><td>日光節約時間</td><td>3月1日至10月31日</td></tr>
<tr><td>民國42年至43年（西元1953-1954年）</td><td>日光節約時間</td><td>4月1日至10月31日</td></tr>
<tr><td>民國44年至45年（西元1955-1956年）</td><td>日光節約時間</td><td>4月1日至9月30日</td></tr>
<tr><td>民國46年至48年（西元1957-1959年）</td><td>夏令時間</td><td>4月1日至9月30日</td></tr>
<tr><td>民國49年至50年（西元1960-1961年）</td><td>夏令時間</td><td>6月1日至9月30日</td></tr>
<tr><td>民國51年至62年（西元1962-1973年）</td><td>　</td><td>停止夏令時間</td></tr>
<tr><td>民國63年至64年（西元1974-1975年）</td><td>日光節約時間</td><td>4月1日至9月30日</td></tr>
<tr><td>民國65年至67年（西元1976-1978年）</td><td>　</td><td>停止日光節約時</td></tr>
<tr><td>民國68年（西元1979年）</td><td>日光節約時間</td><td>7月1日至9月30日</td></tr>
<tr><td>民國69年起（西元1980年）</td><td>　</td><td>停止日光節約時</td></tr>
</tbody></table>
<p>我去翻了<a href="http://subtpg.tpg.gov.tw/og/q1.asp">臺灣省政府網際網路公報查詢系統</a>、<a href="http://gaz.ncl.edu.tw/">政府公報資訊網</a>，搜尋「夏令時」、「日光節約」、「夏季時」的公告，終於把每一年的日光節約時間的實施給湊出來了。</p>
<p>基本上都對，只有民國 34 到 36 年是錯的。</p>
<p>民國 34 年台灣光復，但 34 年實施是 5/1 至 9/30，台灣光復是 10/25，怎麼想都不可能由中華民國政府實施日光節約時間。不過其實有找到<a href="https://zh.wikisource.org/wiki/%E5%9C%8B%E9%98%B2%E6%9C%80%E9%AB%98%E5%A7%94%E5%93%A1%E6%9C%83%E8%A6%8F%E5%AE%9A%E5%85%A8%E5%9C%8B%E5%90%84%E5%9C%B0%E8%87%AA%E4%B8%89%E5%8D%81%E5%9B%9B%E5%B9%B4%E4%BA%94%E6%9C%88%E4%B8%80%E6%97%A5%E8%B5%B7%E8%87%B3%E4%B9%9D%E6%9C%88%E4%B8%89%E5%8D%81%E6%97%A5%E6%AD%A2%E5%B0%87%E6%99%82%E9%96%93%E6%8F%90%E5%89%8D%E4%B8%80%E5%B0%8F%E6%99%82%E4%BB%A4%E4%BB%B0%E9%81%B5%E7%85%A7%E5%B9%B6%E9%A3%AD%E5%B1%AC%E9%81%B5%E7%85%A7%E7%94%B1_%28%E6%B0%91%E5%9C%8B34%E5%B9%B4%E5%9C%8B%E6%B0%91%E6%94%BF%E5%BA%9C%E8%A8%93%E4%BB%A4%29">當年的電文</a>。此外也找到了<a href="https://ja.wikisource.org/wiki/%E8%87%BA%E7%81%A3%E3%83%8E%E6%A8%99%E6%BA%96%E6%99%82%E3%83%8B%E9%97%9C%E3%82%B9%E3%83%AB%E4%BB%B6_%28%E6%98%AD%E5%92%8C%E4%BA%8C%E5%8D%81%E5%B9%B4%E5%8F%B0%E6%B9%BE%E7%B7%8F%E7%9D%A3%E5%BA%9C%E5%91%8A%E7%A4%BA%E7%AC%AC%E4%B8%89%E7%99%BE%E5%85%AB%E5%8D%81%E5%85%AD%E5%8F%B7%29">台灣總督府的告示</a>，要在 1945 年 9 月 21 日改回西部標準時（UTC+8）。</p>
<p>民國 35 年實際上是從 5/15 到 9/30，<a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E7%9F%A5%E5%AF%A6%E8%A1%8C%E5%A4%8F%E5%AD%A3%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B35%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E8%A1%8C%E6%94%BF%E9%95%B7%E5%AE%98%E5%85%AC%E7%BD%B2%E4%BB%A3%E9%9B%BB%29">有公文可證</a>，甚至還有<a href="https://ja.wikisource.org/wiki/%E5%A4%8F%E5%AD%A3%E6%99%82%E9%96%93%E3%83%B2%E5%AF%A6%E8%A1%8C%E3%82%B9%E3%83%AB%E3%83%8B%E4%BB%98%E6%89%BF%E7%9F%A5%E7%9B%B8%E6%88%90%E5%BA%A6_%281946%E5%B9%B4%E5%8F%B0%E6%B9%BE%E7%9C%81%E8%A1%8C%E6%94%BF%E9%95%B7%E5%AE%98%E5%85%AC%E7%BD%B2%E4%BB%A3%E9%9B%BB%29">日譯文</a>。</p>
<p>民國 36 年實際上是從 4/15 到 10/31，而且原本打算<a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E8%A1%8C%E6%94%BF%E9%95%B7%E5%AE%98%E5%85%AC%E7%BD%B2%E6%89%80%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E7%82%BA%E5%A5%89%E4%BB%A4%E8%87%AA%E5%8D%85%E5%85%AD%E5%B9%B4%E5%9B%9B%E6%9C%88%E5%8D%81%E4%BA%94%E6%97%A5%E8%B5%B7%E5%AF%A6%E8%A1%8C%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B36%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E8%A1%8C%E6%94%BF%E9%95%B7%E5%AE%98%E5%85%AC%E7%BD%B2%E4%BB%A3%E9%9B%BB%29">實施到 9/30</a>，在 9/15 又<a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E5%BA%9C%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E6%9C%AC%E5%B9%B4%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E5%A5%89%E4%BB%A4%E5%BB%B6%E9%95%B7%E8%87%B3%E5%8D%85%E5%85%AD%E5%B9%B4%E5%8D%81%E6%9C%88%E5%8D%85%E4%B8%80%E6%97%A5%E5%8D%88%E5%A4%9C%E5%BB%BF%E5%9B%9B%E6%99%82%E6%AD%A2_%28%E6%B0%91%E5%9C%8B36%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A3%E9%9B%BB%29">決定延到 10/31</a>。這在之前的文章寫過了。</p>
<p>每一年的公文整理如下。我發現到網路上很多人直接 copy 氣象局的網站，畢竟是權威機關所以相信也是理所當然的，也不能怪大家不去求證。我已經去信請氣象局更改網頁了，希望可以早日修正才是。</p>
<p>Update: 下表我已經整理至 <a href="https://zh.wikipedia.org/wiki/%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93">Wikipedia: 夏令時間</a> 條目了，所以你如果看到類似的表格，不用懷疑，是我做的，不是我抄他，也不是他抄我。</p>
<table><thead><tr><th>民國</th><th>西元</th><th>始日</th><th>終日</th><th>回標準時</th><th>公告文件</th><th>註</th></tr></thead><tbody>
<tr><td>34</td><td>1945</td><td>5/1</td><td>9/30</td><td>10/1</td><td><a href="https://zh.wikisource.org/wiki/%E5%9C%8B%E9%98%B2%E6%9C%80%E9%AB%98%E5%A7%94%E5%93%A1%E6%9C%83%E8%A6%8F%E5%AE%9A%E5%85%A8%E5%9C%8B%E5%90%84%E5%9C%B0%E8%87%AA%E4%B8%89%E5%8D%81%E5%9B%9B%E5%B9%B4%E4%BA%94%E6%9C%88%E4%B8%80%E6%97%A5%E8%B5%B7%E8%87%B3%E4%B9%9D%E6%9C%88%E4%B8%89%E5%8D%81%E6%97%A5%E6%AD%A2%E5%B0%87%E6%99%82%E9%96%93%E6%8F%90%E5%89%8D%E4%B8%80%E5%B0%8F%E6%99%82%E4%BB%A4%E4%BB%B0%E9%81%B5%E7%85%A7%E5%B9%B6%E9%A3%AD%E5%B1%AC%E9%81%B5%E7%85%A7%E7%94%B1_%28%E6%B0%91%E5%9C%8B34%E5%B9%B4%E5%9C%8B%E6%B0%91%E6%94%BF%E5%BA%9C%E8%A8%93%E4%BB%A4%29">國民政府訓令</a></td><td>未在台灣實施</td></tr>
<tr><td>35</td><td>1946</td><td>5/15</td><td>9/30</td><td>10/1</td><td><a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E7%9F%A5%E5%AF%A6%E8%A1%8C%E5%A4%8F%E5%AD%A3%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B35%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E8%A1%8C%E6%94%BF%E9%95%B7%E5%AE%98%E5%85%AC%E7%BD%B2%E4%BB%A3%E9%9B%BB%29">長官公署代電</a>、<a href="https://ja.wikisource.org/wiki/%E5%A4%8F%E5%AD%A3%E6%99%82%E9%96%93%E3%83%B2%E5%AF%A6%E8%A1%8C%E3%82%B9%E3%83%AB%E3%83%8B%E4%BB%98%E6%89%BF%E7%9F%A5%E7%9B%B8%E6%88%90%E5%BA%A6_%281946%E5%B9%B4%E5%8F%B0%E6%B9%BE%E7%9C%81%E8%A1%8C%E6%94%BF%E9%95%B7%E5%AE%98%E5%85%AC%E7%BD%B2%E4%BB%A3%E9%9B%BB%29">日譯</a></td><td></td></tr>
<tr><td>36</td><td>1947</td><td>4/15</td><td>10/31</td><td>11/1</td><td><a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E8%A1%8C%E6%94%BF%E9%95%B7%E5%AE%98%E5%85%AC%E7%BD%B2%E6%89%80%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E7%82%BA%E5%A5%89%E4%BB%A4%E8%87%AA%E5%8D%85%E5%85%AD%E5%B9%B4%E5%9B%9B%E6%9C%88%E5%8D%81%E4%BA%94%E6%97%A5%E8%B5%B7%E5%AF%A6%E8%A1%8C%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B36%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E8%A1%8C%E6%94%BF%E9%95%B7%E5%AE%98%E5%85%AC%E7%BD%B2%E4%BB%A3%E9%9B%BB%29">長官公署代電</a>、<a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E5%BA%9C%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E6%9C%AC%E5%B9%B4%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E5%A5%89%E4%BB%A4%E5%BB%B6%E9%95%B7%E8%87%B3%E5%8D%85%E5%85%AD%E5%B9%B4%E5%8D%81%E6%9C%88%E5%8D%85%E4%B8%80%E6%97%A5%E5%8D%88%E5%A4%9C%E5%BB%BF%E5%9B%9B%E6%99%82%E6%AD%A2_%28%E6%B0%91%E5%9C%8B36%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A3%E9%9B%BB%29">延長</a></td><td>9 月 15 日公佈延長</td></tr>
<tr><td>37</td><td>1948</td><td>5/1</td><td>9/30</td><td>10/1</td><td><a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E5%BA%9C%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E5%B1%A4%E4%BB%A4%E8%A6%8F%E5%AE%9A%E5%8D%85%E4%B8%83%E5%B9%B4%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E8%87%AA%E5%8D%85%E5%B9%B4%E4%BA%94%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E8%87%B3%E4%B9%9D%E6%9C%88%E5%8D%85%E6%97%A5%E5%8D%88%E5%A4%9C%E5%BB%BF%E5%9B%9B%E6%99%82%E6%AD%A2_%28%E6%B0%91%E5%9C%8B37%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A3%E9%9B%BB%29">省府代電</a></td><td></td></tr>
<tr><td>38</td><td>1949</td><td>5/1</td><td>9/30</td><td>10/1</td><td><a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E7%9C%81%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E8%A1%8C%E6%94%BF%E9%99%A2%E9%9B%BB%E7%9F%A5%E6%9C%AC%E5%B9%B4%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E4%BB%8D%E7%85%A7%E6%88%90%E4%BE%8B%E9%90%98%E9%BB%9E%E6%92%A5%E6%97%A9%E4%B8%80%E5%B0%8F%E6%99%82_%28%E6%B0%91%E5%9C%8B38%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A3%E9%9B%BB%29">省府代電(1)</a>、<a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E7%9C%81%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E6%9C%AC%E5%B9%B4%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E8%87%AA%E4%BA%94%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E8%87%B3%E4%B9%9D%E6%9C%88%E5%8D%85%E6%97%A5%E5%8D%88%E5%BE%8C%E5%BB%BF%E5%9B%9B%E6%99%82%E6%AD%A2_%28%E6%B0%91%E5%9C%8B38%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A3%E9%9B%BB%29">(2)</a></td><td></td></tr>
<tr><td>39</td><td>1950</td><td>5/1</td><td>9/30</td><td>10/1</td><td><a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E7%9C%81%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E8%A1%8C%E6%94%BF%E9%99%A2%E4%BB%A4%E8%87%AA%E4%BA%94%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E6%96%BD%E8%A1%8C%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B39%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A3%E9%9B%BB%29">省府代電</a></td><td></td></tr>
<tr><td>40</td><td>1951</td><td>5/1</td><td>9/30</td><td>10/1</td><td><a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E7%9C%81%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E8%A1%8C%E6%94%BF%E9%99%A2%E4%BB%A4%E8%87%AA%E4%BA%94%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E6%96%BD%E8%A1%8C%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B40%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A3%E9%9B%BB%29">省府代電</a></td><td></td></tr>
<tr><td>41</td><td>1952</td><td>3/1</td><td>10/31</td><td>11/1</td><td><a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E6%9C%AC%E7%9C%81%E6%89%80%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E4%B8%89%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E5%85%A8%E5%9C%8B%E5%AF%A6%E8%A1%8C%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B41%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A3%E9%9B%BB%29">省府代電</a></td><td>改稱日光節約時間</td></tr>
<tr><td>42</td><td>1953</td><td>4/1</td><td>10/31</td><td>11/1</td><td><a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E7%9C%81%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E8%A1%8C%E6%94%BF%E9%99%A2%E4%BB%A4%E4%BB%A5%E5%9B%9B%E5%8D%81%E4%BA%8C%E5%B9%B4%E5%BA%A6%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93%E8%87%AA%E5%9B%9B%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E9%96%8B%E5%A7%8B%E5%AF%A6%E8%A1%8C_%28%E6%B0%91%E5%9C%8B42%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">省府令</a></td><td></td></tr>
<tr><td>43</td><td>1954</td><td>4/1</td><td>10/31</td><td>11/1</td><td><a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E8%A1%8C%E6%94%BF%E9%99%A2%E4%BB%A4%E7%9F%A5%E6%9C%AC%E5%B9%B4%E5%BA%A6%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B43%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">省府令</a></td><td></td></tr>
<tr><td>44</td><td>1955</td><td>4/1</td><td>9/30</td><td>10/1</td><td><a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E7%9C%81%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E8%A1%8C%E6%94%BF%E9%99%A2%E4%BB%A4%E7%9F%A5%E6%94%B9%E8%A8%82%E6%9C%AC%E5%B9%B4%E5%BA%A6%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B44%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">省府令</a></td><td></td></tr>
<tr><td>45</td><td>1956</td><td>4/1</td><td>9/30</td><td>10/1</td><td><a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E7%9C%81%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%B1%A4%E5%A5%89%E7%B8%BD%E7%B5%B1%E4%BB%A4%E6%9C%AC%E5%B9%B4%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93%E4%BB%8D%E8%87%AA%E5%9B%9B%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E8%87%B3%E4%B9%9D%E6%9C%88%E5%8D%85%E6%97%A5%E5%8D%88%E5%A4%9C%E5%BB%BF%E5%9B%9B%E6%99%82%E6%AD%A2%E4%B8%80%E6%A1%88%EF%BC%8C%E4%BB%A4%E5%B8%8C%E9%81%B5%E7%85%A7_%28%E6%B0%91%E5%9C%8B45%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">省府令</a></td><td></td></tr>
<tr><td>46</td><td>1957</td><td>4/1</td><td>9/30</td><td>10/1</td><td><a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E7%9C%81%E5%B1%AC%E5%90%84%E7%B4%9A%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E8%A1%8C%E6%94%BF%E9%99%A2%E4%BB%A4%E7%9F%A5%E5%AF%A6%E6%96%BD%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E4%B8%80%E6%A1%88%EF%BC%8C%E8%BD%89%E5%B8%8C%E9%81%B5%E7%85%A7_%28%E6%B0%91%E5%9C%8B46%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">省府令</a></td><td>改稱夏令時間</td></tr>
<tr><td>47</td><td>1958</td><td>4/1</td><td>9/30</td><td>10/1</td><td><a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E7%9C%81%E5%B1%AC%E5%90%84%E7%B4%9A%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%9B%9B%E5%8D%81%E4%B8%83%E5%B9%B4%E5%BA%A6%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E8%87%AA%E5%9B%9B%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E5%AF%A6%E6%96%BD_%28%E6%B0%91%E5%9C%8B47%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">省府令</a></td><td></td></tr>
<tr><td>48</td><td>1959</td><td>4/1</td><td>9/30</td><td>10/1</td><td><a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E7%9C%81%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E4%BB%A4%E8%87%AA%E5%9B%9B%E6%9C%88%E4%B8%80%E6%97%A5%E5%AF%A6%E8%A1%8C%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%EF%BC%8C%E8%BD%89%E5%B8%8C%E9%81%B5%E7%85%A7_%28%E6%B0%91%E5%9C%8B48%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">省府令</a></td><td></td></tr>
<tr><td>49</td><td>1960</td><td>6/1</td><td>9/30</td><td>10/1</td><td><a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E7%9C%81%E5%B1%AC%E5%90%84%E7%B4%9A%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E3%80%81%E5%90%84%E7%B8%A3%E5%B8%82%E6%94%BF%E5%BA%9C%EF%BC%88%E5%B1%80%EF%BC%89%E7%82%BA%E6%8A%84%E7%99%BC%E6%9C%AC%E5%BA%9C%E6%9A%A8%E6%89%80%E5%B1%AC%E5%90%84%E7%B4%9A%E6%A9%9F%E9%97%9C%E5%85%A8%E5%B9%B4%E5%90%84%E6%9C%88%E4%BB%BD%E8%BE%A6%E5%85%AC%E6%99%82%E9%96%93%E8%A1%A8%E5%8F%8A%E5%9B%9B%E5%8D%81%E4%B9%9D%E5%B9%B4%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E8%B5%B7%E6%AD%A2%E6%97%A5%E6%9C%9F%EF%BC%8C%E5%B8%8C%E9%81%B5%E7%85%A7_%28%E6%B0%91%E5%9C%8B49%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">省府令</a></td><td></td></tr>
<tr><td>50</td><td>1961</td><td>6/1</td><td>9/30</td><td>10/1</td><td><a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E7%9C%81%E5%B1%AC%E5%90%84%E7%B4%9A%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E4%BA%94%E5%8D%81%E5%B9%B4%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E8%87%AA%E5%85%AD%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E8%87%B3%E4%B9%9D%E6%9C%88%E4%B8%89%E5%8D%81%E6%97%A5%E5%8D%88%E5%A4%9C%E4%BA%8C%E5%8D%81%E5%9B%9B%E6%99%82%E6%AD%A2%EF%BC%8C%E5%B8%8C%E9%81%B5%E7%85%A7_%28%E6%B0%91%E5%9C%8B50%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">省府令</a></td><td></td></tr>
<tr><td>51</td><td>1962</td><td>-</td><td>-</td><td>-</td><td><a href="https://zh.wikisource.org/wiki/%E8%A1%8C%E6%94%BF%E9%99%A2%E4%BB%A4%E4%BA%94%E5%8D%81%E4%B8%80%E5%B9%B4%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E6%AF%8B%E9%A0%88%E5%BE%AA%E4%BE%8B%E8%BE%A6%E7%90%86%E5%8F%8A%E5%90%84%E6%A9%9F%E9%97%9C%E8%BE%A6%E5%85%AC%E6%99%82%E9%96%93%E4%BB%8D%E7%85%A7%E5%8E%9F%E8%A6%8F%E5%AE%9A%E8%BE%A6%E7%90%86%E6%A1%88_%28%E6%B0%91%E5%9C%8B51%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">省府令：不實施</a></td><td>未實施</td></tr>
<tr><td>52</td><td>1963</td><td>-</td><td>-</td><td>-</td><td></td><td>未實施</td></tr>
<tr><td>53</td><td>1964</td><td>-</td><td>-</td><td>-</td><td></td><td>未實施</td></tr>
<tr><td>54</td><td>1965</td><td>-</td><td>-</td><td>-</td><td></td><td>未實施</td></tr>
<tr><td>55</td><td>1966</td><td>-</td><td>-</td><td>-</td><td></td><td>未實施</td></tr>
<tr><td>56</td><td>1967</td><td>-</td><td>-</td><td>-</td><td></td><td>未實施</td></tr>
<tr><td>57</td><td>1968</td><td>-</td><td>-</td><td>-</td><td></td><td>未實施</td></tr>
<tr><td>58</td><td>1969</td><td>-</td><td>-</td><td>-</td><td></td><td>未實施</td></tr>
<tr><td>59</td><td>1970</td><td>-</td><td>-</td><td>-</td><td></td><td>未實施</td></tr>
<tr><td>60</td><td>1971</td><td>-</td><td>-</td><td>-</td><td></td><td>未實施</td></tr>
<tr><td>61</td><td>1972</td><td>-</td><td>-</td><td>-</td><td></td><td>未實施</td></tr>
<tr><td>62</td><td>1973</td><td>-</td><td>-</td><td>-</td><td></td><td>未實施</td></tr>
<tr><td>63</td><td>1974</td><td>4/1</td><td>9/30</td><td>10/1</td><td><a href="https://zh.wikisource.org/wiki/%E8%A1%8C%E6%94%BF%E9%99%A2%E5%87%BD%E7%82%BA%E7%AF%80%E7%B4%84%E7%94%A8%E9%9B%BB%E5%AF%A6%E6%96%BD%E3%80%8C%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93%E3%80%8D%E8%87%AA%E5%9B%9B%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E8%87%B3%E4%B9%9D%E6%9C%88%E4%B8%89%E5%8D%81%E6%97%A5%E4%BA%8C%E5%8D%81%E5%9B%9B%E6%99%82%E6%AD%A2_%28%E6%B0%91%E5%9C%8B63%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E5%87%BD%29">省府函</a></td><td>稱為節約能源</td></tr>
<tr><td>64</td><td>1975</td><td>4/1</td><td>9/30</td><td>10/1</td><td><a href="https://zh.wikisource.org/wiki/%E8%A1%8C%E6%94%BF%E9%99%A2%E5%87%BD%E7%82%BA%E7%AF%80%E7%B4%84%E7%94%A8%E9%9B%BB%E5%AF%A6%E6%96%BD%E3%80%8C%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93%E3%80%8D%E8%87%AA%E5%85%AD%E5%8D%81%E5%9B%9B%E5%B9%B4%E5%9B%9B%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E8%87%B3%E4%B9%9D%E6%9C%88%E4%B8%89%E5%8D%81%E6%97%A5%E4%BA%8C%E5%8D%81%E5%9B%9B%E6%99%82%E6%AD%A2_%28%E6%B0%91%E5%9C%8B64%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E5%87%BD%29">省府函</a></td><td></td></tr>
<tr><td>65</td><td>1976</td><td>-</td><td>-</td><td>-</td><td><a href="https://zh.wikisource.org/wiki/%E5%A4%8F%E4%BB%A4%E3%80%8C%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93%E3%80%8D%E8%87%AA%E6%9C%AC%E5%B9%B4%E8%B5%B7%E5%81%9C%E6%AD%A2%E5%AF%A6%E6%96%BD_%28%E6%B0%91%E5%9C%8B65%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E5%87%BD%29">省府函：不實施</a></td><td>未實施</td></tr>
<tr><td>66</td><td>1977</td><td>-</td><td>-</td><td>-</td><td></td><td>未實施</td></tr>
<tr><td>67</td><td>1978</td><td>-</td><td>-</td><td>-</td><td></td><td>未實施</td></tr>
<tr><td>68</td><td>1979</td><td>7/1</td><td>9/30</td><td>10/1</td><td><a href="https://zh.wikisource.org/wiki/%E5%87%BD%E8%BD%89%E8%A1%8C%E6%94%BF%E9%99%A2%E8%A6%8F%E5%AE%9A%E7%AF%80%E7%B4%84%E8%83%BD%E6%BA%90%E5%AF%A6%E6%96%BD%E3%80%8C%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93%E3%80%8D_%28%E6%B0%91%E5%9C%8B68%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E5%87%BD%29">省府函</a></td><td>稱為節約能源</td></tr>
<tr><td>69</td><td>1980</td><td>-</td><td>-</td><td>-</td><td><a href="https://zh.wikisource.org/wiki/%E5%87%BD%E7%9F%A5%E5%85%AD%E5%8D%81%E4%B9%9D%E5%B9%B4%E5%81%9C%E6%AD%A2%E5%AF%A6%E6%96%BD%E3%80%8C%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93%E3%80%8D%E5%8F%8A%E9%87%8D%E6%96%B0%E8%A6%8F%E5%AE%9A%E7%9C%81%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E8%BE%A6%E5%85%AC%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B69%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E5%87%BD%29">省府函：不實施</a></td><td>此後再也沒實施</td></tr>
</tbody></table>
<hr />
<h2 id="yin-yong-wen-xian">引用文獻</h2>
<p>以下原始文獻都可以在<a href="http://gaz.ncl.edu.tw/">政府公報資訊網</a>、<a href="http://subtpg.tpg.gov.tw/og/q1.asp">臺灣省政府網際網路公報查詢系統</a>、<a href="http://db2.th.gov.tw/db2/view/">臺灣總督府（官）報資料庫</a>找到。我把它們全部搬到 Wikisource 了。</p>
<ul>
<li>1945a: <a href="https://zh.wikisource.org/wiki/%E5%9C%8B%E9%98%B2%E6%9C%80%E9%AB%98%E5%A7%94%E5%93%A1%E6%9C%83%E8%A6%8F%E5%AE%9A%E5%85%A8%E5%9C%8B%E5%90%84%E5%9C%B0%E8%87%AA%E4%B8%89%E5%8D%81%E5%9B%9B%E5%B9%B4%E4%BA%94%E6%9C%88%E4%B8%80%E6%97%A5%E8%B5%B7%E8%87%B3%E4%B9%9D%E6%9C%88%E4%B8%89%E5%8D%81%E6%97%A5%E6%AD%A2%E5%B0%87%E6%99%82%E9%96%93%E6%8F%90%E5%89%8D%E4%B8%80%E5%B0%8F%E6%99%82%E4%BB%A4%E4%BB%B0%E9%81%B5%E7%85%A7%E5%B9%B6%E9%A3%AD%E5%B1%AC%E9%81%B5%E7%85%A7%E7%94%B1_%28%E6%B0%91%E5%9C%8B34%E5%B9%B4%E5%9C%8B%E6%B0%91%E6%94%BF%E5%BA%9C%E8%A8%93%E4%BB%A4%29">國防最高委員會規定全國各地自三十四年五月一日起至九月三十日止將時間提前一小時令仰遵照並飭屬遵照由 (民國34年國民政府訓令)</a></li>
<li>1945b: <a href="https://ja.wikisource.org/wiki/%E8%87%BA%E7%81%A3%E3%83%8E%E6%A8%99%E6%BA%96%E6%99%82%E3%83%8B%E9%97%9C%E3%82%B9%E3%83%AB%E4%BB%B6_%28%E6%98%AD%E5%92%8C%E4%BA%8C%E5%8D%81%E5%B9%B4%E5%8F%B0%E6%B9%BE%E7%B7%8F%E7%9D%A3%E5%BA%9C%E5%91%8A%E7%A4%BA%E7%AC%AC%E4%B8%89%E7%99%BE%E5%85%AB%E5%8D%81%E5%85%AD%E5%8F%B7%29">臺灣ノ標準時ニ關スル件 (昭和二十年台湾総督府告示第三百八十六号)</a></li>
<li>1946a: <a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E7%9F%A5%E5%AF%A6%E8%A1%8C%E5%A4%8F%E5%AD%A3%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B35%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E8%A1%8C%E6%94%BF%E9%95%B7%E5%AE%98%E5%85%AC%E7%BD%B2%E4%BB%A3%E9%9B%BB%29">電知實行夏季時間 (民國35年臺灣省行政長官公署代電)</a></li>
<li>1946b: <a href="https://ja.wikisource.org/wiki/%E5%A4%8F%E5%AD%A3%E6%99%82%E9%96%93%E3%83%B2%E5%AF%A6%E8%A1%8C%E3%82%B9%E3%83%AB%E3%83%8B%E4%BB%98%E6%89%BF%E7%9F%A5%E7%9B%B8%E6%88%90%E5%BA%A6_%281946%E5%B9%B4%E5%8F%B0%E6%B9%BE%E7%9C%81%E8%A1%8C%E6%94%BF%E9%95%B7%E5%AE%98%E5%85%AC%E7%BD%B2%E4%BB%A3%E9%9B%BB%29">夏季時間ヲ實行スルニ付承知相成度 (1946年台湾省行政長官公署代電)</a></li>
<li>1947a: <a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E8%A1%8C%E6%94%BF%E9%95%B7%E5%AE%98%E5%85%AC%E7%BD%B2%E6%89%80%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E7%82%BA%E5%A5%89%E4%BB%A4%E8%87%AA%E5%8D%85%E5%85%AD%E5%B9%B4%E5%9B%9B%E6%9C%88%E5%8D%81%E4%BA%94%E6%97%A5%E8%B5%B7%E5%AF%A6%E8%A1%8C%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B36%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E8%A1%8C%E6%94%BF%E9%95%B7%E5%AE%98%E5%85%AC%E7%BD%B2%E4%BB%A3%E9%9B%BB%29">電行政長官公署所屬各機關為奉令自卅六年四月十五日起實行夏令時間 (民國36年臺灣省行政長官公署代電)</a></li>
<li>1947b: <a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E5%BA%9C%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E6%9C%AC%E5%B9%B4%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E5%A5%89%E4%BB%A4%E5%BB%B6%E9%95%B7%E8%87%B3%E5%8D%85%E5%85%AD%E5%B9%B4%E5%8D%81%E6%9C%88%E5%8D%85%E4%B8%80%E6%97%A5%E5%8D%88%E5%A4%9C%E5%BB%BF%E5%9B%9B%E6%99%82%E6%AD%A2_%28%E6%B0%91%E5%9C%8B36%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A3%E9%9B%BB%29">電府屬各機關學校為本年夏令時間奉令延長至卅六年十月卅一日午夜廿四時止 (民國36年臺灣省政府代電)</a></li>
<li>1948: <a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E5%BA%9C%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E5%B1%A4%E4%BB%A4%E8%A6%8F%E5%AE%9A%E5%8D%85%E4%B8%83%E5%B9%B4%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E8%87%AA%E5%8D%85%E5%B9%B4%E4%BA%94%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E8%87%B3%E4%B9%9D%E6%9C%88%E5%8D%85%E6%97%A5%E5%8D%88%E5%A4%9C%E5%BB%BF%E5%9B%9B%E6%99%82%E6%AD%A2_%28%E6%B0%91%E5%9C%8B37%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A3%E9%9B%BB%29">電府屬各機關學校為奉層令規定卅七年夏令時間自卅年五月一日零時起至九月卅日午夜廿四時止 (民國37年臺灣省政府代電)</a></li>
<li>1949a: <a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E7%9C%81%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E8%A1%8C%E6%94%BF%E9%99%A2%E9%9B%BB%E7%9F%A5%E6%9C%AC%E5%B9%B4%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E4%BB%8D%E7%85%A7%E6%88%90%E4%BE%8B%E9%90%98%E9%BB%9E%E6%92%A5%E6%97%A9%E4%B8%80%E5%B0%8F%E6%99%82_%28%E6%B0%91%E5%9C%8B38%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A3%E9%9B%BB%29">電省屬各機關學校為奉行政院電知本年夏令時間仍照成例鐘點撥早一小時 (民國38年臺灣省政府代電)</a></li>
<li>1949b: <a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E7%9C%81%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E6%9C%AC%E5%B9%B4%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E8%87%AA%E4%BA%94%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E8%87%B3%E4%B9%9D%E6%9C%88%E5%8D%85%E6%97%A5%E5%8D%88%E5%BE%8C%E5%BB%BF%E5%9B%9B%E6%99%82%E6%AD%A2_%28%E6%B0%91%E5%9C%8B38%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A3%E9%9B%BB%29">電省屬各機關學校為本年夏令時間自五月一日零時起至九月卅日午後廿四時止 (民國38年臺灣省政府代電)</a></li>
<li>1950: <a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E7%9C%81%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E8%A1%8C%E6%94%BF%E9%99%A2%E4%BB%A4%E8%87%AA%E4%BA%94%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E6%96%BD%E8%A1%8C%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B39%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A3%E9%9B%BB%29">電省屬各機關學校為奉行政院令自五月一日零時起施行夏令時間 (民國39年臺灣省政府代電)</a></li>
<li>1951: <a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E7%9C%81%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E8%A1%8C%E6%94%BF%E9%99%A2%E4%BB%A4%E8%87%AA%E4%BA%94%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E6%96%BD%E8%A1%8C%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B40%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A3%E9%9B%BB%29">電省屬各機關學校為奉行政院令自五月一日零時起施行夏令時間 (民國40年臺灣省政府代電)</a></li>
<li>1952: <a href="https://zh.wikisource.org/wiki/%E9%9B%BB%E6%9C%AC%E7%9C%81%E6%89%80%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E4%B8%89%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E5%85%A8%E5%9C%8B%E5%AF%A6%E8%A1%8C%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B41%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A3%E9%9B%BB%29">電本省所屬各機關學校為三月一日零時起全國實行日光節約時間 (民國41年臺灣省政府代電)</a></li>
<li>1953: <a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E7%9C%81%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E8%A1%8C%E6%94%BF%E9%99%A2%E4%BB%A4%E4%BB%A5%E5%9B%9B%E5%8D%81%E4%BA%8C%E5%B9%B4%E5%BA%A6%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93%E8%87%AA%E5%9B%9B%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E9%96%8B%E5%A7%8B%E5%AF%A6%E8%A1%8C_%28%E6%B0%91%E5%9C%8B42%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">令省各機關學校為奉行政院令以四十二年度日光節約時間自四月一日零時起開始實行 (民國42年臺灣省政府令)</a></li>
<li>1954: <a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E8%A1%8C%E6%94%BF%E9%99%A2%E4%BB%A4%E7%9F%A5%E6%9C%AC%E5%B9%B4%E5%BA%A6%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B43%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">令各機關學校為奉行政院令知本年度日光節約時間 (民國43年臺灣省政府令)</a></li>
<li>1955: <a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E7%9C%81%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E8%A1%8C%E6%94%BF%E9%99%A2%E4%BB%A4%E7%9F%A5%E6%94%B9%E8%A8%82%E6%9C%AC%E5%B9%B4%E5%BA%A6%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B44%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">令省屬各機關學校為奉行政院令知改訂本年度日光節約時間 (民國44年臺灣省政府令)</a></li>
<li>1956: <a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E7%9C%81%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%B1%A4%E5%A5%89%E7%B8%BD%E7%B5%B1%E4%BB%A4%E6%9C%AC%E5%B9%B4%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93%E4%BB%8D%E8%87%AA%E5%9B%9B%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E8%87%B3%E4%B9%9D%E6%9C%88%E5%8D%85%E6%97%A5%E5%8D%88%E5%A4%9C%E5%BB%BF%E5%9B%9B%E6%99%82%E6%AD%A2%E4%B8%80%E6%A1%88%EF%BC%8C%E4%BB%A4%E5%B8%8C%E9%81%B5%E7%85%A7_%28%E6%B0%91%E5%9C%8B45%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">令省屬各機關學校為層奉總統令本年日光節約時間仍自四月一日零時起至九月卅日午夜廿四時止一案，令希遵照 (民國45年臺灣省政府令)</a></li>
<li>1957: <a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E7%9C%81%E5%B1%AC%E5%90%84%E7%B4%9A%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E8%A1%8C%E6%94%BF%E9%99%A2%E4%BB%A4%E7%9F%A5%E5%AF%A6%E6%96%BD%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E4%B8%80%E6%A1%88%EF%BC%8C%E8%BD%89%E5%B8%8C%E9%81%B5%E7%85%A7_%28%E6%B0%91%E5%9C%8B46%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">令省屬各級機關學校為奉行政院令知實施夏令時間一案，轉希遵照 (民國46年臺灣省政府令)</a></li>
<li>1958: <a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E7%9C%81%E5%B1%AC%E5%90%84%E7%B4%9A%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%9B%9B%E5%8D%81%E4%B8%83%E5%B9%B4%E5%BA%A6%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E8%87%AA%E5%9B%9B%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E5%AF%A6%E6%96%BD_%28%E6%B0%91%E5%9C%8B47%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">令省屬各級機關學校為四十七年度夏令時間自四月一日零時起實施 (民國47年臺灣省政府令)</a></li>
<li>1959: <a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E7%9C%81%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E5%A5%89%E4%BB%A4%E8%87%AA%E5%9B%9B%E6%9C%88%E4%B8%80%E6%97%A5%E5%AF%A6%E8%A1%8C%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%EF%BC%8C%E8%BD%89%E5%B8%8C%E9%81%B5%E7%85%A7_%28%E6%B0%91%E5%9C%8B48%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">令省屬各機關學校為奉令自四月一日實行夏令時間，轉希遵照 (民國48年臺灣省政府令)</a></li>
<li>1960: <a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E7%9C%81%E5%B1%AC%E5%90%84%E7%B4%9A%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E3%80%81%E5%90%84%E7%B8%A3%E5%B8%82%E6%94%BF%E5%BA%9C%EF%BC%88%E5%B1%80%EF%BC%89%E7%82%BA%E6%8A%84%E7%99%BC%E6%9C%AC%E5%BA%9C%E6%9A%A8%E6%89%80%E5%B1%AC%E5%90%84%E7%B4%9A%E6%A9%9F%E9%97%9C%E5%85%A8%E5%B9%B4%E5%90%84%E6%9C%88%E4%BB%BD%E8%BE%A6%E5%85%AC%E6%99%82%E9%96%93%E8%A1%A8%E5%8F%8A%E5%9B%9B%E5%8D%81%E4%B9%9D%E5%B9%B4%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E8%B5%B7%E6%AD%A2%E6%97%A5%E6%9C%9F%EF%BC%8C%E5%B8%8C%E9%81%B5%E7%85%A7_%28%E6%B0%91%E5%9C%8B49%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">令省屬各級機關學校、各縣市政府（局）為抄發本府暨所屬各級機關全年各月份辦公時間表及四十九年夏令時間起止日期，希遵照 (民國49年臺灣省政府令)</a></li>
<li>1961: <a href="https://zh.wikisource.org/wiki/%E4%BB%A4%E7%9C%81%E5%B1%AC%E5%90%84%E7%B4%9A%E6%A9%9F%E9%97%9C%E5%AD%B8%E6%A0%A1%E7%82%BA%E4%BA%94%E5%8D%81%E5%B9%B4%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E8%87%AA%E5%85%AD%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E8%87%B3%E4%B9%9D%E6%9C%88%E4%B8%89%E5%8D%81%E6%97%A5%E5%8D%88%E5%A4%9C%E4%BA%8C%E5%8D%81%E5%9B%9B%E6%99%82%E6%AD%A2%EF%BC%8C%E5%B8%8C%E9%81%B5%E7%85%A7_%28%E6%B0%91%E5%9C%8B50%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">令省屬各級機關學校為五十年夏令時間自六月一日零時起至九月三十日午夜二十四時止，希遵照 (民國50年臺灣省政府令)</a></li>
<li>1962: <a href="https://zh.wikisource.org/wiki/%E8%A1%8C%E6%94%BF%E9%99%A2%E4%BB%A4%E4%BA%94%E5%8D%81%E4%B8%80%E5%B9%B4%E5%A4%8F%E4%BB%A4%E6%99%82%E9%96%93%E6%AF%8B%E9%A0%88%E5%BE%AA%E4%BE%8B%E8%BE%A6%E7%90%86%E5%8F%8A%E5%90%84%E6%A9%9F%E9%97%9C%E8%BE%A6%E5%85%AC%E6%99%82%E9%96%93%E4%BB%8D%E7%85%A7%E5%8E%9F%E8%A6%8F%E5%AE%9A%E8%BE%A6%E7%90%86%E6%A1%88_%28%E6%B0%91%E5%9C%8B51%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E4%BB%A4%29">行政院令五十一年夏令時間毋須循例辦理及各機關辦公時間仍照原規定辦理案 (民國51年臺灣省政府令)</a></li>
<li>1974: <a href="https://zh.wikisource.org/wiki/%E8%A1%8C%E6%94%BF%E9%99%A2%E5%87%BD%E7%82%BA%E7%AF%80%E7%B4%84%E7%94%A8%E9%9B%BB%E5%AF%A6%E6%96%BD%E3%80%8C%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93%E3%80%8D%E8%87%AA%E5%9B%9B%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E8%87%B3%E4%B9%9D%E6%9C%88%E4%B8%89%E5%8D%81%E6%97%A5%E4%BA%8C%E5%8D%81%E5%9B%9B%E6%99%82%E6%AD%A2_%28%E6%B0%91%E5%9C%8B63%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E5%87%BD%29">行政院函為節約用電實施「日光節約時間」自四月一日零時起至九月三十日二十四時止 (民國63年臺灣省政府函)</a></li>
<li>1975: <a href="https://zh.wikisource.org/wiki/%E8%A1%8C%E6%94%BF%E9%99%A2%E5%87%BD%E7%82%BA%E7%AF%80%E7%B4%84%E7%94%A8%E9%9B%BB%E5%AF%A6%E6%96%BD%E3%80%8C%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93%E3%80%8D%E8%87%AA%E5%85%AD%E5%8D%81%E5%9B%9B%E5%B9%B4%E5%9B%9B%E6%9C%88%E4%B8%80%E6%97%A5%E9%9B%B6%E6%99%82%E8%B5%B7%E8%87%B3%E4%B9%9D%E6%9C%88%E4%B8%89%E5%8D%81%E6%97%A5%E4%BA%8C%E5%8D%81%E5%9B%9B%E6%99%82%E6%AD%A2_%28%E6%B0%91%E5%9C%8B64%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E5%87%BD%29">行政院函為節約用電實施「日光節約時間」自六十四年四月一日零時起至九月三十日二十四時止 (民國64年臺灣省政府函)</a></li>
<li>1976: <a href="https://zh.wikisource.org/wiki/%E5%A4%8F%E4%BB%A4%E3%80%8C%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93%E3%80%8D%E8%87%AA%E6%9C%AC%E5%B9%B4%E8%B5%B7%E5%81%9C%E6%AD%A2%E5%AF%A6%E6%96%BD_%28%E6%B0%91%E5%9C%8B65%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E5%87%BD%29">夏令「日光節約時間」自本年起停止實施 (民國65年臺灣省政府函)</a></li>
<li>1979: <a href="https://zh.wikisource.org/wiki/%E5%87%BD%E8%BD%89%E8%A1%8C%E6%94%BF%E9%99%A2%E8%A6%8F%E5%AE%9A%E7%AF%80%E7%B4%84%E8%83%BD%E6%BA%90%E5%AF%A6%E6%96%BD%E3%80%8C%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93%E3%80%8D_%28%E6%B0%91%E5%9C%8B68%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E5%87%BD%29">函轉行政院規定節約能源實施「日光節約時間」 (民國68年臺灣省政府函)</a></li>
<li>1980: <a href="https://zh.wikisource.org/wiki/%E5%87%BD%E7%9F%A5%E5%85%AD%E5%8D%81%E4%B9%9D%E5%B9%B4%E5%81%9C%E6%AD%A2%E5%AF%A6%E6%96%BD%E3%80%8C%E6%97%A5%E5%85%89%E7%AF%80%E7%B4%84%E6%99%82%E9%96%93%E3%80%8D%E5%8F%8A%E9%87%8D%E6%96%B0%E8%A6%8F%E5%AE%9A%E7%9C%81%E5%B1%AC%E5%90%84%E6%A9%9F%E9%97%9C%E8%BE%A6%E5%85%AC%E6%99%82%E9%96%93_%28%E6%B0%91%E5%9C%8B69%E5%B9%B4%E8%87%BA%E7%81%A3%E7%9C%81%E6%94%BF%E5%BA%9C%E5%87%BD%29">函知六十九年停止實施「日光節約時間」及重新規定省屬各機關辦公時間 (民國69年臺灣省政府函)</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>關於 Ruby 的 autoload 與 Rails 的 autoload_paths 以及 reopen module &#x2F; class</title>
            <link>https://blog.yorkxin.org/posts/autoload-in-ruby-autoload-paths-in-rails-and-module-reopening/</link>
            <pubDate>Mon, 10 Feb 2014 05:04:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2014/02/10/autoload-in-ruby-autoload-paths-in-rails-and-module-reopening.html</guid>
            
            <description><![CDATA[
                <p>最近在實作一個特別的需求，做了一個 gem 搞這種事：</p>
<ul>
<li>在 Gem 裡面， <code>lib/models/post.rb</code> 定義 <code>Post &lt; ActiveRecord::Base</code></li>
<li>在 App 裡面， <code>app/models/post.rb</code> 打開 <code>class Post</code> 多寫一些 app-specific methods</li>
</ul>
<p>然後就搞了三天搞不定。</p>
<p>具體的現象是：</p>
<ul>
<li>在 Gem 裡面，不論是使用 <a href="http://ruby-doc.org/core-2.1.0/Kernel.html#method-i-autoload"><code>Kernel#autoload</code></a> 還是 Rails 的 <code>config.autoload_paths &lt;&lt;</code> 來做到自動載入，都無法在 App 改寫 Post class 。</li>
<li>如果在 Gem 裡面不做 autoloading ，則 Rails 會去抓 App 裡面的 <code>app/models/post.rb</code>, which is not inherited from ActiveRecord::Base 。</li>
</ul>
<p>之後試了繼承（很難搞）和 module ，最後是用 ActiveSupport::Concern 包了 module ，把 association 之類的東西寫在 <code>included do</code> 裡面，解決。</p>
<p>今天讀到這篇文章 <a href="http://urbanautomaton.com/blog/2013/08/27/rails-autoloading-hell/">Rails autoloading — how it works, and when it doesn't</a> ，對於 Ruby 和 Rails 的 "autoload" 有粗略的瞭解了。簡單整理如下：</p>
<ul>
<li>Ruby 的 <code>Kernel#autoload</code> 是告訴 Ruby runtime 「要找某個 constant 的時候，可以載入某檔案」，比較像是「登記」，在登記之後， Ruby runtime 若發現程式裡面有要用某個 const ，但沒有定義，就會載入該檔案，這是發生在「第一次使用」的時候，用第二次就不會觸發。</li>
<li>Rails 的 autoloading 跟 Ruby 的 <code>Kernel#autoload</code> 完全不一樣，實作方式是用 <code>Module#const_missing</code> ：抓不到（const 在 runtime 沒定義）的時候才自動根據 constant 找檔名，例如 <code>Taiwan::Taipei::SungShan</code> 就是會找 <code>taiwan/taipei/sung_shan.rb</code> 。</li>
<li>承上，「要去哪裡找檔案」這件事，是在 <code>config.autoload_paths</code> 設定的，這個 array 就是「要自動從檔案載入缺失的 const 的時候，就去依序搜尋哪些路徑」，類似 shell 的 <code>$PATH</code> 。如果檔案不存在，就會 raise NameError ；如果檔案存在，但 const name 跟所要找的不同，就會出現「Expected app/models/user.rb to define User」這種錯誤。</li>
<li>承上，第一次載入完成以後，就可以在 Runtime 裡面找到，所以不會再度觸發 <code>const_missing</code> 來自動搜尋。</li>
</ul>
<p>所以：</p>
<ul>
<li><code>Kernel#autoload</code> 不應跟 Rails 的 <code>autoload_paths</code> 混淆，要視為兩個完全不同的功能</li>
<li>誰第一次載入誰算數， Rails 只在找不到該 const 的時候才會去 <code>autoload_paths</code> 搜尋</li>
<li>所以，如果某個 const (class / module) 已經在 runtime 裡面定義了，那麼要在 Rails 裡面 reopen 它，就必須確定它一定會執行，例如 initializers 裡面，或是手動 require 它。如果是放在某個 autoload paths 裡面，例如 <code>app/models/</code> ，則 Rails 並不會執行之，因為同名的 const 已經在 Runtime 裡面了。</li>
</ul>
<p>這也就是為什麼會有「在 gem 和在 app 裡面，同名的 model class 是 mutually-exclusive，除非手動 require 才能改寫其內容」。也就是說，想要在 gem 裡面定義一個 model ，然後在 rails app 裡面 reopen 它，是不可能的，必須要手動載入它的 reopening。</p>
<p>說得更 general 一點就是：如果該 class / module 已經在 Gem 裡面載入，則要在 Rails 裡面 reopen 它，就必須放在 <code>autoload_paths</code> 以外的地方，並且手動 require 之。</p>
<hr />
<p>該文很推薦一讀，除了詳細說明了 Ruby 和 Rails 的 autloading 機制，還提到一些陷阱，例如說 Rails 的 autoloading 其實不會理 <a href="http://ruby-doc.org/core-2.1.0/Module.html#method-c-nesting">Module.nesting</a> (lexical context of current line) ，這樣子某些情況下會變成「第一次可以成功 autoload ，但第二次卻說 NameError 找不到 const」這種問題。</p>

            ]]></description>
        </item>
        <item>
            <title>OAuth 2.0 Tutorial: Protect Grape API with Doorkeeper</title>
            <link>https://blog.yorkxin.org/posts/oauth2-tutorial-grape-api-doorkeeper-en/</link>
            <pubDate>Mon, 04 Nov 2013 16:10:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/11/05/oauth2-tutorial-grape-api-doorkeeper-en.html</guid>
            
            <description><![CDATA[
                <p>In this tutorial, I'll demonstrate how to protect the API with OAuth 2 protocol. The API is built with Grape, and mounted under Ruby on Rails.</p>
<p>This tutorial was originally written in Chinese, so if you know Chinese please read this: <a href="http://blog.yorkxin.org/posts/2013/10/10/oauth2-tutorial-grape-api-doorkeeper">OAuth 2.0 Tutorial: Grape API 整合 Doorkeeper</a>.</p>
<p>To protect the API with OAuth 2, we have to build the following things:</p>
<ul>
<li><strong>Resource Owner</strong> - The role who can grant authorization to the 3rd-party application, i.e. the User.</li>
<li><strong>Authorization Server</strong> - Everything about authorization goes here, such as:
<ul>
<li><strong>Clients</strong> - We need a basic CRUD interface to manage clients (applications).</li>
<li><strong>Access Token</strong> (Model) - We need a Model to store Access Tokens.</li>
<li><strong>Authorization Endpoint</strong> - Here to process Auth Code Grant and Implicit Grant flows.</li>
<li><strong>Token Endpoint</strong> - The Access Tokens are actually issued here.</li>
</ul>
</li>
<li><strong>Resource Server</strong> - A location for applications to access, i.e. the API. Some APIs that need Access Tokens to access are called "Protected Resources."
<ul>
<li><strong>Guard on Resource Server</strong> - To protect some APIs from accessing them without Access Tokens.</li>
</ul>
</li>
</ul>
<p>Most components are implemented with existing solutions:</p>
<ul>
<li>Resource Owner (User) - <strong><a href="https://github.com/plataformatec/devise">Devise</a></strong></li>
<li>Authorization Server (OAuth 2 Provider) - <strong><a href="https://github.com/applicake/doorkeeper">Doorkeeper</a></strong></li>
<li>Resource Server (API) - <strong><a href="https://github.com/intridea/grape">Grape</a></strong></li>
<li>Guard - Manually integrated <strong><a href="https://github.com/nov/rack-oauth2">Rack::OAuth2</a></strong> into Grape.</li>
</ul>
<p>Since <code>doorkeeper_for</code> from Doorkeeper can only be used in Rails, and Rack::OAuth2 is simply a Rack Middleware, so we have to mash them up manually. I've written a simple review for the current solutions: <a href="http://blog.yorkxin.org/posts/2013/10/08/oauth2-ruby-and-rails-integration-review">〈Ruby / Rails 的 OAuth 2 整合方案簡單評比〉</a> (Chinese only; may be translated into English someday)</p>
<p>I've put the whole process in <a href="https://github.com/yorkxin/oauth2-api-sample">chitsaou/oauth2-api-sample</a> repository. Each step has a corresponding step-x tag, for example the result of Step 1 is available at step-1 tag.</p>
<hr />
<span id="continue-reading"></span>
<h2 id="step-1-build-resource-owner-business-logic-user">Step 1: Build Resource Owner Business Logic (User)</h2>
<p>As mentioned above, we'll build it with Devise. This should be the basics of every Rails Developer, so I'm going to skip the details. Check out <a href="https://github.com/yorkxin/oauth2-api-sample/tree/step-1">step-1 tag</a> for the result.</p>
<p>You can try access <code>/pages/secret</code> and it should ask you to login.</p>
<h2 id="step-2-build-the-resource-server-api">Step 2: Build the Resource Server (API)</h2>
<p>Here I'm building the API with Grape, because I don't want the request pass through too many stacks on Rails.</p>
<p>This is simple, and not the key part of this tutorial. If you hit any problems, just check the official document. Check out <a href="https://github.com/yorkxin/oauth2-api-sample/tree/step-2">step-2 tag</a> for the result.</p>
<h2 id="step-3-build-the-authorization-server-provider">Step 3: Build the Authorization Server (Provider)</h2>
<p>It's based on Rails, so I'm going to use Doorkeeper. Check out <a href="https://github.com/yorkxin/oauth2-api-sample/tree/step-3">step-3 tag</a> for the result.</p>
<p><a href="http://railscasts.com/episodes/353-oauth-with-doorkeeper">There is a video tutorial on RailsCasts</a> which is worth checkout. However it is not that hard if we follow the official document:</p>
<p>Install the Doorkeeper Gem</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># Gemfile
gem &#x27;doorkeeper&#x27;
</code></pre>
<p>Don't forget to run <code>bundle install</code>.</p>
<p>Then run the following command to install:</p>
<pre><code>$ rails generate doorkeeper:install
$ rails generate doorkeeper:migration
$ rake db:migrate
</code></pre>
<p>Then change the configuration file to make the Doorkeeper authenticate Resource Owner with Devise:</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># config&#x2F;initializers&#x2F;doorkeeper.rb

# Authenticate Resource Owner with Devise
resource_owner_authenticator do
  current_user || warden.authenticate!(:scope =&gt; :user)
end
</code></pre>
<p>That's all. We have built a Authorization Server.</p>
<p>Doorkeeper creates the foolowing tables:</p>
<ul>
<li><strong>oauth_applications</strong> - The registry for Clients</li>
<li><strong>oauth_access_grants</strong> - The registry of Auth Codes (issued during the first step of Authroization Code Grant Type Flow)</li>
<li><strong>oauth_access_tokens</strong> - Stores the Access Tokens that actually issued, including the corresponding Refresh Tokens (disabled by default)</li>
</ul>
<p>Doorkeeper registers the following Routes:</p>
<table><thead><tr><th>Method (REST)</th><th>Path</th><th>For</th></tr></thead><tbody>
<tr><td>new</td><td>/oauth/authorize</td><td>Authorization Endpoint</td></tr>
<tr><td>create</td><td>/oauth/authorize</td><td>Action when User grants the authorization request</td></tr>
<tr><td>destroy</td><td>/oauth/authorize</td><td>Action when User denies the authorization request</td></tr>
<tr><td>show</td><td>/oauth/authorize/:code</td><td>(For local test?)</td></tr>
<tr><td>update</td><td>/oauth/authorize</td><td>(Unknown Update Grant?)</td></tr>
<tr><td>create</td><td>/oauth/token</td><td>Token Endpoint</td></tr>
<tr><td>show</td><td>/oauth/token/info</td><td>Token Debug Endpoint</td></tr>
<tr><td>resources</td><td>/oauth/applications</td><td>Clients Management (CURD)</td></tr>
<tr><td>index</td><td>/oauth/authorized_applications</td><td>Resource Owner manages the authorized Clients</td></tr>
<tr><td>destroy</td><td>/oauth/authorized_applications/:id</td><td>Resource Owner manages the authorized Clients</td></tr>
</tbody></table>
<p>The show action of Authorization Endpoint displays the grant code only, which I think is for local testing; the update actions has on actual method to catch it, not sure whether it is a dead feature or not.</p>
<p>You can found that:</p>
<ul>
<li>It gives Authorization Endpoint &amp; Token Endpoint.</li>
<li>It gives Token Debug Endpoint, which can be used to verify the Token in Implicit Flow.</li>
<li>It comes with a Clients management interface.</li>
<li>It also comes with a interface for users to manage authorized Clients.</li>
</ul>
<p>Doorkeeper provides everything that an Authorization Server needs.</p>
<h3 id="step-3-1-create-a-client-for-testing">Step 3.1: Create a Client for Testing</h3>
<p>After Authorization Server is built, we can now create a new Client. Open <code>/oauth/applications/new</code> and fill <code>http://localhost:12345/auth/demo/callback</code> in Redirect URI field, and submit. There does not have to be a web server on localhost:12345. We can still grab the grant code or token for testing.</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2013/2013-11-05-oauth2-tutorial-grape-api-doorkeeper-en/1wLQZN9CS9SixjFgRaq1_oauth2-new-client.png" alt="oauth2-new-client.png" /></p>
<h3 id="step-3-2-get-access-token">Step 3.2: Get Access Token</h3>
<p>Now we can try to get the Access Token. We're going to simulate the Client to run the Authorization Code Grant Flow. Here is how:</p>
<p>First, open the show page of the Client that we just created. You'll see a page displaying the Application ID, Secret and other info about the Client. At the bottom of the page, there is a <strong>Authorize</strong> link. Click it and it'll open the following location:</p>
<pre><code>http:&#x2F;&#x2F;localhost:9999&#x2F;oauth&#x2F;authorize
    ?client_id=4a407c6a8d3c75e17a5560d0d0e4507c77b047940db6df882c86aaeac2c788d6
    &amp;redirect_uri=http%3A%2F%2Flocalhost%3A12345%2Fauth%2Fdemo%2Fcallback
    &amp;response_type=code
</code></pre>
<p>(Note: assume this Rails App runs on localhost:9999; additional line breaks are inserted for readability)</p>
<p>As the grant flow spec describes, the Client now sends a request to Authorization Endpoint for a Grant Code, presenting its Redirect URI and Client ID.</p>
<p>Now it (Doorkeeper) asks you (Resoruce Owner) whether to Authorize or Deny. We're going to select Authorize here.</p>
<p>It then navigates you to a location that the browser cannot open, which is the Redirect URI that we assigned when creating the app. But don't worry, we've got the Grant Code:</p>
<pre><code>http:&#x2F;&#x2F;localhost:12345&#x2F;auth&#x2F;demo&#x2F;callback
    ?code=21e1c81db4e619a23d4ed46134884104225d4189baa005220bd9b358be8b591a
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
          Grant Code
</code></pre>
<p>If you have noticed that during Step 3.1, the client creation page hints you to fill <code>urn:ietf:wg:oauth:2.0:oob</code> in Redirect URI field, and you actually did, then the grant code will be displayed in the show action. This behavior is for local testing, which is like one of <a href="https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi">Google OAuth 2.0's workflows</a>.</p>
<p>Now the Client has got the Grant Code. According to the spec, the Client should exchange the Grant Code for the actual Access Token through a back channel to Authorization Server.</p>
<p>Because there are too many arguments to fill, here I put the screenshot of <a href="http://www.getpostman.com/">Postman</a>. Fill the form and hit Send, then you'll get the Access Token!</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2013/2013-11-05-oauth2-tutorial-grape-api-doorkeeper-en/SbFoCbPQTB2S9gvuXWLR_oauth2-token-request-en.png" alt="oauth2-token-request-en.png" /></p>
<h2 id="step-4-build-the-guard-on-resource-server">Step 4: Build the Guard on Resource Server</h2>
<p>This is the most difficult part in the tutorial. As I said in the <a href="http://blog.yorkxin.org/posts/2013/10/08/oauth2-ruby-and-rails-integration-review">last article</a> (Chinese only), if the API is built with Grape, then <strong>no any Guard solution can be used immediately</strong>. If you build your API with Rails, the <code>doorkeeper_for</code> guard is currently not fully implemented. My current solution is taking Bearer Token middleware of Rack::OAuth2 and attach it onto Grape, and also used some logic from <code>doorkeeper_for</code>.</p>
<p>I'll detail this step as much as possible. Check out <a href="https://github.com/yorkxin/oauth2-api-sample/tree/step-4">step-4 tag</a> for the result.</p>
<p>The guard module is made with <a href="http://api.rubyonrails.org/classes/ActiveSupport/Concern.html">ActiveSupport::Concern</a>, and put in <code>api/concerns/api_guard.rb</code>.</p>
<h3 id="step-4-1-install-rack-middleware-to-fetch-access-token-string">Step 4.1: Install Rack Middleware to "Fetch" Access Token (String)</h3>
<p>When installing (<code>use</code>) the Rack::OAuth2 Middleware, we have to provide it a block, which will be called by the middleware. However, it will only be called if <strong>ahe request comes with an OAuth2 Token</strong>, that is:</p>
<ul>
<li>It calls the block if the request comes with <code>Authorization: Bearer XXX</code> (header) or <code>?access_token=xxx</code> (query param)</li>
<li>It does not call the block if the request does not have the arguments above, instead it <strong>passes to the next middleware stack</strong> （!）</li>
</ul>
<p>Besides, this Middleware does only store the return value of block call into <code>request.env["some key"]</code>, which means that <strong>it is used to "fetch" the Access Token</strong>, not used for checking whether the Access Token is valid and let the request complete. We have to check the validity of Access Token in the API layer.</p>
<p>So we install this Middleware but only use it to fetch the Access Token string:</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># api&#x2F;concerns&#x2F;api_guard.rb
included do
  # OAuth2 Resource Server Authentication
  use Rack::OAuth2::Server::Resource::Bearer, &#x27;The API&#x27; do |request|
    # The authenticator only fetches the raw token string

    # Must yield access token to store it in the env
    request.access_token
  end
end
</code></pre>
<h3 id="step-4-2-make-a-private-method-to-take-out-the-fetched-access-token-string">Step 4.2: Make a Private Method to Take Out the Fetched Access Token (String)</h3>
<p>I've mentioned above that the Middleware stores the Token in <code>request.env</code>. Actually it is stored in<code>request.env[Rack::OAuth2::Server::Resource::ACCESS_TOKEN]</code>. So let's take it out:</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># api&#x2F;concerns&#x2F;api_guard.rb
helpers do
  private
  def get_token_string
    # The token was stored after the authenticator was invoked.
    # It could be nil. The authenticator does not check its existence.
    request.env[Rack::OAuth2::Server::Resource::ACCESS_TOKEN]
  end
end
</code></pre>
<h3 id="step-4-3-make-a-private-method-to-convert-token-string-to-instance">Step 4.3: Make a Private Method to Convert Token String to Instance</h3>
<p>Token String is simply a String, and we still have to find the actual Access Token instance in the data model. I read the logic in <code>doorkeeper_for</code> helper, and learned that I can invoke its <code>AccessToken.authenticate</code> directly, which would return an instance if found, nil if not found:</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># api&#x2F;concerns&#x2F;api_guard.rb
  helpers do
    private
    def find_access_token(token_string)
      Doorkeeper::AccessToken.authenticate(token_string)
    end
  end
</code></pre>
<h3 id="step-4-4-make-a-service-to-verify-access-token">Step 4.4: Make a Service to verify Access Token</h3>
<p>This service is built as a module called OAuth2::AccessTokenValidationService. I put it in app/services. It validates the token is not expired, not revoked, and has sufficient scopes. The "expired" and "revoked" are validated with Doorkeeper::AccessToken's built-in methods. The sufficiency of scopes validates whether the authorized scopes is equal to or more than the required scopes. The validator retruns one of the four constants defined in that module: <code>VALID</code>, <code>EXPIRED</code>, <code>REVOKED</code> and <code>INSUFFICIENT_SCOPE</code>.</p>
<p>I put <code>validate_access_token</code> helper in Grape Endpoint to make it easier to access, which directly return the result of validation, i.e. the four results mentioned above. The caller can then determine how to response according to the validation result:</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># api&#x2F;concerns&#x2F;api_guard.rb
helpers do
  private
  def validate_access_token(access_token, scopes)
    OAuth2::AccessTokenValidationService.validate(access_token, scopes: scopes)
  end
end
</code></pre>
<p>The easiest way to compare scope sufficiency is set comparison: if set of authorized scopes is a superset of set of required scopes, then the scope is sufficient; otherwise it is insufficient. However if you would like to implement a logic like "Scope A includes Scope B," you should use another algorithm.</p>
<p>Here is the simple set comparison algorithm:</p>
<ul>
<li>If no scopes required, then any Access Token has sufficient scopes. Returns true.</li>
<li>If there are scopes required, then compare the two set of scopes to see whether the set of authorized scopes is a superset of set of required scopes.</li>
</ul>
<p>Ruby comes with a built-in Set datastructure so that we can do this by converting Array to Set.</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># app&#x2F;services&#x2F;oauth2&#x2F;access_token_validation_service.rb
protected
def sufficent_scope?(token, scopes)
  if scopes.blank?
    # if no any scopes required, the scopes of token is sufficient.
    return true
  else
    # If there are scopes required, then check whether
    # the set of authorized scopes is a superset of the set of required scopes
    required_scopes = Set.new(scopes)
    authorized_scopes = Set.new(token.scopes)

    return authorized_scopes &gt;= required_scopes
  end
end
</code></pre>
<h3 id="step-4-5-make-the-guard-to-deny-request-without-valid-access-token">Step 4.5: Make the Guard to Deny Request without Valid Access Token</h3>
<p>Now we're going to build the <code>guard!</code> method to deny API uses.</p>
<p>In order to make the program flow more clear, an exception for each error situation is defined. We can handle these exceptions with <code>rescue_from</code> in Grape, or raise Rack::OAuth2 built-in exceptions directly in those exceptions.</p>
<p>Here is the logic:</p>
<ol>
<li>First, fetch the Token String
<ul>
<li>If no Token is given, it means that the Client does not know that it needs to present a token. Raise MissingTokenError.</li>
<li>According to the spec, the response should have status 401, without any detailed error message.</li>
</ul>
</li>
</ol>
<ul>
<li>If there is a Token, but but found in the database, Raise TokenNotFound.
<ul>
<li>According to the spec, the response should have status 401, with "Invalid Token Error."</li>
</ul>
</li>
<li>If the Token is found in the database, then verify it to see if it can be used to access the API: check if it is expired or revoked. If scopes are required, check the authorized scopes.
<ul>
<li>If the result is VALID, assign <code>@current_user</code> to the Resource Owner (User) bound to the Access Token.</li>
<li>Otherwise, raise respective exceptions.</li>
<li>According to the spec, if the validation failed due to insufficient scopes, the response should have status 403 with "Insufficient Scope Error," otherwise it should have 401 status code with "Invalid Token Error."</li>
</ul>
</li>
</ul>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># app&#x2F;api&#x2F;concerns&#x2F;api_guard.rb
helpers do
  def guard!(scopes: [])
    token_string = get_token_string()

    if token_string.blank?
      raise MissingTokenError

    elsif (access_token = find_access_token(token_string)).nil?
      raise TokenNotFoundError

    else
      case validate_access_token(access_token, scopes)
      when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
        raise InsufficientScopeError.new(scopes)

      when Oauth2::AccessTokenValidationService::EXPIRED
        raise ExpiredError

      when Oauth2::AccessTokenValidationService::REVOKED
        raise RevokedError

      when Oauth2::AccessTokenValidationService::VALID
        @current_user = User.find(access_token.resource_owner_id)

      end
    end
  end
end
</code></pre>
<h3 id="step-4-6-forward-exceptions-to-the-exceptions-built-in-in-rack-oauth2">Step 4.6: Forward Exceptions to the Exceptions Built-in in Rack::OAuth2</h3>
<p>I implemented this by capturing all the exceptons with <code>rescue_from</code> in Grape. You can directly raise it, too.</p>
<p>Notes that:</p>
<ul>
<li>There's a set of <code>error_description</code> strings in Bearer::ErrorMethods, each <code>error</code> code has a corresponding description string.
<ul>
<li>Howerver they're only filled-in automatically when the exceptions are raised from Rack authenticator with corresponding methods (e.g. <code>insufficiet_scope!</code>.)</li>
<li><strong>They're not filled-in if we call the error responder middlewares directly</strong></li>
<li>So we have to manually fill them in.</li>
</ul>
</li>
<li>If the error is due to not presenting a token, we can assume that Client does not know that it has to authenticate.
<ul>
<li>So we don't use any <code>error</code> code that is defined in the spec.</li>
<li><code>error_description</code> can be obmitted too.</li>
<li>Respond 401 with Bearer::Unauthorized middleware.</li>
</ul>
</li>
<li>If the error is due to token not found, expired or revoked, use <code>invalid_token</code> for <code>error</code> code.
<ul>
<li>Actually we can use the same <code>error_description</code> string.</li>
<li>In my implementation, I raise different Exceptions and different <code>error_description</code>s for different errors.</li>
<li>You can use the same error description in your implementation, which still fulfills the requirements of spec.</li>
<li>Respond 401 with Bearer::Unauthorized middleware.</li>
</ul>
</li>
<li>If the error is due to insufficent scope of the token scope, use <code>insufficient_scope</code> for <code>error</code> code.
<ul>
<li>Respond 403 with Bearer::Forbiddden middleware.</li>
<li>However, in the implmentation of Rack::OAuth2, it does not respond with <code>WWW-Authenticate</code> header (actually it is only required in 401 response)</li>
<li>All error message including <code>scope</code>, will be found in JSON response body.</li>
<li>I've implemented <a href="https://github.com/yorkxin/rack-oauth2/tree/scope-error-params">a fork</a> that fills error messages in the <code>WWW-Authenticate</code> header.</li>
</ul>
</li>
<li>My implementation does not care about <code>error_uri</code> and <code>realm</code>; the <code>realm</code> will be fallen back to Rack::OAuth2's default one.</li>
</ul>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># app&#x2F;api&#x2F;concerns&#x2F;api_guard.rb
included do |base|
  install_error_responders(base)
end

# ...

module ClassMethods
  private
  def install_error_responders(base)
    error_classes = [ MissingTokenError, TokenNotFoundError,
                      ExpiredError, RevokedError, InsufficientScopeError]
    base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler
  end

  def oauth2_bearer_token_error_handler
    Proc.new {|e|
      response = case e
        when MissingTokenError
          Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new

        when TokenNotFoundError
          Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
            :invalid_token,
            &quot;Bad Access Token.&quot;)
        # etc. etc.
        end

      response.finish
    }
  end
end
</code></pre>
<h3 id="step-4-7-make-a-guard-for-the-whole-api">Step 4.7: Make a Guard for the whole API</h3>
<p>This usage is copied from <code>doorkeeper_for :all</code>, which is used to "require OAuth 2 Token for all the endpoints under this API." For Grape, it should be implmented as a class method in <code>Grape::API</code> class, so I put it in <code>ClassMethods</code> module. Call it in Grape::API and a <code>before</code> filter will be inserted; all the endpoints will go through that filter.</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># app&#x2F;api&#x2F;concerns&#x2F;api_guard.rb
module ClassMethods
  def guard_all!(scopes: [])
    before do
      guard! scopes: scopes
    end
  end
end
</code></pre>
<h3 id="step-4-8-now-we-can-lock-up-the-api-with-oauth-2">Step 4.8: Now We Can Lock Up the API with OAuth 2</h3>
<p>Requiring OAuth 2 on a single Endpoint:</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># app&#x2F;api&#x2F;v1&#x2F;sample_api.rb
module V1
  class SampleAPI &lt; Base
    get &quot;secret&quot; do
      guard! # Requires a valid OAuth 2 Access Token to use this Endpoint
      { :secret =&gt; &quot;only smart guys can see this ;)&quot; }
    end
  end
end
</code></pre>
<p>Requiring OAuth 2 on all the endpoints under an API:</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># app&#x2F;api&#x2F;v1&#x2F;secret_api.rb
module V1
  class SecretAPI &lt; Base
    guard_all!  # Requires a valid OAuth 2 Access Token to use all Endpoints

    get &quot;secret1&quot; do
      { :secret1 =&gt; &quot;Hi, #{current_user.email}&quot; }
    end

    get &quot;secret2&quot; do
      { :secret2 =&gt; &quot;only smart guys can see this ;)&quot; }
    end
  end
end
</code></pre>
<h3 id="try-it-out">Try It Out!</h3>
<p>Request to the endpoint without a valid token will be rejected:</p>
<pre><code>$ curl -i http:&#x2F;&#x2F;localhost:9999&#x2F;api&#x2F;v1&#x2F;secret&#x2F;secret1.json
HTTP&#x2F;1.1 401 Unauthorized
WWW-Authenticate: Bearer realm=&quot;The API&quot;
Content-Type: application&#x2F;json
Cache-Control: no-cache

{&quot;error&quot;:&quot;unauthorized&quot;}
</code></pre>
<p>If we present a valid token, it will pass, and tells me whom the user of that token is:</p>
<pre><code>$ curl -i http:&#x2F;&#x2F;localhost:9999&#x2F;api&#x2F;v1&#x2F;secret&#x2F;secret1.json \
&gt; -H &quot;Authorization: Bearer a14bb554309df32fbb6a3bad6cba25f32a28acc931a74ead06ca904c05281b4c&quot;
HTTP&#x2F;1.1 200 OK
Content-Type: application&#x2F;json
Cache-Control: max-age=0, private, must-revalidate

{&quot;secret1&quot;:&quot;Hi, ducksteven@gmail.com&quot;}
</code></pre>
<h2 id="step-5-using-scope">Step 5: Using Scope</h2>
<p>So far, the OAuth 2 Guard supports "scopes" but the Authorization Server does not support it. How do we restrict some API endpoints must be accessed with an Access Token that was authorized with <em>specific scopes</em>? Here is how. For more details you can read the document of Doorkeeper: <em><a href="https://github.com/applicake/doorkeeper/wiki/Using-Scopes">Using Scopes</a></em>. Also check out the source code in  <a href="https://github.com/yorkxin/oauth2-api-sample/tree/step-5">step-5 tag</a>.</p>
<p>First, add scope declarations in <code>config/initializers/doorkeeper.rb</code>:</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># config&#x2F;initializers&#x2F;doorkeeper.rb

# Define access token scopes for your provider
# For more information go to https:&#x2F;&#x2F;github.com&#x2F;applicake&#x2F;doorkeeper&#x2F;wiki&#x2F;Using-Scopes
default_scopes  :public              # If the Client does not ask for any scopes, use these scopes
optional_scopes :top_secret,         # Other scopes that the Client can ask for
                :el, :psy, :congroo
</code></pre>
<p>Don't forget to restart the Rails server.</p>
<p>However if you open "Authorize" link, the URL does not contain <code>scope</code> parameter, which means "the scopes to request." So we have to modify it manually first. Add <code>scope=top_secret</code> parameter, like this:</p>
<pre><code>http:&#x2F;&#x2F;localhost:9999&#x2F;oauth&#x2F;authorize
    ?client_id=4a407c6a8d3c75e17a5560d0d0e4507c77b047940db6df882c86aaeac2c788d6
    &amp;redirect_uri=http%3A%2F%2Flocalhost%3A12345%2Fauth%2Fdemo%2Fcallback
    &amp;response_type=code
    &amp;scope=top_secret
</code></pre>
<p>It'll ask you whether to authorize or deny, again. So you know that Authorization Server understands whether you have granted the client with specific scopes in the past. After authorization, the client will get another grant code, which can be exchanged for the Access Token, for example I got <code>5d840a4e43049eb1e66367bc788059f9bf16b53f853f3cd4f001e51a5c95abfd</code> this time.</p>
<p>Now add 2 endpoints in SampleAPI that require specific scopes to access:</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby">get &quot;top_secret&quot; do
  guard! scopes: [:top_secret]
  { :top_secret =&gt; &quot;T0P S3CR37 :p&quot; }
end

get &quot;choice_of_sg&quot; do
  guard! scopes: [:el, :psy, :congroo]
  { :says =&gt; &quot;El. Psy. Congroo.&quot; }
end
</code></pre>
<p>If we access <code>top_secret</code> endpoint with the previous Access Token, it will reject:</p>
<pre><code>$ curl -i http:&#x2F;&#x2F;localhost:9999&#x2F;api&#x2F;v1&#x2F;sample&#x2F;top_secret.json \
&gt; -H &quot;Authorization: Bearer a14bb554309df32fbb6a3bad6cba25f32a28acc931a74ead06ca904c05281b4c&quot;
HTTP&#x2F;1.1 403 Forbidden
Content-Type: application&#x2F;json
Cache-Control: no-cache

{
  &quot;error&quot;:&quot;insufficient_scope&quot;,
  &quot;error_description&quot;:&quot;The request requires higher privileges than provided by the access token.&quot;,
  &quot;scope&quot;:&quot;top_secret&quot;
}
</code></pre>
<p>However if we use the new token, it will pass:</p>
<pre><code>$ curl -i http:&#x2F;&#x2F;localhost:9999&#x2F;api&#x2F;v1&#x2F;sample&#x2F;top_secret.json \
&gt; -H &quot;Authorization: Bearer 5d840a4e43049eb1e66367bc788059f9bf16b53f853f3cd4f001e51a5c95abfd&quot;
HTTP&#x2F;1.1 200 OK
Content-Type: application&#x2F;json
Cache-Control: max-age=0, private, must-revalidate

{&quot;top_secret&quot;:&quot;T0P S3CR37 :p&quot;}
</code></pre>
<p>If we use the new token to access <code>choice_of_sg</code>, it won't pass:</p>
<pre><code>$ curl -i http:&#x2F;&#x2F;localhost:9999&#x2F;api&#x2F;v1&#x2F;sample&#x2F;choice_of_sg.json \
&gt; -H &quot;Authorization: Bearer 5d840a4e43049eb1e66367bc788059f9bf16b53f853f3cd4f001e51a5c95abfd&quot;
HTTP&#x2F;1.1 403 Forbidden
Content-Type: application&#x2F;json
Cache-Control: no-cache

{
  &quot;error&quot;:&quot;insufficient_scope&quot;,
  &quot;error_description&quot;:&quot;The request requires higher privileges than provided by the access token.&quot;,
  &quot;scope&quot;:&quot;el psy congroo&quot;
}
</code></pre>
<p>That's expected, because the scopes does not match. To access <code>choice_of_sg</code> endpoint, we have to get a new access token with required scopes. As mentioned before, we have to build an authorization URL:</p>
<pre><code>http:&#x2F;&#x2F;localhost:9999&#x2F;oauth&#x2F;authorize
    ?client_id=4a407c6a8d3c75e17a5560d0d0e4507c77b047940db6df882c86aaeac2c788d6
    &amp;redirect_uri=http%3A%2F%2Flocalhost%3A12345%2Fauth%2Fdemo%2Fcallback
    &amp;response_type=code
    &amp;scope=el%20psy%20congroo
             ^^^   ^^^ space
</code></pre>
<p>For multiple scopes, separate them with spaces <code>%20</code>.</p>
<p>This time I got a new token <code>0b39839282957d8f80c01901c2468ed52341707594897ec9767af392306f1e55</code>. If I access <code>choice_of_sg</code> with the new token, it will pass:</p>
<pre><code>curl -i http:&#x2F;&#x2F;localhost:9999&#x2F;api&#x2F;v1&#x2F;sample&#x2F;choice_of_sg.json \
-H &quot;Authorization: Bearer 0b39839282957d8f80c01901c2468ed52341707594897ec9767af392306f1e55&quot;
HTTP&#x2F;1.1 200 OK
Content-Type: application&#x2F;json
Cache-Control: max-age=0, private, must-revalidate

{&quot;says&quot;:&quot;El. Psy. Congroo.&quot;}
</code></pre>
<hr />
<h2 id="conclusion">Conclusion</h2>
<p>This tutorial only demos the simplest way to build an OAuth 2 Authorization Server and OAuth 2 Bearer Token Guard to protect Grape API, from scratch. Some features / issues are not covered:</p>
<ul>
<li>We have to restrict "who can register new clients", for example, only admins or logged-in users can register. I think this can be done by <code>admin_authenticator</code> and <code>enable_application_owner</code> options in Doorkeeper.</li>
<li>Did not demo the usage of Refresh Token.</li>
<li>Doorkeeper cannot disable some grant flows.
<ul>
<li>I implmented this feature in my fork, and I also posted a <a href="https://github.com/applicake/doorkeeper/pull/295">pull request</a>. Maybe the maintainer will merge it someday.</li>
</ul>
</li>
<li>Since the guard was built manually, some features in Doorkeeper are completely unused, for example:
<ul>
<li><code>access_token_methods</code> option (the methods which the client can present access tokens with.)</li>
<li>i18n configurations built-in in Doorkeeper.</li>
</ul>
</li>
<li>The guard does not use the same "realm" parameter in error resopnse.</li>
<li>The guard does not put parameters in <code>WWW-Authenticate</code> header for <code>insufficient_scope</code> error
<ul>
<li><a href="https://github.com/yorkxin/rack-oauth2/tree/scope-error-params">My fork</a> has implemented this feature, but I haven't post a pull request to the origin.</li>
<li>Although putting this header in 403 response doesn't make sense, I still feel it better to do this, since other error responses put this too.</li>
</ul>
</li>
<li>The scope matching is implemented with a simple set comparison, which is not useful for some cases like "A scope includes B scope."</li>
</ul>
<p>Finally, the Guard is not packaged as a gem. I think I may make a gem someday.</p>

            ]]></description>
        </item>
        <item>
            <title>北京印象</title>
            <link>https://blog.yorkxin.org/posts/beijing-image/</link>
            <pubDate>Thu, 31 Oct 2013 13:10:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/10/31/beijing-image.html</guid>
            
            <description><![CDATA[
                <p>最近剛從北京回來，一來去 RubyConf China ，一來去遊玩。我記下這次北京行的印象，與 Conf 無關的部份。</p>
<h2 id="ji-chang-yin-xiang">機場印象</h2>
<p>飛機降落在北京首都國際機機場的第三航廈 E 區。一下飛機，第一印象就是「高端大气上档次」（這句話要用簡體字寫比較有感覺），門面修整得非常漂亮，挑高的大廳讓人毫無壓力，萬年流行的玻璃帷幕包住整個航廈，跟桃園國際機場不是一個等級。中國政府應該是很重視門面，之後我注意到，連收費站都蓋得高端大气上档次。</p>
<p>入關的時候，大概是因為這裡專門停國際線班機，所以外國人的通道排得很長，中國公民的卻沒什麼人。懶得排隊，只好違背良心厚臉皮去排中國公民的通道……（其實服務人員說可以去排外國人那邊）。既然是國際線，怎麼不把中國公民的通道挪一些給外國人呢？我看到的是各一半。</p>
<p>入關不需要填入境卡，出境也不用填出境卡，不確定是不是台灣人才有的優待。不過這裡檢驗的人都是警察，一臉嚴肅貌，在台灣的卻是約聘公務員，且出境時的檢查也差很多，中國北京機場的海關會搜得很仔細，台灣桃園機場的安檢隨便搜，我皮夾手機沒拿出來嗶嗶嗶，安檢人員隨便掃了一下就照樣過關……。</p>
<p>入境的時候沒有拍到高端大气的照片，只在回程時拍了出境大廳的照片：</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574935876/"><img src="https://farm8.staticflickr.com/7344/10574935876_94409eb336_z.jpg" alt="" /></a></p>
<p>另外機場沒有把台灣歸類在「國內出發」裡面，我是有感到一點欣慰……。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574966314/"><img src="https://farm4.staticflickr.com/3820/10574966314_90fd562410_z.jpg" alt="" /></a></p>
<span id="continue-reading"></span>
<h2 id="di-tie-yin-xiang">地鐵印象</h2>
<p>做為一個鐵道迷，會對地鐵感興趣也是理所當然。</p>
<p>下飛機的地方是 E 區，取行李要去 C 區，得坐電車。這時我注意到非常熟悉的車廂、非常熟悉的車內配置、非常熟悉的無人駕駛、非常熟悉的軌道、非常熟悉的膠輪特有的晃動感，找了一下，的確找到了 Bombardier 的銘板（在地板上），果然跟台北捷運文湖線一樣是龐巴迪的系統。可惜因為趕路所以沒拍到照片就是了……。</p>
<p>我們坐機場快線去市區轉地鐵。然而機場快線坐上去其實不像是捷運，而是像台鐵的區間車，但又沒有像區間車那樣鐵皮。機場快線會從 3 號航廈跑到 2 號航廈，再跑回市區，運行的路線是三角形。這才意識到原來 2 號航廈和 3 號航廈離這麼遠。</p>
<p>在北京坐地鐵，進站是需要過安檢的，跟機場安檢一樣，你必須把行李放進 X 光機，但不需要搜身就是了。這是跟台灣最大的差別，我猜測這也是為什麼進站和出站的剪票口是在不同的角落，例如下圖，只有出站，沒有進站。有一天在下午五點左右看到湧入地鐵站的人潮，個個都要過安檢，就塞住了。之後去天安門廣場，過地下道也有同樣的安檢，實在是不知道你們在怕什麼？在台灣這種會被罵成「擾民」XD 。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574917816/"><img src="https://farm8.staticflickr.com/7319/10574917816_f9eca0454b_z.jpg" alt="" /></a></p>
<p>地鐵站設計基本上非常新潮，與台北捷運二期路網相比，有過之而無不及，去查了一下<a href="http://zh.wikipedia.org/wiki/%E5%8C%97%E4%BA%AC%E5%9C%B0%E9%90%B5">歷史</a>，才知道在 2000 年以前，北京地鐵只有 1 號線和 2 號線，之後的路線都是申辦奧運成功之後才開始狂蓋猛蓋的，難怪除了 1 號線和 2 號線之外，風格都非常新潮。而俗稱老線的 1 號線和 2 號線，看起來就像台鐵台北地下段。聽說蔣中正當年要把台北鐵路地下化，就是因為中共在蓋北京地鐵，所以也要來蓋一個「地鐵」來證明自己不落伍，當然從現代的眼光來看根本算不上地鐵就是了。</p>
<p>下圖是 6 號線東大橋站：</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574874295/"><img src="https://farm6.staticflickr.com/5485/10574874295_857485ed7d_z.jpg" alt="" /></a></p>
<p>下圖是 2 號線前門站月台，整個就是傳統鐵路的感覺：</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574793735/"><img src="https://farm8.staticflickr.com/7366/10574793735_1b3de46a74_z.jpg" alt="" /></a></p>
<p>下圖是 10 號線的月台，有整片的月台門：</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574731235/"><img src="https://farm8.staticflickr.com/7392/10574731235_a58e4961b6_z.jpg" alt="" /></a></p>
<p>很多路線都是 L 字形互相交會，此外還有兩個環線，2 號線和 10 號線，基本上分別跑二環和四環。讓我想到台北捷運也是刻意用 L 字型來做到各線之間盡量只有一次轉乘。</p>
<p>地鐵的電車似乎都是中國國產的，銘板都說明是中國本土的公司製造，在台灣要不是外國進口的（川崎重工、西門子）就是日本設計、<a href="http://zh.wikipedia.org/wiki/%E5%8F%B0%E7%81%A3%E8%BB%8A%E8%BC%9B">台灣車輛公司</a>製造。另外電車的隔音非常好，車外的噪音不會很大聲，噪音像是過彎時磨軌的聲音（台北捷運橘線古亭↔東門區間的巨大聲響即是）、馬達運作的聲音，都不會干擾到車廂裡面正常講話的聲音。附帶一提，北京地鐵也相當乾淨，裡面禁止飲食，我也沒看到有人違反這個規定。</p>
<p>車廂內的路線指示牌設計我也覺得很不錯，用燈號來表示之後會跑到哪些車站，過站的就不亮燈，並且因為中國的地鐵習慣以數字命名，所以不會像台北捷運那樣，硬是要把「往XX、OO」的轉乘指示塞進去的情況。附帶一提，它不會像台北捷運一樣，路線圖的方向與列車行走方向一致，例如在台北捷運裡，車子的前後方向是固定的，往上行的那一頭，就不會調頭往下行，所以基本上路線圖的起迄是跟列車行走方向一致，兩側門上方的路線圖是相反的圖片。然而在北京地鐵，我坐到的車，都沒有調整這個細節，也就是說左右兩側的門，用的圖是同一張，不會調整成跟列車方向一致。下圖是 4 號線的。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574864004/"><img src="https://farm8.staticflickr.com/7365/10574864004_dd81f64eff_z.jpg" alt="" /></a></p>
<p>地鐵票價是均一價人民幣 2 元，不管你坐多遠（機場快線除外），沒有看到任何票價表，買一張儲值卡 20 元押金 + 20 元儲值，跟台北的悠遊卡一樣。（是的我買了一張）</p>
<p>車上的電視，訊號應該是無線數位電視（根據馬賽克停格），但輸出的螢幕用的卻是類比訊號（根據雪花），這在北京地鐵的幾條線裡面都有發現這種情況，到底是為什麼呢……？</p>
<p>另外在市區裡的地鐵，還有神奇的窗外廣告，這種神奇的技術我不知道怎麼辦到的，如下圖。要是在台灣沒有專利的話，我實在覺得台北捷運應該也來搞一套。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574932294/"><img src="https://farm3.staticflickr.com/2866/10574932294_0b049bfa54_z.jpg" alt="" /></a></p>
<h2 id="jiao-tong-yin-xiang-qi-ta">交通印象（其他）</h2>
<p>交通高峰時段會一直塞車，有人笑稱首都北京是「首堵」，聽說還有下班時間塞兩個小時才能回到家的。地鐵也是非常塞，差不多是台北捷運藍線下班時間那麼塞。</p>
<p>路上的車輛會一直按喇叭，在台灣沒辦法想像，基本上就是一直叭叭叭，而且還會亂超車，非常兇。之前有個朋友說「如果聽到電話的背景音是喇叭聲，就知道對方是在中國大陸」，這下我相信了。</p>
<p>路舖得非常平，台北市當然沒辦法比。我想應該是北京做為首都所以投入很多資源顧門面吧，但至少有在顧門面。</p>
<p>有看到兩輛連結的公車，跟台中即將上路的 BRT 很神似。有看到路面電車，不是走軌道，但有電車線、集電弓。</p>
<p>路上看到的機車都是電動的，很少看到內燃機的，還有看到電動車專賣店。當下猜測是政府規定，果然沒錯，有人告訴我<a href="https://twitter.com/ethantw/status/393785704188088320">「四環以內禁騎摩托車」</a>。不過這裡的機車大部份沒有前後燈，也不用戴安全帽，晚上就是那種會隨時冒出來的東西，真的沒問題嗎……？</p>
<p><a href="http://zh.wikipedia.org/wiki/%E5%8C%97%E4%BA%AC%E5%9B%9B%E7%8E%AF%E8%B7%AF">四環光是一圈就要 65km</a> ，從會場（灯市口）拉車到 After Party 的場地（中關村），走四環路還要 1 個小時，可見北京比起台北有多大……。說到北京很大，有一天從頤和園坐地鐵要到三里屯，感覺就像從淡水到台北車站再換藍線到信義區那種感覺，當然北京地鐵走的更遠。</p>
<p>慢車道和快車道之間會有圍籬，腳踏車基本上騎在慢車道裡面，當然也有跟台灣一樣騎到人行道上面的，但似乎是極少數。大馬路的公車走在快車道上面，候車亭也在快車道旁，而不是在人行道上，這樣應該是可以避免像台灣那樣「騎機車要閃公車亂換車道」的情況。大馬路的人行道非常寬，人行道也有圍籬圍起來，同樣的設計之前在板橋、澳門有看到。</p>
<p>北京也有跟台北市 YouBike 一樣的公共自行車。</p>
<p>地攤在北京是真正的舖在路邊地上的攤子，都是坐在地上賣的，名符其實的「擺地攤」。</p>
<p>沒看到台灣那種騎樓，我猜是因為北京很少下雨，所以不需要騎樓這種設施。台灣的騎樓還兼俱人行道的功能，不過其實也都佔滿機車和攤販，想躲雨也沒得躲、想走路也要閃來閃去啊。</p>
<p>計程車很便宜。夜間的計程車，駕駛座會有擋板和防護罩，看來治安不太好啊。</p>
<p>另外路上常常見到垃圾以及很厚的沙塵……。</p>
<h2 id="kong-qi-wu-ran-yin-xiang">空氣污染印象</h2>
<p>一直聽說北京空氣不好，出發前我還很怕我的鼻子會過敏，不過到那邊才發現完全考慮錯方向，北京空氣差是「很多粉塵」，而且很乾燥，每天鼻屎都很厚而且還會流血，指甲與肉銜接的地方一直脫皮，臉和嘴唇也一直是乾燥脫皮的。</p>
<p>粉塵嚴重到什麼程度呢？在台灣冬天的早晨會有霧，能見度不高，太陽出來的時候就會散開，但在北京，粉塵嚴重到全天都是霧霧的能見度，連對街的交通號誌都看不清楚，路邊停的車子個個都積上一層灰塵。我直到這時才真正體會到北京的空氣污染有多嚴重。不過離開北京的當天，空氣倒是很清新。下圖的顆粒不是因為 ISO 太高，我想表達的是粉塵很多，右邊路邊的車輛也是積了一層灰。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574932886/"><img src="https://farm4.staticflickr.com/3818/10574932886_7bda24b284_z.jpg" alt="" /></a></p>
<p>十月底的北京倒不至於寒冷，沒有台北冷氣團來襲時那麼冷，因為北京是乾冷，台北是溼冷。白天沒那麼冷，所以我的穿法是在台灣的秋裝，裡面穿衛生衣，加上一件很厚的羽絨外套，就可以了。相信我，因為我很怕冷。</p>
<h2 id="yu-yan-yin-xiang">語言印象</h2>
<p>一直以來我都只在大陸連續劇等電視節目裡面聽到對岸的口音，這次因為去參加 RubyConf China ，所以聽到了不同的口音，即使知道對方來自深圳、上海、杭州、山東、北京，我還是不太會區分哪個是哪個。但與台灣最明顯的差異是ㄓㄔㄕㄖ（zh-, ch-, sh-, r-）聲母的翹舌音非常明顯，ㄥ（-eng）韻母的讀法也跟台灣不一樣。而北京當地的口音，最明顯的就是ㄦ（-er）尾，很多詞後面會都有ㄦ尾，比如「沒事」會說成「沒 sher」（跟事ㄦ分開不一樣，有點像是後面的ㄦ取代前面音的空韻母）。</p>
<p>在台灣我們稱為「國語」的普通話，其實不管怎麼練「字正腔圓」都不會像中國的普通話那樣標準，相聲演員也不像，反而眷村裡的榮民唸得還比較像。不過我要強調我並不推崇完全標準的國語或普通話，能夠溝通就行了，廣播也就算了，在日常生活這麼強調標準音，反而是一種病態。這個觀點也適用於英語。最近馬英九亂講話要新移民媽媽「不要教小孩子英語，因為會有口音」引發反彈，我想立報這篇〈<a href="http://www.lihpao.com/?action-viewnews-itemid-134530">誰是標準？關於新移民教小孩中文／英文／母語</a>〉的觀點很符合我的心態。</p>
<p>北京當地回應「謝謝」的方法有「沒事兒」、「沒關係」、「不謝」，「沒事兒」是最常聽到的，反而台灣的「不客氣」不常聽到。</p>
<p>稱計程車司機為「師傅」，稱廚師也是「師傅」，叫餐廳的服務生時要用丹田的力量大聲喊「服務員ㄦ！」。附帶一提如果剛從台灣過去，可能會稱「小姐」，但實際上不可以稱女性為「小姐」，一般的場合是稱「女士」，在大陸，「小姐」指的是某種性服務。</p>
<p>關於繁體字，一位大陸朋友跟我說，基本上看得懂繁體字，因為沒辦法避免，一定會看到。書法家寫繁體字，招牌想要仿古也會用繁體字。招牌寫繁體字的話，政府會視為圖形，所以不強制繁體字，註冊的文字簡體就行了。遇到看不懂的，按一下翻譯程式、查個字典也就知道了，頂多是習慣一下兩岸不同的用詞（指資訊科技相關的）。有些人也喜歡繁體字超過簡體字。我想這就是馬英九所謂的「識繁書簡」吧。在台灣，因為簡體字不在日常生活使用（會寫俗體字），所以一般人很少會在生活中接觸到，在網路上比較常會接觸到。然而有些人對簡體字有排斥甚至仇恨心態，我會說只要當作不同的語言就好了，而且這個語言非常輕鬆就能學會，甚至簡體字寫成的知識比繁體字多很多（特別是資訊科技相關的），實在不需要堅持那個意識型態。</p>
<p>另外我覺得大陸那邊人講普通話都是用丹田在發聲，台灣人很多是用喉嚨發聲的，有些人「說話宏亮」感覺就很像他們講話的方式。</p>
<h2 id="shi-wu-yin-xiang">食物印象</h2>
<p>早餐都是吃飯店的 buffet ，雖然不錯，但卻有點遠離真正北京在地的飲食習慣。</p>
<p>外食有吃到老北京的炸醬麵加合菜，炸醬麵本身是不錯，只是特大碗（海碗居），但老北京合菜口味卻很重。話說我還蠻喜歡豆汁的，那是一種喝起來很像酸菜湯的湯，發酵原理很類似，不過我們對於這些合菜都不太能接受。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574809444/"><img src="https://farm4.staticflickr.com/3793/10574809444_98a8be2357_z.jpg" alt="" /></a></p>
<p>還吃到東來順的羊肉爐，肉質很棒，只是有點騷味。後來才知道台北也有名叫東來順的涮羊肉餐廳，只是一時找不到資料（愛評網上面好像沒有），不知道是不是北京開過來的正版。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574795286/"><img src="https://farm3.staticflickr.com/2875/10574795286_0a0397b00a_z.jpg" alt="" /></a></p>
<p>還去全聚德吃到正統的北京烤鴨，的確是鮮嫩多汁，脆皮入口即化，鴨骨熬的湯也非常棒。（有遇到天安門爆炸事件，詳下文）</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574780355/"><img src="https://farm4.staticflickr.com/3788/10574780355_05f79e66b9_z.jpg" alt="" /></a></p>
<p>還去吃海底撈火鍋，這間店很神奇，服務非常到位，不是指服務態度（這種要學很容易），而是真正貼心地知道你要什麼：</p>
<ul>
<li>候位時有樸克牌和五子棋、乾果飲料給你打發時間</li>
<li>一坐定，就幫你把外套罩起來、給你圍巾，避免吸到味道</li>
<li>還發給手機用的夾鏈袋，而且套上去之後還是可以滑手機</li>
<li>還發給擦眼鏡的眼鏡布</li>
</ul>
<p>特別好吃的東西是「滑」，相當於台灣的「漿」，例如蝦滑就是蝦漿。滑有專門的沾醬，口味有微微的辣，剛好襯托出滑的鮮味，非常棒。醬料台最大的差別是沒有「沙茶醬」這種東西（東南亞才有），有一種叫做「海鮮醬」的沾醬，味道也很棒。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574890845/"><img src="https://farm4.staticflickr.com/3779/10574890845_29517c1b34_z.jpg" alt="" /></a></p>
<p>有一天半夜還去了簋街（讀如「鬼街」，感謝 jjgod 指正），這個地方很神奇，到午夜還是燈火通明，有非常多的熱炒店。我們去的店是賣龍蝦的，小小隻，很好吃，但就是非常非常鹹。我們還點了皮蛋豆腐，這裡的皮蛋豆腐跟台灣不一樣，必須攪散才能吃，不然會吃到鹽巴顆粒。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574759265/"><img src="https://farm6.staticflickr.com/5494/10574759265_c8b721baf6_z.jpg" alt="" /></a></p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574824284/"><img src="https://farm4.staticflickr.com/3703/10574824284_410c6070a8_z.jpg" alt="" /></a></p>
<p><a href="http://www.flickr.com/photos/chitsaou/10575039563/"><img src="https://farm6.staticflickr.com/5493/10575039563_5e6a80db30_z.jpg" alt="" /></a></p>
<p>北京當地的 Conf 志願者說，老北京的菜主要來自於蒙古人、回族（穆斯林）、有錢人吃剩下不想吃的食物，像是牛雜。話說回來，吃了幾間餐廳都沒有「豬肉」這種食物，只有牛肉和雞肉，甚至北京首都機場還有清真餐廳，應該可以說明穆斯林在這裡某種程度上影響了北京的飲食。</p>
<p>在這裡的飲用水，跟台灣一樣要去外面買瓶裝的，當地人最推薦的品牌是農夫什麼的，紅色標籤，但喝起來還是有奇怪的味道。不過很便宜就是了。</p>
<p>柑橘類都吃到很小顆的，大概比奇異果再小一點，跟台灣常見的蘋果大小的不同。不曉得是不是這個季節盛產。</p>
<p>蕃茄叫做西紅柿。百香果叫做西番蓮。土豆指的是馬鈴薯不是花生。</p>
<p>泡麵裡面會有叉子。</p>
<p>台灣馳名商標（？）也有開到北京，例如<a href="http://www.flickr.com/photos/chitsaou/10574792126/">此圖的 85℃ 、鮮芋仙</a>，還有看到 CoCo 茶飲。當然做為一個台灣人是不會在中國吃這些店的。</p>
<h2 id="tian-an-men-yin-xiang">天安門印象</h2>
<p><a href="http://zh.wikipedia.org/wiki/%E5%A4%A9%E5%AE%89%E9%97%A8%E5%B9%BF%E5%9C%BA">天安門廣場</a>非常非常非常大，附近很多嚴肅的建築，像是毛主席紀念館、政府機關、與社會主義有關的建築物等等。以前看照片不覺得廣場有多大，真正去那邊才發現非常大。毛主席紀念館是其中最大的一個建築，南北的門兩側各有兩個社會主義風格的雕像，後來查維基百科，<a href="http://zh.wikipedia.org/wiki/%E4%B8%AD%E5%8D%8E%E9%97%A8_(%E5%8C%97%E4%BA%AC)">原址是北京城的大清門</a>（民國後改叫中華門），可惜 1954 的時候被拆掉了，後來這空地用來蓋毛主席紀念館。</p>
<p>靠近天安門那邊有<a href="http://zh.wikipedia.org/wiki/%E4%BA%BA%E6%B0%91%E8%8B%B1%E9%9B%84%E7%BA%AA%E5%BF%B5%E7%A2%91">人民英雄紀念碑</a>，上面刻的是繁體字，是中共建政不久後就立下的，然而因為被鐵鏈圍住無法進入，所以實際上是看不到上面寫什麼的，被圍起來的原因可以查一下維基百科。</p>
<p>天安門廣場與天安門之間有一條大馬路，叫做<a href="http://zh.wikipedia.org/wiki/%E9%95%BF%E5%AE%89%E8%A1%97">長安街</a>，維基百科上面沒寫多寬，根據朋友數的結果，單側就有 7 線道。</p>
<p>天安門廣場南邊有兩個門，一個是<a href="http://zh.wikipedia.org/wiki/%E6%AD%A3%E9%98%B3%E9%97%A8">正陽門</a>，一個是前門，前門和正陽門之間有一個半圓，我猜這是當時北京城的甕城。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10575050483/"><img src="https://farm6.staticflickr.com/5498/10575050483_c069001350_z.jpg" alt="" /></a></p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574809546/"><img src="https://farm4.staticflickr.com/3762/10574809546_c0ec47c06c_z.jpg" alt="" /></a></p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574767635/"><img src="https://farm8.staticflickr.com/7333/10574767635_1c28e4bb6a_z.jpg" alt="" /></a></p>
<h3 id="tian-an-men-bao-zha-shi-jian">天安門爆炸事件</h3>
<p>我和同行的伙伴在 10 月 28 日去逛天安門廣場，本來想再去故宮，也就是天安門，但肚子餓了就去全聚德吃烤鴨。根據照片的時間，進門吃烤鴨的時間是 11:50 。吃的時候沒有注意到外面發生了什麼事，只覺得為什麼中午人這麼少，難道是星期一的緣故嗎。</p>
<p>出餐廳的時候，服務員說了一句「出去往南邊走，你們現在也只能往那邊走」，指著有一群人被圍在外面的柵欄處，幾個武警守在柵欄外。出了柵欄，回頭一看，整個廣場裡面都沒有人車，外面都圍起來了。似乎是發生什麼事了？定時封閉？有活動？有大官出巡？但是問當地人也說不知道為什麼。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574785675/"><img src="https://farm8.staticflickr.com/7447/10574785675_459cd9fa07_z.jpg" alt="" /></a></p>
<p>看起來暫時不能去故宮，一行人就只好去搭地鐵前往頤和園。在地鐵上，朋友上了 Facebook ，才知道<a href="http://www.appledaily.com.tw/realtimenews/article/new/20131028/282592/">天安門發生爆炸事件</a>（蘋果日報），聽到這事我和我的伙伴們都驚呆了。後來更詳細的新聞是 5 死、數十人受傷。</p>
<p>我們閃過這一劫真是上天保佑……。</p>
<h2 id="yi-he-yuan-yin-xiang">頤和園印象</h2>
<p>頤和園很漂亮，裡面的遺跡可以讓我想像到當年做為慈禧太后後花園的風光歲月。印象最深的是仿西藏風格的建築（稱四大部洲）、超級長的長廊（而且廊上的每個圖都不一樣）、超級大的湖、寺裡的巨大佛像、稱為「智慧海」的佛殿（外面有1001個佛像的雕刻）。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574851316/"><img src="https://farm3.staticflickr.com/2814/10574851316_25c6270afc_z.jpg" alt="" /></a></p>
<p>一進門就會看到一個碑，上面寫著 1860 年英法聯軍燒了頤和園。而導遊在帶的時候也是先提這件事，木造的建築幾乎全毀，沒燒掉的都是石造的，再強調現在看到的是市政府近年重修的。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10575094723/"><img src="https://farm3.staticflickr.com/2848/10575094723_0447c14551_z.jpg" alt="" /></a></p>
<p>過程中，除了帶上慈禧太后在這裡的故事，也一直提到「被英法聯軍燒毀」。沒被燒掉的像是石橋、智慧海，但智慧海外面的 1001 尊小佛像是嵌在牆上的，頭被列強砍走，現在看到的是近代重修，重新雕上去的頭（被列強砍走是導遊說的，後來查維基百科說是文革時摧毀的）。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574866266/"><img src="https://farm8.staticflickr.com/7305/10574866266_48426145b8_z.jpg" alt="" /></a></p>
<p>然而我一直被導遊混淆視聽，因為光是「被燒毀」無法解釋為什麼有這麼多老舊的木造建築（涼亭、長廊等），後來去查維基百科才，才知道慈禧太后在 1880 年代挪用海軍軍費修整出來的頤和園，這麼多木造建築應該都是在那時候興建的。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574900524/"><img src="https://farm3.staticflickr.com/2808/10574900524_3f9c0e300f_z.jpg" alt="" /></a></p>
<p>雖然沒有背景知識有點吃虧，但總的來說頤和園是很漂亮的花園，此外若對歷史有興趣的話，還可以近距離接觸這些歷史，若對歷史沒興趣，欣賞清代建築、逛逛山水也很適合，非常值得一逛。我們這次只走了 2 個小時，即使有導遊還是只能算走馬看花，沒有完全逛完，估計要一整天才行。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574852435/"><img src="https://farm8.staticflickr.com/7381/10574852435_18903a755e_z.jpg" alt="" /></a></p>
<p>可惜的是沒能去看北大……聽說以前是王府。</p>
<p>附帶一提，凡是特別的地磚路，例如橋上正中央有一條路，那就是皇帝走的。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10575083533/"><img src="https://farm3.staticflickr.com/2847/10575083533_2880142579_z.jpg" alt="" /></a></p>
<h2 id="san-li-tun-yin-xiang">三里屯印象</h2>
<p>三里屯這個行程是專門要去 Apple Store 朝聖的，因為台灣沒有直營店，要去的話只能出國順便逛。</p>
<p>直營店乍看之下雖然跟台灣的某些經銷商差不多，也提供無壓力的試用，特別的地方是漂亮的裝潢（包括傳說中的透明樓梯），以及 Genius Bar ，一對一的服務，這種在台灣就完全沒有了。</p>
<p><a href="http://www.flickr.com/photos/chitsaou/10574884335/"><img src="https://farm6.staticflickr.com/5513/10574884335_3c70d5198b_z.jpg" alt="" /></a></p>
<p>Apple Store 坐落在三里屯的精品商圈裡面，附近除了電影院，還有 Uniqlo 、NIKE 等精品商店，常常遇到西洋人，附近還有很多酒吧，甚至有掮客在招攬。要比喻的話，就很像台北市的信義區商圈。但最大的不同是三里屯有一堆大使館（ｒｙ</p>
<p>對了，這裡的 Apple 產品賣得比台灣貴，要撿便宜不能去中國。Uniqlo 的價位倒是跟台灣差不多。</p>
<h2 id="conclusion">Conclusion</h2>
<ul>
<li>北京乍看之下很華麗，但細節的地方卻不那麼美好，例如雖然有高樓大廈，但卻常常看到垃圾。地攤我倒覺得沒什麼，反而這才是都市的活力。</li>
<li>空氣很糟，不適合我常住久居。但回來台北就不覺得台北空氣很髒了。</li>
<li>旅遊景點很多，歷史控可以來看看。</li>
<li>「招攬」特別有人情味，但要小心詐騙，像是聲稱帶你去哪裡多少錢，其實不是跳表的計程車，專門坑遊客。</li>
<li>物價跟台北差不多，甚至還高一點，尤其是外國品牌特別貴，像是 Apple Store 、星巴克、漢堡王。</li>
<li>人提供的服務就非常低廉，例如計程車。</li>
<li>便利商店不便利，飲料不會放冰箱（至少我去的那間）。</li>
<li>路上很多施工，半夜也施工，挖到一半就放置不管的比比皆是。台灣至少會圍起來。</li>
<li>最神奇的奇觀是在便利商店裡面有人孔蓋。</li>
<li>安檢安檢安檢，不知道政府在怕什麼。</li>
<li>北京感覺是砸很多錢去顧門面、整市容的地方，但總比台北懶得搞、亂搞來得好。</li>
<li>遇到的人大多很和善、很健談，非常值得交朋友。不曉得是不是因為是去參加技術研討會，當然我也希望中國人都很親切。</li>
<li>吸菸很普遍，連飯店都有可吸菸樓層，訂房的時候要注意一下。</li>
<li>沒逛到書店我表示很幹。</li>
</ul>
<hr />
<p>關於上網：中華電信的話可以事先預約開通漫遊上網，我買的方案是 3 天 999，也可以落地時依照自動收到的簡訊開通。用 3G 漫遊的話不會有 GFW ，可以上 Facebook 、 Twitter 等在中國大陸被封鎖的網站。</p>
<p>關於證件：台灣人去中國大陸要辦台胞證，上面寫「无业人员」是旅行社故意弄的，據說這樣比較不會被海關刁難；不過同行人是寫「技术人员」或「职员」，好像也沒怎樣……。至於中華民國護照，在 check-in 飛機的時候會用到（即使是在中國），過海關不會用到。</p>

            ]]></description>
        </item>
        <item>
            <title>OAuth 2.0 Tutorial: Grape API 整合 Doorkeeper</title>
            <link>https://blog.yorkxin.org/posts/oauth2-tutorial-grape-api-doorkeeper/</link>
            <pubDate>Thu, 10 Oct 2013 12:47:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/10/10/oauth2-tutorial-grape-api-doorkeeper.html</guid>
            
            <description><![CDATA[
                <p>這篇文章示範如何使用 OAuth 2 保護 API ，其中 API 是用 Grape 造出來的，掛在 Rails 底下。</p>
<p>英文版 / English Version: <a href="http://blog.yorkxin.org/posts/2013/11/05/oauth2-tutorial-grape-api-doorkeeper-en">OAuth 2.0 Tutorial: Protect Grape API with Doorkeeper</a></p>
<p>整個實作流程會造出這些東西：</p>
<ul>
<li><strong>Resource Owner</strong> - 可以授權給第三方 App 的角色，也就是 User。</li>
<li><strong>Authorization Server</strong> - 用來處理與 OAuth 2 授權有關的事務，像是：
<ul>
<li><strong>Clients</strong> - 需要有 Clients (Apps) 的 CRUD 。</li>
<li><strong>Access Token</strong> (Model) - 需要有個 Model 來儲存 Access Token 。</li>
<li><strong>Authorization Endpoint</strong> - 這裡來處理 Auth Code Grant 和 Implicit Grant 。</li>
<li><strong>Token Endpoint</strong> - 這裡來真正核發 Token 。</li>
</ul>
</li>
<li><strong>Resource Server</strong> - 給 App 存取的地方，也就是 API ，一部份需要 Access Token 才能存取的叫做 Protected Resource 。
<ul>
<li><strong>Resource Server 上面的 Guard</strong> - 用途是「保護某些 API ，必須要帶 Access Token 才能存取」，俗稱保全。</li>
</ul>
</li>
</ul>
<p>本文使用這些套件來實作：</p>
<ul>
<li>Resource Owner (User) - <strong><a href="https://github.com/plataformatec/devise">Devise</a></strong></li>
<li>Authorization Server (OAuth 2 Provider) - <strong><a href="https://github.com/applicake/doorkeeper">Doorkeeper</a></strong></li>
<li>Resource Server (API) - <strong><a href="https://github.com/intridea/grape">Grape</a></strong></li>
<li>Guard - 用 <strong><a href="https://github.com/nov/rack-oauth2">Rack::OAuth2</a></strong> 來整合 Grape</li>
</ul>
<p>因為 Doorkeeper 的 <code>doorkeeper_for</code> 只能用在 Rails ，而 Guard 只是一個 Rack Middleware ，所以這裡要自己拼湊。詳情請見先前的文章 <a href="http://blog.yorkxin.org/posts/2013/10/08/oauth2-ruby-and-rails-integration-review">〈Ruby / Rails 的 OAuth 2 整合方案簡單評比〉</a>。</p>
<p>所有過程我都會放在 <a href="https://github.com/yorkxin/oauth2-api-sample">chitsaou/oauth2-api-sample</a> 這個 repository ，各 step 有對應的 step-x tag ，例如 Step 1 完成的結果可以在 step-1 這個 tag 看到。</p>
<hr />
<span id="continue-reading"></span>
<h2 id="step-1-zao-resource-owner-luo-ji-user">Step 1: 造 Resource Owner 邏輯 (User)</h2>
<p>用 Devise 做。這個應該是 Rails Developer 的基本功，所以不解釋了，請見 <a href="https://github.com/yorkxin/oauth2-api-sample/tree/step-1">step-1 tag</a>。</p>
<p>可以試著打開 <code>/pages/secret</code> ，會要求登入。</p>
<h2 id="step-2-zao-resource-server-api">Step 2: 造 Resource Server (API)</h2>
<p>用 Grape 是因為不想要讓 API 經過太多 Rails 的 stack。</p>
<p>這個不難，而且不是本文的重點，所以直接看官方文件就好了。成品可以看 <a href="https://github.com/yorkxin/oauth2-api-sample/tree/step-2">step-2 tag</a> 。</p>
<h2 id="step-3-zao-authorization-server-provider">Step 3: 造 Authorization Server (Provider)</h2>
<p>既然底是 Rails ，那麼就直接上 Doorkeeper 就好了。可以看 <a href="https://github.com/yorkxin/oauth2-api-sample/tree/step-3">step-3 tag</a> 。</p>
<p><a href="http://railscasts.com/episodes/353-oauth-with-doorkeeper">RailsCasts 有 tutorial</a> ，若有買 Pro 不妨去看看。不過其實照官方文件做也不難：</p>
<p>安裝 Doorkeeper Gem</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># Gemfile
gem &#x27;doorkeeper&#x27;
</code></pre>
<p>別忘了跑 <code>bundle install</code> 。</p>
<p>然後跑這些來安裝：</p>
<pre><code>$ rails generate doorkeeper:install
$ rails generate doorkeeper:migration
$ rake db:migrate
</code></pre>
<p>再照他文件說的去接 Devise 來認證 Resource Owner：</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># config&#x2F;initializers&#x2F;doorkeeper.rb

# 認證 Resource Owner 的方法，直接接 Devise
resource_owner_authenticator do
  current_user || warden.authenticate!(:scope =&gt; :user)
end
</code></pre>
<p>這樣就好了。</p>
<p>Doorkeeper 會建這些 model:</p>
<ul>
<li><strong>OauthApplication</strong> - Clients 的註冊資料庫</li>
<li><strong>OauthAccessGrant</strong> - Auth Code 流程第一步產生的 Auth Grants 的資料庫</li>
<li><strong>OauthAccessToken</strong> - 真正核發出去的 Access Tokens，包含對應的 Refresh Token （預設關閉）</li>
</ul>
<p>Doorkeeper 開的 Routes 有這些：</p>
<table><thead><tr><th>Method (REST)</th><th>Path</th><th>用途</th></tr></thead><tbody>
<tr><td>new</td><td>/oauth/authorize</td><td>Authorization Endpoint</td></tr>
<tr><td>create</td><td>/oauth/authorize</td><td>User 許可 Authorization 時的 action</td></tr>
<tr><td>destroy</td><td>/oauth/authorize</td><td>User 拒絕 Authorization 時的 action</td></tr>
<tr><td>show</td><td>/oauth/authorize/:code</td><td>（應該是用來 Local 測試的）</td></tr>
<tr><td>update</td><td>/oauth/authorize</td><td>（不明的 update grant）</td></tr>
<tr><td>create</td><td>/oauth/token</td><td>Token Endpoint</td></tr>
<tr><td>show</td><td>/oauth/token/info</td><td>Token Debug Endpoint</td></tr>
<tr><td>resources</td><td>/oauth/applications</td><td>Clients 管理界面</td></tr>
<tr><td>index</td><td>/oauth/authorized_applications</td><td>Resource Owner 管理授權過的 Clients</td></tr>
<tr><td>destroy</td><td>/oauth/authorized_applications/:id</td><td>Resource Owner 管理授權過的 Clients</td></tr>
</tbody></table>
<p>其中 Authorization Endpoint 的 show 只會顯示 grant code ，可能是 Local Testing 要用的；而 update 則是沒有任何 action 去接它，不確定是不是 dead feature 。</p>
<p>可以發現到：</p>
<ul>
<li>幫你蓋好了 Authorization Endpoint 和 Token Endpoint</li>
<li>還附加 Token Debug Endpoint ，在 Implicit Flow 可以驗證 Token 的真實性。</li>
<li>還有附 Clients 管理界面</li>
<li>還可以讓 User 管理授權過的 Clients</li>
</ul>
<p>所以一個 Authorization Server 該有的東西它都提供了。</p>
<h3 id="step-3-1-kai-ce-shi-yong-de-client">Step 3.1: 開測試用的 Client</h3>
<p>蓋完 Authorization Server 之後，要去開一個 Client 。可以打開 <code>/oauth/applications</code> ，其中 Client 的 redierct URI 填入 <code>http://localhost:12345/auth/demo/callback</code> ，實際上沒有跑 Web server 在 localhost:12345 也沒關係，最終目的是拿到 code 或 token。</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2013/2013-10-10-oauth2-tutorial-grape-api-doorkeeper/1wLQZN9CS9SixjFgRaq1_oauth2-new-client.png" alt="oauth2-new-client.png" /></p>
<h3 id="step-3-2-na-qu-access-token">Step 3.2: 拿取 Access Token</h3>
<p>現在可以來試著拿 Access Token 了，我們要用人腦模擬 Client ，來跑 <a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-1-auth-code-grant-flow">Authorization Code Grant 的流程</a>。</p>
<p>步驟如下：</p>
<p>首先打開剛剛生的 Client 的 show 頁面，會看到有 Application ID 、 Secret 等資訊的頁面。最下面有一個 Authorize 的連結，點下去會打開到這個網址（假設這個 Rails App 開在 localhost:9999 ，下同）（中間斷行比較好讀）：</p>
<pre><code>http:&#x2F;&#x2F;localhost:9999&#x2F;oauth&#x2F;authorize
    ?client_id=4a407c6a8d3c75e17a5560d0d0e4507c77b047940db6df882c86aaeac2c788d6
    &amp;redirect_uri=http%3A%2F%2Flocalhost%3A12345%2Fauth%2Fdemo%2Fcallback
    &amp;response_type=code
</code></pre>
<p>就像之前在流程文裡介紹過的，它用 GET 去 Authorization Endpoint 求 Grant Code ，附上自己的 Client ID 和 Redirection URI 。</p>
<p>接著會問你要 Authorize 還是 Deny ，當然選 Authorize 。</p>
<p>接著會跑到一個瀏覽器打不開的網址，是先前設定的 Redirection URI，不過沒關係，我們已經得到 Grant Code 了（中間斷行比較好讀）：</p>
<pre><code>http:&#x2F;&#x2F;localhost:12345&#x2F;auth&#x2F;demo&#x2F;callback
    ?code=21e1c81db4e619a23d4ed46134884104225d4189baa005220bd9b358be8b591a
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
          Grant Code
</code></pre>
<p>如果在 Step 3.1 照網頁上的指示填入 <code>urn:ietf:wg:oauth:2.0:oob</code> ，最後會出現 grant code 的 show 頁面，直接把 grant code 曬給你，這是 local 用來測試用的，類似 <a href="https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi">Google OAuth 2.0 的流程</a>。</p>
<p>到這裡，Client 就拿到 Grant Code 了，依照流程，接下來是 Client 要另外從後台偷偷去 Authorization Server 把這個 Grant Code 換成 Access Token。</p>
<p>因為要填的資料太多了，我抓一下 <a href="http://www.getpostman.com/">Postman</a> 的畫面，填完最後按下「Send」就會拿到 Access Token 了！</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2013/2013-10-10-oauth2-tutorial-grape-api-doorkeeper/YZa3unuQgSf2kUkoSMDF_oauth2-token-request-zh.png" alt="oauth2-token-request-zh.png" /></p>
<h2 id="step-4-zao-resource-server-shang-de-guard">Step 4: 造 Resource Server 上的 Guard</h2>
<p>造 Guard 這件事比較難，就像我<a href="http://blog.yorkxin.org/posts/2013/10/08/oauth2-ruby-and-rails-integration-review">前一篇文章</a>說過的，在 API 是 Grape 的情況下， <strong>沒有一個 Guard 是可以直接拿來用的</strong> ；即使你用 Rails 做 API 好了，<code>doorkeeper_for</code> 現在也還只是半成品。我目前的做法是把 Rack::OAuth2 的 Bearer Token middleware 接到 Grape 上面，邏輯參考了 <code>doorkeeper_for</code> 的實作方式。</p>
<p>這裡會寫得比較仔細。可以看 <a href="https://github.com/yorkxin/oauth2-api-sample/tree/step-4">step-4 tag</a> 。</p>
<p>我寫成一個 module ，用 <a href="http://api.rubyonrails.org/classes/ActiveSupport/Concern.html">ActiveSupport::Concern</a> 去簡化 module 化的程式，放在 <code>api/concerns/api_guard.rb</code>。</p>
<h3 id="step-4-1-an-zhuang-rack-middleware-lai-zhua-qu-access-token-string">Step 4.1: 安裝 Rack Middleware 來抓取 Access Token (String)</h3>
<p>Rack::OAuth2 這個 Rack Middleware 在安裝 (<code>use</code>) 的時候要傳一個 block ，它會去 call ，但 call 的條件是「Request 有帶 OAuth 2 Token」這樣才會 call ，意思是說：</p>
<ul>
<li>如果 Request 有帶 <code>Authorization: Bearer XXX</code> 或 <code>?access_token=xxx</code> 才會 call</li>
<li>如果 Request 不帶上述的參數，就不會 call ，直接 <strong>pass 到下一個 middleware stack</strong> （！）</li>
</ul>
<p>而且這個 Middleware 在 call 之後其實會把 return value 直接存進 <code>request.env["某個 key"]</code> 裡面，意思就是說 <strong>「它只是給你 fetch access token 用的」</strong> ，不能拿來「確認 Access Token 有效並放行 API access」，這件事要在 API 層做。</p>
<p>那麼就來安裝這個 Middleware 吧，但只拿來 fetch access token string ：</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># api&#x2F;concerns&#x2F;api_guard.rb
included do
  # OAuth2 Resource Server Authentication
  use Rack::OAuth2::Server::Resource::Bearer, &#x27;The API&#x27; do |request|
    # The authenticator only fetches the raw token string

    # Must yield access token to store it in the env
    request.access_token
  end
end
</code></pre>
<h3 id="step-4-2-zuo-yi-ge-private-method-lai-qu-chu-xian-qian-na-dao-de-access-token-string">Step 4.2: 做一個 private method 來取出先前拿到的 Access Token (String)</h3>
<p>前文提到 Middleware 會把 Token 存在 <code>request.env</code> 裡面，具體就是 <code>request.env[Rack::OAuth2::Server::Resource::ACCESS_TOKEN]</code> ，所以就把它拿出來吧。</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># api&#x2F;concerns&#x2F;api_guard.rb
helpers do
  private
  def get_token_string
    # The token was stored after the authenticator was invoked.
    # It could be nil. The authenticator does not check its existence.
    request.env[Rack::OAuth2::Server::Resource::ACCESS_TOKEN]
  end
end
</code></pre>
<h3 id="step-4-3-zuo-yi-ge-private-method-lai-ba-token-string-bian-cheng-instance">Step 4.3: 做一個 private method 來把 Token String 變成 Instance</h3>
<p>Token String 只是單純的字串，還需要去 Database 裡面撈才能換成 Instance 。參考了 <code>doorkeeper_for</code> 的做法，我直接呼叫它的 <code>AccessToken.authenticate</code> ，撈不到會直接回 <code>nil</code>：</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># api&#x2F;concerns&#x2F;api_guard.rb
helpers do
  private
  def find_access_token(token_string)
    Doorkeeper::AccessToken.authenticate(token_string)
  end
end
</code></pre>
<h3 id="step-4-4-zuo-yi-ge-service-lai-yan-zheng-access-token-shi-fou-he-fa">Step 4.4: 做一個 service 來驗證 Access Token 是否合法</h3>
<p>OAuth2::AccessTokenValidationService 我放在 app/service 裡面，其中會驗證是否過期 (expired) 、是否被撤銷 (revoked) ，這兩個都是 Doorkeeper::AccessToken 內建的 methods。但此外還要驗證所需的 scopes 是否包含在 Access Token 的 scopes 裡面。回傳的驗證結果會是 <code>VALID</code> 、 <code>EXPIRED</code> 、 <code>REVOKED</code> 、 <code>INSUFFICIENT_SCOPE</code> 四個常數的其中一個（定義在該 module 裡面）。</p>
<p>在 Grape Endpoint 放一個 <code>validate_access_token</code> helper 來方便處理這件事，它會直接回傳結果，也就是上述四個之中的一個， caller 就可以根據驗證結果決定要怎麼回 response 。</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># api&#x2F;concerns&#x2F;api_guard.rb
helpers do
  private
  def validate_access_token(access_token, scopes)
    OAuth2::AccessTokenValidationService.validate(access_token, scopes: scopes)
  end
end
</code></pre>
<p>在 Service 裡面要驗證 scopes ，我的演算法其實很簡單，就是集合比較而已；有的網站會有「A scope 包含 B scope」的設計，如果要做成這樣的話，就不能用單純的集合比較了。純集合比較的演算法是這樣：</p>
<ul>
<li>如果沒有要求任何 scopes ，那其實任何 Access Token 都符合，就回 true。</li>
<li>如果有要求任何 scopes ，那麼「授權過的 scopes」就得是「所需的 scopes」的宇集，剛好 Ruby 有內建 Set 這個資料結構，把兩個 Array 都轉成 Set 就能方便比較了。</li>
</ul>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># app&#x2F;services&#x2F;oauth2&#x2F;access_token_validation_service.rb
protected
def sufficent_scope?(token, scopes)
  if scopes.blank?
    # if no any scopes required, the scopes of token is sufficient.
    return true
  else
    # If there are scopes required, then check whether
    # the set of authorized scopes is a superset of the set of required scopes
    required_scopes = Set.new(scopes)
    authorized_scopes = Set.new(token.scopes)

    return authorized_scopes &gt;= required_scopes
  end
end
</code></pre>
<h3 id="step-4-5-zhi-zuo-guard-lai-dang-zhu-mei-you-he-fa-access-token-de-requests">Step 4.5: 製作 Guard 來擋住沒有合法 Access Token 的 Requests</h3>
<p>現在要真正寫 <code>guard!</code> method 來擋 API use 了。</p>
<p>為了讓程式流程看起來更簡潔，根據不同的錯誤情況，定義了不同的 Exception ，各 Exception 要怎麼處理，則可以交由 Grape 的 <code>rescue_from</code> 處理（我是這樣做的），或 Exception 裡面直接 raise Rack::OAuth2 內建的 exception。</p>
<p>邏輯是這樣：</p>
<ol>
<li>先去抓出 Token String
<ul>
<li>如果沒給 Token ，表示 Client 不知道要認證，丟 MissingTokenError</li>
<li>照 spec 是要回 401 但是不給任何錯誤訊息</li>
</ul>
</li>
</ol>
<ul>
<li>如果有給 Token 但是資料庫裡面找不到，丟 TokenNotFound
<ul>
<li>照 spec 是要回 401 加上 Invalid Token Error</li>
</ul>
</li>
<li>如果找得到 Token 則進一步驗證是否可以用來存取該 API （根據有否過期、被撤銷，如果有要求 scope 的話則再檢查 scope）
<ul>
<li>若驗證結果是 VALID ，則把 <code>@current_user</code> 指定給該 Access Token 綁定的 Resource Owner (User)</li>
<li>若驗證結果不是 VALID ，則丟出相對應的 Exceptions</li>
<li>照 spec ，如果是因為 scope 不足，則是回 403 加上 Insufficient Scope Error ，其他情況則是要回 401 加上 Invalid Token Error</li>
</ul>
</li>
</ul>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># app&#x2F;api&#x2F;concerns&#x2F;api_guard.rb
helpers do
  def guard!(scopes: [])
    token_string = get_token_string()

    if token_string.blank?
      raise MissingTokenError

    elsif (access_token = find_access_token(token_string)).nil?
      raise TokenNotFoundError

    else
      case validate_access_token(access_token, scopes)
      when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
        raise InsufficientScopeError.new(scopes)

      when Oauth2::AccessTokenValidationService::EXPIRED
        raise ExpiredError

      when Oauth2::AccessTokenValidationService::REVOKED
        raise RevokedError

      when Oauth2::AccessTokenValidationService::VALID
        @current_user = User.find(access_token.resource_owner_id)

      end
    end
  end
end
</code></pre>
<h3 id="step-4-6-ba-exception-zhuan-song-dao-rack-oauth2-nei-jian-de-cuo-wu-hui-ying-fang-shi">Step 4.6: 把 Exception 轉送到 Rack::OAuth2 內建的錯誤回應方式</h3>
<p>我的做法是用 Grape 的 <code>rescue_from</code> 去接 exceptions ，當然要直接 raise 也可以就是了。</p>
<p>要注意的是：</p>
<ul>
<li>Bearer::ErrorMethods 有內建一組 <code>error_description</code> 的預設值，根據不同的 <code>error</code> code 去對應
<ul>
<li>但只有在 Rack 的 authenticator 裡面使用相對應的 helper method (如 <code>insufficiet_scope!</code>) 才會填入</li>
<li><strong>直接 call 這個 middleware 則不會自動填入錯誤訊息</strong></li>
<li>所以必須手動填入</li>
</ul>
</li>
<li>沒給 Token 要視為「Client 不知道要 Authenticate」
<ul>
<li>所以 <code>error</code> code 不屬於 Spec 裡面定義的任何一個</li>
<li><code>error_description</code> 也不需要給。</li>
<li>使用 Bearer::Unauthorized 回 401</li>
</ul>
</li>
<li>Token 找不到、過期 (Expired) 、被撤銷 (Revoked) 的 <code>error</code> code 都是 <code>invalid_token</code>
<ul>
<li>其實可以用同一個 <code>error_description</code></li>
<li>我的寫法會把三種情況分別丟不同的 Exception，並填入不同的 <code>error_description</code></li>
<li>你實作的時候可以用同一個，這並不會違反 spec</li>
<li>使用 Bearer::Unauthorized 回 401</li>
</ul>
</li>
<li>Token 的 scope 不足會使用 <code>insufficient_scope</code> 的 <code>error</code>
<ul>
<li>使用 Bearer::Forbidden 回 403</li>
<li>可是 Rack::OAuth2 的實作並沒有填入 <code>WWW-Authenticate</code> header （只有 401 強制要求要填）</li>
<li>所有的 error message （包括 <code>scope</code>）會出現在 JSON response body 裡面</li>
<li>我有<a href="https://github.com/yorkxin/rack-oauth2/tree/scope-error-params">另外實作一個 fork</a> ，會一併填入 <code>WWW-Authenticate</code> 裡面</li>
</ul>
</li>
<li>這個實作沒有填入 <code>error_uri</code> 和 <code>realm</code> ，其 <code>realm</code> 會使用 Rack::OAuth2 內建的。</li>
</ul>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># app&#x2F;api&#x2F;concerns&#x2F;api_guard.rb
included do |base|
  install_error_responders(base)
end

# ...

module ClassMethods
  private
  def install_error_responders(base)
    error_classes = [ MissingTokenError, TokenNotFoundError,
                      ExpiredError, RevokedError, InsufficientScopeError]
    base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler
  end

  def oauth2_bearer_token_error_handler
    Proc.new {|e|
      response = case e
        when MissingTokenError
          Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new

        when TokenNotFoundError
          Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
            :invalid_token,
            &quot;Bad Access Token.&quot;)
        # etc. etc.
        end

      response.finish
    }
  end
end
</code></pre>
<h3 id="step-4-7-zhi-zuo-yong-lai-dang-quan-api-de-guard">Step 4.7: 製作用來擋全 API 的 Guard</h3>
<p>這是仿 <code>doorkeeper_for :all</code> 的，用途是「這個 API 底下的所有 Endpoints 都要擋」。在 Grape 的世界裡面，它要是放在 Grape::API 裡面的 class method ，所以我寫在 ClassMethods module 裡面，一 call 就是塞 before filter 進去，它底下每個 endpoint 都會過這個 filter。</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># app&#x2F;api&#x2F;concerns&#x2F;api_guard.rb
module ClassMethods
  def guard_all!(scopes: [])
    before do
      guard! scopes: scopes
    end
  end
end
</code></pre>
<h3 id="step-4-8-xian-zai-ke-yi-yong-oauth-2-lai-dang-api-liao">Step 4.8: 現在可以用 OAuth 2 來擋 API 了</h3>
<p>單獨擋一個 Endpoint:</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># app&#x2F;api&#x2F;v1&#x2F;sample_api.rb
module V1
  class SampleAPI &lt; Base
    get &quot;secret&quot; do
      guard! # Requires a valid OAuth 2 Access Token to use this Endpoint
      { :secret =&gt; &quot;only smart guys can see this ;)&quot; }
    end
  end
end
</code></pre>
<p>擋一個 API 底下所有 Endpoints:</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># app&#x2F;api&#x2F;v1&#x2F;secret_api.rb
module V1
  class SecretAPI &lt; Base
    guard_all!  # Requires a valid OAuth 2 Access Token to use all Endpoints

    get &quot;secret1&quot; do
      { :secret1 =&gt; &quot;Hi, #{current_user.email}&quot; }
    end

    get &quot;secret2&quot; do
      { :secret2 =&gt; &quot;only smart guys can see this ;)&quot; }
    end
  end
end
</code></pre>
<h3 id="shi-shi-kan">試試看！</h3>
<p>不帶 Token 就去打 API 會被拒絕：</p>
<pre><code>$ curl -i http:&#x2F;&#x2F;localhost:9999&#x2F;api&#x2F;v1&#x2F;secret&#x2F;secret1.json
HTTP&#x2F;1.1 401 Unauthorized
WWW-Authenticate: Bearer realm=&quot;The API&quot;
Content-Type: application&#x2F;json
Cache-Control: no-cache

{&quot;error&quot;:&quot;unauthorized&quot;}
</code></pre>
<p>附 Token 再去打 API 就沒問題了，並且會告訴我這個 User 是誰：</p>
<pre><code>$ curl -i http:&#x2F;&#x2F;localhost:9999&#x2F;api&#x2F;v1&#x2F;secret&#x2F;secret1.json \
&gt; -H &quot;Authorization: Bearer a14bb554309df32fbb6a3bad6cba25f32a28acc931a74ead06ca904c05281b4c&quot;
HTTP&#x2F;1.1 200 OK
Content-Type: application&#x2F;json
Cache-Control: max-age=0, private, must-revalidate

{&quot;secret1&quot;:&quot;Hi, ducksteven@gmail.com&quot;}
</code></pre>
<h2 id="step-5-shi-yong-scope">Step 5: 使用 Scope</h2>
<p>到目前為止，實作出來的 OAuth 2 的 Guard 雖然支援「scopes」，但 Authorization Server 不支援。要怎麼限制某些 API 必須使用「授權了某些 scopes」的 Access Token 才能存取呢？以下是範例，詳細可以參考 Doorkeeper 的文件 <em><a href="https://github.com/applicake/doorkeeper/wiki/Using-Scopes">Using Scopes</a></em> 。程式碼見 <a href="https://github.com/yorkxin/oauth2-api-sample/tree/step-5">step-5 tag</a> 。</p>
<p>首先在 <code>config/initializers/doorkeeper.rb</code> 裡面增加 scopes 的定義，例如</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby"># config&#x2F;initializers&#x2F;doorkeeper.rb

# Define access token scopes for your provider
# For more information go to https:&#x2F;&#x2F;github.com&#x2F;applicake&#x2F;doorkeeper&#x2F;wiki&#x2F;Using-Scopes
default_scopes  :public              # 如果 Client 不索取任何 scopes 則預設使用這組 scopes
optional_scopes :top_secret,         # 其他可以額外申請的 scopes
                :el, :psy, :congroo
</code></pre>
<p>要重開 Rails server 生效。</p>
<p>接著打開 Authorize 的頁面，點下去的網址不會帶有「想要索取的 scopes」，所以先把網址複製下來，後面手動附上 <code>scope=top_secret</code> 參數：（中間斷行比較好讀）</p>
<pre><code>http:&#x2F;&#x2F;localhost:9999&#x2F;oauth&#x2F;authorize
    ?client_id=4a407c6a8d3c75e17a5560d0d0e4507c77b047940db6df882c86aaeac2c788d6
    &amp;redirect_uri=http%3A%2F%2Flocalhost%3A12345%2Fauth%2Fdemo%2Fcallback
    &amp;response_type=code
    &amp;scope=top_secret
</code></pre>
<p>到這裡會再問你要不要 Authorize ，所以你知道了 Authorization Server 會區別帶有不同 scopes 的 grants。Authorize 之後會得到一組 grant code ，再照標準流程拿 Token 。我這次拿到的 Token 是 <code>5d840a4e43049eb1e66367bc788059f9bf16b53f853f3cd4f001e51a5c95abfd</code>.</p>
<p>現在我在 SampleAPI 裡面新增兩個 endpoint ，需要 scopes 才能存取：</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby">get &quot;top_secret&quot; do
  guard! scopes: [:top_secret]
  { :top_secret =&gt; &quot;T0P S3CR37 :p&quot; }
end

get &quot;choice_of_sg&quot; do
  guard! scopes: [:el, :psy, :congroo]
  { :says =&gt; &quot;El. Psy. Congroo.&quot; }
end
</code></pre>
<p>用之前申請過的 Access Token 來打 <code>top_secret</code> 這個 API 會被拒絕（中間斷行比較好讀）：</p>
<pre><code>$ curl -i http:&#x2F;&#x2F;localhost:9999&#x2F;api&#x2F;v1&#x2F;sample&#x2F;top_secret.json \
&gt; -H &quot;Authorization: Bearer a14bb554309df32fbb6a3bad6cba25f32a28acc931a74ead06ca904c05281b4c&quot;
HTTP&#x2F;1.1 403 Forbidden
Content-Type: application&#x2F;json
Cache-Control: no-cache

{
  &quot;error&quot;:&quot;insufficient_scope&quot;,
  &quot;error_description&quot;:&quot;The request requires higher privileges than provided by the access token.&quot;,
  &quot;scope&quot;:&quot;top_secret&quot;
}
</code></pre>
<p>若用新拿到的 Token 就會過了：</p>
<pre><code>$ curl -i http:&#x2F;&#x2F;localhost:9999&#x2F;api&#x2F;v1&#x2F;sample&#x2F;top_secret.json \
&gt; -H &quot;Authorization: Bearer 5d840a4e43049eb1e66367bc788059f9bf16b53f853f3cd4f001e51a5c95abfd&quot;
HTTP&#x2F;1.1 200 OK
Content-Type: application&#x2F;json
Cache-Control: max-age=0, private, must-revalidate

{&quot;top_secret&quot;:&quot;T0P S3CR37 :p&quot;}
</code></pre>
<p>然而如果拿這個 Token 去打 <code>choice_of_sg</code> 就會被拒絕（中間斷行比較好讀）：</p>
<pre><code>$ curl -i http:&#x2F;&#x2F;localhost:9999&#x2F;api&#x2F;v1&#x2F;sample&#x2F;choice_of_sg.json \
&gt; -H &quot;Authorization: Bearer 5d840a4e43049eb1e66367bc788059f9bf16b53f853f3cd4f001e51a5c95abfd&quot;
HTTP&#x2F;1.1 403 Forbidden
Content-Type: application&#x2F;json
Cache-Control: no-cache

{
  &quot;error&quot;:&quot;insufficient_scope&quot;,
  &quot;error_description&quot;:&quot;The request requires higher privileges than provided by the access token.&quot;,
  &quot;scope&quot;:&quot;el psy congroo&quot;
}
</code></pre>
<p>當然，因為 scope 不符啊。這時候就要再重新申請一個 Token ，照前面說過的流程，要先有一個 Authorize 的 URL:</p>
<pre><code>http:&#x2F;&#x2F;localhost:9999&#x2F;oauth&#x2F;authorize
    ?client_id=4a407c6a8d3c75e17a5560d0d0e4507c77b047940db6df882c86aaeac2c788d6
    &amp;redirect_uri=http%3A%2F%2Flocalhost%3A12345%2Fauth%2Fdemo%2Fcallback
    &amp;response_type=code
    &amp;scope=el%20psy%20congroo
             ^^^   ^^^ space
</code></pre>
<p>多重 scope 的情況下，各 scope 之間用空格 <code>%20</code> 分開。</p>
<p>最後我拿到的新 Token 是 <code>0b39839282957d8f80c01901c2468ed52341707594897ec9767af392306f1e55</code> 。再用它去打 <code>choice_of_sg</code> API 就會回我了：</p>
<pre><code>curl -i http:&#x2F;&#x2F;localhost:9999&#x2F;api&#x2F;v1&#x2F;sample&#x2F;choice_of_sg.json \
-H &quot;Authorization: Bearer 0b39839282957d8f80c01901c2468ed52341707594897ec9767af392306f1e55&quot;
HTTP&#x2F;1.1 200 OK
Content-Type: application&#x2F;json
Cache-Control: max-age=0, private, must-revalidate

{&quot;says&quot;:&quot;El. Psy. Congroo.&quot;}
</code></pre>
<hr />
<h2 id="jie-yu">結語</h2>
<p>以上的 Tutorial 只簡單示範了如何從零建造一個 OAuth 2 Authorization Server 並且用 OAuth 2 Bearer Token 來保護 Grape API 。以下這些問題沒有處理：</p>
<ul>
<li>要可以限制「誰才可以開 Clients」 （例如站長，或是有登入的使用者），我想應該是 Doorkeeper 的 <code>admin_authenticator</code> 和 <code>enable_application_owner</code> options 。</li>
<li>沒有示範 Refresh Token</li>
<li>Doorkeeper 不能設定只要開啟哪些 Grant Flows
<ul>
<li>這我有開一個 fork 出來實作，也貼了 <a href="https://github.com/applicake/doorkeeper/pull/295">Pull Request</a> ，等他 merge 。</li>
</ul>
</li>
<li>因為 Guard 是自己寫的，Doorkeeper 的一些功能完全不使用，例如 <code>access_token_methods</code> （設定 Client 可以用哪些方式向 API 出示 Token）</li>
<li>承上，也沒有使用到 Doorkeeper 內建的錯誤訊息 i18n 機制</li>
<li>Guard 的 Error Response 的 "realm" 不同步</li>
<li>Guard 的 <code>insufficient_scope</code> Error 沒有把參數放在 <code>WWW-Authenticate</code> header
<ul>
<li><a href="https://github.com/yorkxin/rack-oauth2/tree/scope-error-params">我的 fork</a> 有實作這個，還沒開 PR 給原版。</li>
<li>雖然在 403 放這個 header doesn't make sense ，但總覺得還是統一放在這裡比較好啊…</li>
</ul>
</li>
<li>Scope 的 matching 只用了簡單的集合比較，不適用於某些 A scope 吃 B scope 情況</li>
</ul>
<p>又，寫完的 Guard 並沒有包成 Gem 還是什麼的……之後應該會包一下吧。</p>
<p>有任何問題（包括指教本文的謬誤）歡迎在下面的留言板提出 :)</p>

            ]]></description>
        </item>
        <item>
            <title>Ruby &#x2F; Rails 的 OAuth 2 整合方案簡單評比</title>
            <link>https://blog.yorkxin.org/posts/oauth2-ruby-and-rails-integration-review/</link>
            <pubDate>Tue, 08 Oct 2013 13:00:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/10/08/oauth2-ruby-and-rails-integration-review.html</guid>
            
            <description><![CDATA[
                <p>要讓 API 可以用 OAuth 2 的方式來上鎖（認證），必須要準備這些東西：</p>
<ul>
<li>造一個 <strong>Authorization Server</strong> 用來管理 Client 和 Token ，包括核發 Access Token</li>
<li>在 Resource Server （即 API） 放一個 <strong>Guard</strong> ，用來限制某些 endpoints 必須帶 OAuth 2 Access Token</li>
</ul>
<p>因為我要造的 API 是 <a href="https://github.com/intridea/grape">Grape</a> 搭起來的，掛在 Rails 底下，於是就 survey 了一些方案：</p>
<ul>
<li><a href="https://github.com/applicake/doorkeeper">Doorkeeper</a></li>
<li><a href="https://github.com/nov/rack-oauth2">Rack::OAuth2</a></li>
<li><a href="https://github.com/opperator/warden-oauth2">Warden::OAuth2</a></li>
<li><a href="https://github.com/intridea/grape/blob/master/lib/grape/middleware/auth/oauth2.rb">Grape::Middleware::Auth::OAuth2</a> （偽）</li>
</ul>
<p>你可能會問： <a href="https://github.com/intridea/oauth2">OAuth2</a> 這個 gem 呢？其實他只有實作 Client ，所以不在本文範圍（完）。</p>
<span id="continue-reading"></span>
<h2 id="doorkeeper">Doorkeeper</h2>
<ul>
<li>完整的 Authorization Server solution 。</li>
<li>簡陋的 Resource Server Guard solution</li>
<li>有內建 Client management</li>
<li>For Ruby on Rails -- 如果你用 Sinatra 就不能用了。</li>
</ul>
<p>基本上幫你搞定 Authorization Server 的所有需求，但 Resource Server Guard 則是很簡陋，或說根本是隨便寫的，只會幫你丟 401 Unauthorized ，Bearer Token 的要求他幾乎沒實作。具體程式碼在 <a href="https://github.com/applicake/doorkeeper/blob/v0.7.3/lib/doorkeeper/helpers/filter.rb">lib/doorkeeper/helpers/filter.rb</a> (Tag: v0.7.3) 。有人開 Pull Request 但沒有 merge： <a href="https://github.com/applicake/doorkeeper/pull/240">support the spec of 'invalid_token' response by masarakki</a> 。</p>
<h2 id="rack-oauth2">Rack::OAuth2</h2>
<ul>
<li>有 Authorization Server</li>
<li>有 Resource Server Guard ，完成度很高</li>
<li>沒有內建管理 Client 的方式</li>
<li>Rack Middleware × 2 ： Authorization Endpoint 和 Token Endpoint</li>
<li>View 要自己刻</li>
<li>沒有綁 Model ，自己刻。</li>
</ul>
<p>簡單來說只幫你處理掉 Protocol 而已，支持它的 Model 和 View 必須自己處理。</p>
<p>Resource Server Guard 「完成度很高」是相對於 Doorkeeper 而言。</p>
<p>問題點：</p>
<ul>
<li><code>insufficient_scope</code> error 沒有帶 <code>WWW-Authenticate</code> 和 required scopes。</li>
<li>它的 authenticator （就是 use middlware 的時候要丟進去的 block）只會在「有傳 token」的情況下才會 call，沒傳 token 就會跳過，繼續往下一個 Rack stack 跑，所以只能用來「抓 token」。</li>
<li>承上，這個 authenticator procedure 要 yield 一個 token object 用來儲存在 Rack env 裡面（raw string 或 model instance 都行），所以它真的就是拿來「抓 token」用的而已。</li>
<li>承上，真正驗證 Token 正確與否的程式要寫在 Application layer ，不能寫在 Rack middleware ，否則就會發生「沒傳 token 卻可以 access API」的問題。</li>
</ul>
<h2 id="warden-oauth2">Warden::OAuth2</h2>
<p>它會跟 Devise 的 warden 卡在一起，所以我沒有深入研究。</p>
<h2 id="grape-de-oauth-2-authorization">Grape 的 OAuth 2 authorization</h2>
<p>Source Code (master): https://github.com/intridea/grape/blob/master/lib/grape/middleware/auth/oauth2.rb</p>
<p>檢閱日期是 2013/10/07 ，與當下最新版本 v0.6.0 一致。</p>
<ul>
<li>既然是 API Framework 提供的，當然只有 Resource Server Guard</li>
<li>但是沒做完</li>
<li>而且 <strong>它並不會 mount 這個 middleware</strong> ，所以實際上無法使用（具體見 <a href="https://github.com/intridea/grape/pull/160">這個 Pull Request</a>）</li>
</ul>
<p>既然目前的版本無法使用，只能看 code 說故事。它的問題所在：</p>
<ul>
<li>要自己造 Model ，預設是 <code>AccessToken</code> ，並且這 class 要實作 <code>#expired</code> 和 <code>#permission_for?</code></li>
<li><code>#permission_for?</code> 就是 scope 驗證，傳進去的參數是整個 Rack <code>env</code> （……）。</li>
<li>但 <code>insufficiet_scope</code> 的 response 並不會提示 required scopes （同 Rack::OAuth2）</li>
<li>完全沒有 <code>error_description</code></li>
<li>Auth-Scheme 同時吃 <code>Bearer</code> 和 <code>OAuth2</code> ，第二種不是 spec 規定的，顯然不需要</li>
</ul>
<h2 id="wo-de-zuo-fa-doorkeeper-rack-oauth2">我的做法: Doorkeeper + Rack::OAuth2</h2>
<p>以上每個 solution 都不完美，就只好自己搭了。</p>
<p>我的做法是這樣：</p>
<ul>
<li>Doorkeepr 拿來蓋 Authorization Server ，一鍵搞定</li>
<li>API 是用 Grape 蓋起來的，所以不用（不能用） Doorkeeper 的 <code>doorkeeper_for</code>。</li>
<li>Guard 改用 Rack::OAuth 2 ，自己寫一個 module 給 Grape 來處理兩邊的整合</li>
</ul>
<p>之後會寫一篇 Tutorial ……最近幾天內會發表。</p>
<p><strong>Edit</strong>: 寫完了： <a href="http://blog.yorkxin.org/posts/2013/10/10/oauth2-tutorial-grape-api-doorkeeper">OAuth 2.0 Tutorial: Grape API 整合 Doorkeeper</a></p>

            ]]></description>
        </item>
        <item>
            <title>關於 HTTP Authentication 的 &quot;realm&quot;</title>
            <link>https://blog.yorkxin.org/posts/realm-in-http-authentication/</link>
            <pubDate>Wed, 02 Oct 2013 03:12:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/10/02/realm-in-http-authentication.html</guid>
            
            <description><![CDATA[
                <p>最近在學 OAuth 2.0，在讀 spec 的時候，發現 Bearer Token 的 auth-param 有 <code>realm</code> 和 <code>scope</code> 。我一開始搞混，後來去查了 HTTP Basic Auth 裡面也有 <code>realm</code> ，在瀏覽器上面出現的效果就是像下圖箭頭指的地方：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2013/2013-10-02-realm-in-http-authentication/http-basic-auth-realm.png" alt="" /></p>
<p>且就算 realm 不同，瀏覽器都會試同樣一組帳號密碼，我試過<a href="https://github.com/yorkxin/http-basic-auth-demo">用 Sinatra 做一個微 App</a> ，分別有以下的 routes：</p>
<table><thead><tr><th>Path</th><th>Realm</th><th>Username</th><th>Password</th></tr></thead><tbody>
<tr><td>/a1</td><td>Protected A</td><td>abc</td><td>a1</td></tr>
<tr><td>/a2</td><td>Protected A</td><td>abc</td><td>a2</td></tr>
<tr><td>/b</td><td>Protected B</td><td>abc</td><td>b</td></tr>
</tbody></table>
<p>這樣子的話：</p>
<ol>
<li>先登入 a1 ，再去 b （不同的 realm）則瀏覽器會用 a2 用過的密碼試著去登入 b。</li>
</ol>
<ul>
<li>先登入 a1 ，再登入 a2 ，再去 a1 （同樣的 realm）則瀏覽器會用 a2 用過的密碼試著去登入 a1。</li>
</ul>
<p>我在 Safari, Chrome, Firefox 都試出一樣的行為。從結果來看應該是不能從 <code>realm</code> 去劃分區域，並且預期瀏覽器會認 <code>realm</code> 來決定要用哪一組帳號密碼，甚至瀏覽器會把上一次用的  <code>realm</code> 記起來，忘掉之前用過的，再遇到同一個 host 裡面需要 authenticate 的場合，就用新的帳密。所以作用域是 per-host 嗎？</p>
<p>如果說 OAuth 2 的 <code>realm</code> 跟 HTTP Basic Auth 的 <code>realm</code> 一樣，那是否表示 <code>realm</code> 是給人類看的，而不是給程式看的呢？我不確定。求科普。<a href="http://stackoverflow.com/a/12701105/664245">在 StackOverflow 上面有人說</a>「Realm 一樣的，就會用同一組帳密」，但從結果來看不是這樣。</p>

            ]]></description>
        </item>
        <item>
            <title>各大網站 OAuth 2.0 實作差異</title>
            <link>https://blog.yorkxin.org/posts/oauth2-implementation-differences-among-famous-sites/</link>
            <pubDate>Mon, 30 Sep 2013 13:50:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/09/30/oauth2-implementation-differences-among-famous-sites.html</guid>
            
            <description><![CDATA[
                <p>因為我現在要自己做 OAuth 2 的服務，所以當然要先看一下別人怎麼做。</p>
<p>然而，即使 OAuth 2.0 的 spec 規定了很多通訊協定上的實作細節，但現實總是跟理想有所差距，各大網站的 OAuth 2 實作都或多或少與 spec 有差異，真正完全照 spec 來做的很少（功能閹割的不算數）。不過概念上都符合 OAuth 2 ，只是實作上有差。</p>
<p>我去看了以下這些網站的 OAuth 2 API 文件，清單是從 <a href="http://en.wikipedia.org/wiki/OAuth#List_of_OAuth_service_providers">Wikipedia / OAuth / List of OAuth service providers</a> 找來的。為了排版美觀， Grant Type 的 Authorization Code 簡寫成 Auth Code， Client Credentials 簡寫成 Client Cred.，Resource Owner Password 簡寫成 Password。檢閱日期是 2013 年 9 月 27 日，未來各 API 可能會改變，導致下表的情況跟現況不一致。</p>
<table><thead><tr><th>Service</th><th>spec<br>相容</th><th>支援的 Grant Types</th><th>scope<br>分隔</th><th>Refresh<br>Token</th><th>Client 認證</th></tr></thead><tbody>
<tr><td><a href="https://developers.facebook.com/docs/facebook-login/login-flow-for-web-no-jssdk/">Facebook</a></td><td>✕</td><td>Auth Code, Implicit, Client Cred.</td><td>comma</td><td>△ (自製)</td><td>GET</td></tr>
<tr><td><a href="http://developer.github.com/v3/oauth/">GitHub</a></td><td>✕</td><td>Auth Code, Password (自製)</td><td>comma</td><td>✕</td><td>POST</td></tr>
<tr><td><a href="https://dev.twitter.com/docs/auth/application-only-auth">Twitter</a></td><td>◯</td><td>Client Cred.</td><td>(無 scope)</td><td>✕</td><td>Basic</td></tr>
<tr><td><a href="https://developers.google.com/accounts/docs/OAuth2">Google</a></td><td>✕</td><td>Auth Code, Implicit</td><td>space</td><td>◯</td><td>POST</td></tr>
<tr><td><a href="http://msdn.microsoft.com/en-us/library/live/hh243647.aspx">Microsoft</a></td><td>✕</td><td>Auth Code, Implicit</td><td>？？？</td><td>◯</td><td>POST</td></tr>
<tr><td><a href="https://www.dropbox.com/developers/core/docs#oa2-authorize">Dropbox</a></td><td>◯</td><td>Auth Code, Implicit</td><td>(無 scope)</td><td>✕</td><td>Basic, POST</td></tr>
<tr><td><a href="https://images-na.ssl-images-amazon.com/images/G/01/lwa/dev/docs/website-developer-guide._TTH_.pdf">Amazon</a></td><td>◯</td><td>Auth Code, Implicit</td><td>space</td><td>◯</td><td>Basic, POST</td></tr>
<tr><td><a href="http://dev.bitly.com/authentication.html">Bitly</a></td><td>✕</td><td>Auth Code (半自製), Password</td><td>(無 scope)</td><td>✕</td><td>POST (Auth Code),<br> Basic (Password)</td></tr>
<tr><td><a href="http://open.weibo.com/wiki/%E6%8E%88%E6%9D%83%E6%9C%BA%E5%88%B6%E8%AF%B4%E6%98%8E">新浪微博</a></td><td>✕</td><td>Auth Code</td><td>comma</td><td>✕</td><td>Basic, GET</td></tr>
<tr><td><a href="http://developers.douban.com/wiki/?title=oauth2">豆瓣</a></td><td>✕</td><td>Auth Code, Implicit</td><td>comma</td><td>◯</td><td>POST</td></tr>
<tr><td><a href="http://developers.box.com/oauth/">BOX</a></td><td>✕</td><td>Auth Code</td><td>(無 scope)</td><td>◯</td><td>POST</td></tr>
<tr><td><a href="https://github.com/37signals/api/blob/master/sections/authentication.md">Basecamp</a></td><td>✕</td><td>Auth Code</td><td>(無 scope)</td><td>◯</td><td>POST</td></tr>
</tbody></table>
<span id="continue-reading"></span>
<p>大概總結一下：</p>
<p>關於 Endpoints:</p>
<ul>
<li>GitHub 和 Basecamp 省略了 <code>response_type</code> 和 <code>grant_type</code> ，猜測是因為只有一種流程。但一樣情況的 Twitter 、 BOX 、新浪微博，則依然要傳 switch 參數。</li>
<li>通常 Authorization Endpoint 都是跟主站放在一起的，而不是放在 API 裡面，我想這個目的是要讓 User 直接用 cookie 登入並授權。</li>
</ul>
<p>關於 Grant Types:</p>
<ul>
<li>幾乎大家都至少會支援 Authorization Code Grant （Twitter 不支援）。</li>
<li>適合實作第三方登入的尤其會支援 Implicit 來做無縫接入（新浪微博似乎是個例外）。</li>
<li>Client Credentials 很少有人會支援（Twitter 用來給 App 存取「非代表使用者」的資料）。</li>
<li>Resource Owner Password 很少有人支援。GitHub 有一種類似的用法，但不是照規格，而是一種自製的流程。</li>
<li>除了內建的四種 Grant Types ，某些服務還會設計自己的 Grant Type，像是 Microsoft 就有一個叫 Sing-In Control Flow ，是 Implicit 的一種變體。</li>
</ul>
<p>關於 Client Authorization （認證）：</p>
<ul>
<li>大部份都有支援 POST</li>
<li>並非全部都有支援 HTTP Basic Auth ，但規格書裡面規定的是要至少支援 Basic Auth ，因為這一點，所以我把很多個服務標成「不相容 spec」</li>
<li>少數有使用 GET</li>
</ul>
<p>關於資料格式：</p>
<ul>
<li>space 的分隔符 (delimiter) 很多用 <code>,</code> 逗號 (comma) ，照規格書用空格 (space) 的也有，不過還有完全不存在 "scope" 的概念，授權範圍就是完全存取。</li>
<li>Microsoft 的 delimiter 我不知道到底是用什麼，詳情見下文。</li>
<li>Token Type 雖然不是每個都是 "Bearer" ，但基本上概念都接近。目前沒有看到使用 MAC Token 的。</li>
</ul>
<p>其他：</p>
<ul>
<li>Refresh Token 不是大家都有支援。</li>
<li>某些服務會提供 SDK ，像是 Facebook 、新浪微博，這樣子在網頁、手機上面，不需要手動規劃流程，由 SDK 完成，這樣也可以防止 App 在流程中動手腳，偷走密碼之類的資料。</li>
</ul>
<p>以下各服務的筆記。</p>
<h2 id="facebook">Facebook</h2>
<p>文件： https://developers.facebook.com/docs/facebook-login/login-flow-for-web-no-jssdk/</p>
<h3 id="grant-types">Grant Types</h3>
<ul>
<li>Authorization Code</li>
<li>Implicit</li>
<li>Client Credentials (用來取得 App Access Token)</li>
</ul>
<h3 id="endpoints">Endpoints</h3>
<ul>
<li>Authorization Endpoint: <code>https://www.facebook.com/dialog/oauth</code> （非 graph.facebook.com）</li>
<li>Token Endpoint: <code>https://graph.facebook.com/oauth/access_token</code></li>
</ul>
<h3 id="te-se">特色</h3>
<ul>
<li>概念上跟 OAuth 2 雷同，但沒有完全照標準。</li>
<li>scope 用逗號 <code>,</code> 分隔 （非標準）</li>
<li>Client Authentication 用 GET （非標準）</li>
<li>Authorzation Endpoint 有提供同時拿 Authorization Code 和 Access Token 的功能（我不清楚是什麼用途）</li>
<li>Implicit Flow 在使用 Token 之前，建議 validate ，防偽造攻擊。詳見 <a href="https://developers.facebook.com/docs/facebook-login/login-flow-for-web-no-jssdk/#confirm">Confirming Identity</a> 段落。</li>
<li>Token 分成很多種，分別有不同的用途。詳見 <a href="https://developers.facebook.com/docs/facebook-login/access-tokens/">Access Tokens</a>。</li>
<li>Desktop App 的 Redirection URI 固定為 <code>https://www.facebook.com/connect/login_success.html</code> ，並要求開發者內嵌一個 browser （如 WebView），監視其 state change 來取得 Access Token。</li>
<li>有自己的 Token Refresh 機制，見 <a href="https://developers.facebook.com/docs/facebook-login/reauthentication/">Re-authentication</a>。</li>
<li>Token Type 不是 Bearer Token，但用起來很像。</li>
</ul>
<p>其他值得參考的文件：</p>
<ul>
<li><a href="https://developers.facebook.com/docs/facebook-login/access-tokens/">Access Tokens</a> - 不同 Access Token 用於各種 Scenario 的詳細操作步驟</li>
<li><a href="https://developers.facebook.com/docs/facebook-login/permissions/">Permissions</a> - 即 scopes 的定義</li>
<li><a href="https://developers.facebook.com/docs/facebook-login/security/#appsecret">Login Security</a> - 安全性指南</li>
<li><a href="https://developers.facebook.com/docs/reference/api/securing-graph-api/">Securing Graph API Calls</a> - 建議跑在 Web Server 上面的程式，用數位簽章簽過 reqeust</li>
</ul>
<h3 id="guan-yu-gu-ding-de-redirection-uri">關於固定的 Redirection URI</h3>
<p>前面提到說它的 Redirection URI 固定為　<code>https://www.facebook.com/connect/login_success.html</code> ，這個 URI 我覺得很有意思，打開來是這樣的內容：（斷行是我自己加的，原文沒斷行）</p>
<pre data-lang="html" class="language-html "><code class="language-html" data-lang="html">Success &lt;br &#x2F;&gt;&lt;b style=&quot;color:red&quot;&gt;安全警告：請以保護密碼的相同態度處理以上網址，切勿和任何人分享。&lt;&#x2F;b&gt;
&lt;script type=&quot;text&#x2F;javascript&quot;&gt;
setTimeout(function() {
  window.history.replaceState &amp;&amp; window.history.replaceState({}, &quot;&quot;, &quot;blank.html#_=_&quot;);
},500);
&lt;&#x2F;script&gt;
</code></pre>
<p>看這段 JavaScript，用途是「500ms 之後自動把瀏覽器的歷史記錄消滅掉，並且讓現在網址變成 <code>blank.html#_=_</code>」。這是什麼用途呢？</p>
<p>我猜測搭配 Implicit Grant Flow 用的。在 Implicit Grant Flow 裡面， Access Token 會包在 Fragment 裡面（就是網址最後面的那個 <code>#xxxx</code>），這樣子一來會被 User-Agent 放進歷史記錄裡，二來如果人眼看得到這個 User-Agent 的 Location Bar ，就可以看到 Access Token 。</p>
<p>所以 Facebook 推薦給 Desktop App 的實作方式，就是程式監視內嵌的 User-Agent 的當前網址，看到這個固定的 Redirection URI ，就可以抓 Access Token 了。而不管有沒有抓到，網頁裡的 script 都會把 Access Toekn 消除掉。</p>
<p>又，這種 <code>#_=_</code> 也常常在 Facebook 第三方登入的時候看到，我猜測也是基於相同的原因，不過還沒有證實。</p>
<h2 id="github">GitHub</h2>
<p>文件： http://developer.github.com/v3/oauth/</p>
<h3 id="grant-types-1">Grant Types</h3>
<ul>
<li>Authorization Code</li>
<li>Resource Owner Password （自製，非標準）</li>
</ul>
<h3 id="endpoints-1">Endpoints</h3>
<p>用於 Authorization Code:</p>
<ul>
<li>Authorization Endpoint: <code>https://github.com/login/oauth/authorize</code></li>
<li>Token Endpoint: <code>https://github.com/login/oauth/access_token</code></li>
</ul>
<p>Resource Owner Password Grant Flow 是自製的流程，叫做 Authorizations，用的是 RESTful API，不是直接套用 OAuth 標準。在 Grant 的時候不使用上述的 Endpoints。</p>
<h3 id="te-se-1">特色</h3>
<ul>
<li>scope 用 <code>,</code> 逗號區隔（非標準）</li>
<li>Client Authentication 支援 POST Auth （非標準）</li>
<li>所謂「自製的 Resource Owner Password Grant Type」，我指的是 GitHub 另外提供「讓 Resource Owner 自行管理 Authorizations」的 API ，見 <a href="http://developer.github.com/v3/oauth/#oauth-authorizations-api">OAuth Authorizations API</a> 段落。</li>
<li>因為 Endpoint 只需要支援 Authorization Code Grant ，所以省略了 <code>respose_type</code> 和 <code>grant_type</code>。</li>
<li>沒有 Refresh Token</li>
<li>Token 沒有時效，亦即除非手動 revoke ，否則永遠有效。</li>
<li>Token Type 不是 Bearer Token，但用起來很像。</li>
</ul>
<h3 id="github-app-ji-shi-shi-yong-oauth">GitHub.app 即是使用 OAuth</h3>
<p>GitHub 的 Mac GUI 應用程式就是用 OAuth 來存取資料的。你可以打開 Keychain Access ，找到 <em>"github.com/mac (you@example.com)"</em> 這個項目，按 Show Password 就可以看到 OAuth Access Token 了：</p>
<p>![Screen Shot 2013-09-30 at 2.46.03 PM.png](https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2013/2013-09-30-oauth2-implementation-differences-among-famous-sites/xQ9h86TQDmXKQB5CKT7T_Screen Shot 2013-09-30 at 2.46.03 PM.png)</p>
<p>用同樣的 Access Token 可以 call API：</p>
<pre><code>$ curl -i -H &quot;Authorization: token bXXXXXXXXXXXXXXXXXXXXXXXXXXXXXb&quot; https:&#x2F;&#x2F;api.github.com&#x2F;user
HTTP&#x2F;1.1 200 OK
Server: GitHub.com
X-OAuth-Scopes: notifications, repo, user
X-Accepted-OAuth-Scopes: user, user:email, user:follow, site_admin
X-GitHub-Media-Type: github.beta
X-Content-Type-Options: nosniff
# 部份略

{
    &quot;login&quot;: &quot;chitsaou&quot;,
    &quot;id&quot;: 10737
    (略)
}
</code></pre>
<h2 id="twitter">Twitter</h2>
<p>文件： https://dev.twitter.com/docs/auth/application-only-auth</p>
<h3 id="grant-types-2">Grant Types</h3>
<ul>
<li>Client Credentials</li>
</ul>
<h3 id="endpoints-2">Endpoints</h3>
<ul>
<li>Token Endpoint: <code>https://api.twitter.com/oauth2/token</code></li>
</ul>
<h3 id="te-se-2">特色</h3>
<ul>
<li>與 spec 相容</li>
<li>只准用在 Application-only Path ，即是「代表 App 操作」。如果要「代表 User 操作」，則必須走 OAuth 1.0a 的流程。見 <a href="https://dev.twitter.com/docs/auth/obtaining-access-tokens">OAuth Signed</a> 文件。</li>
<li>Client Authentication 用 Basic Auth （標準）。</li>
<li>Access Token 使用 Bearer Token 標準 (RFC 6750) 。</li>
<li>沒有 Refresh Token</li>
<li>Access Token 沒有時效，但文件裡提及 app credentials 會過期，這個我搞不懂。（原文是 <em>Obtain or revoke a bearer token with incorrect or expired app credentials</em>）</li>
<li>用 Bearer Token (RFC 6750)</li>
</ul>
<h2 id="google">Google</h2>
<p>文件： https://developers.google.com/accounts/docs/OAuth2</p>
<h3 id="grant-types-3">Grant Types</h3>
<ul>
<li>Authorization Code</li>
<li>Implicit</li>
</ul>
<p>還有一種 Grant Flow 叫做 <a href="https://developers.google.com/accounts/docs/OAuth2ForDevices">"for Devices"</a> ，這是專門為 User-Agent 功能很少的設備來設計的，例如遊戲機、印表機。</p>
<h3 id="endpoints-3">Endpoints</h3>
<ul>
<li>Authorization Endpoint: <code>https://accounts.google.com/o/oauth2/auth</code></li>
<li>Token Endpoint: <code>https://accounts.google.com/o/oauth2/token</code></li>
</ul>
<h3 id="te-se-3">特色</h3>
<ul>
<li>scope 用空白分隔。（標準）。</li>
<li>Client Authentication 用 POST （非標準）。</li>
<li>特別提到 Authorization Code Grant Flow 適用 Chrome App</li>
<li>特別提到 Implicit Grant Flow 最好 validate token （同 Facebook）</li>
<li>把 Native App 歸類在「使用 Authorization Code Grant Flow」裡面，但如此一來 secret 就不再是 secret ，會被抓出來。</li>
<li>對於 Native App，預設有一個 Redirection URI 叫做 <code>urn:ietf:wg:oauth:2.0:oob</code> 。跟 Facebook 的 Desktop App 指南一樣，這是可以給內嵌 browser 監視其 state change 來取得 Access Token 的方式。見 <a href="https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi">Using OAuth 2.0 for Installed Applications / Choosing a Redirect URI</a></li>
<li>有 Refresh Token</li>
<li>用 Bearer Token (RFC 6750)</li>
</ul>
<h2 id="microsoft-windows-live">Microsoft (Windows Live)</h2>
<p>文件： http://msdn.microsoft.com/en-us/library/live/hh243647.aspx</p>
<h3 id="grant-types-4">Grant Types</h3>
<ul>
<li>Authorization Code Grant</li>
<li>Implicit</li>
<li>Sign-in Control （自製，從 Implicit Flow 變化來的，本文略）</li>
</ul>
<h3 id="endpoints-4">Endpoints</h3>
<ul>
<li>Authorization Endpoint: <code>https://login.live.com/oauth20_authorize.srf</code></li>
<li>Token Endpoint: <code>https://login.live.com/oauth20_token.srf</code></li>
</ul>
<h3 id="te-se-4">特色</h3>
<ul>
<li>scope 分隔不明。（沒明說，照文件的<a href="http://msdn.microsoft.com/en-us/library/live/hh243649.aspx#authorization_rest">範例</a>是空格，但 <a href="https://github.com/plexinc/omniauth-live-connect/blob/master/lib/omniauth/strategies/live_connect.rb#L6">omniauth-live-connect</a> 卻是逗號）</li>
<li>Client Authentication 用 POST （非標準）。</li>
<li>在 Authorization Code Grant Type Flow 裡面，可以自行指定 App 為 Desktop 或 Mobile ，這樣子 Redirection URI 會固定為 <code>https://login.live.com/oauth20_desktop.srf</code> 。跟 Facebook 的 Desktop App 指南一樣，要求開發者內嵌一個 browser （如 WebView），監視其 state change 來取得 Access Token。不過跟 Facebook 不同的是，它裡面打開是空空如也，沒有 script 來把 access token 藏起來。</li>
<li>有 Refresh Token</li>
<li>Access Token 沒有時效，但文件裡提及 app credentials 會過期，這個我搞不懂。（原文是 <em>Obtain or revoke a bearer token with incorrect or expired app credentials</em>）</li>
<li>Token Type 沒明說，但看起來也不是 MAC Token ，而是類似 Bearer Token。</li>
</ul>
<h2 id="dropbox">Dropbox</h2>
<p>文件： https://www.dropbox.com/developers/core/docs</p>
<h3 id="grant-types-5">Grant Types</h3>
<ul>
<li>Authorization Code</li>
<li>Implicit</li>
</ul>
<h3 id="endpoints-5">Endpoints</h3>
<ul>
<li>Authorization Endpoint: <code>https://www.dropbox.com/1/oauth2/authorize</code> （非 api.dropbox.com）</li>
<li>Token Endpoint: <code>https://api.dropbox.com/1/oauth2/token</code></li>
</ul>
<h3 id="te-se-5">特色</h3>
<ul>
<li>與 spec 相容</li>
<li>沒有 scope 的概念</li>
<li>Client Authentication 支援 Basic Auth 和 POST Auth</li>
<li>用 Bearer Token (RFC 6750)</li>
</ul>
<h2 id="amazon">Amazon</h2>
<p>文件： http://login.amazon.com/website 裡面的一份 <a href="https://images-na.ssl-images-amazon.com/images/G/01/lwa/dev/docs/website-developer-guide._TTH_.pdf">PDF</a></p>
<h3 id="grant-types-6">Grant Types</h3>
<ul>
<li>Authorization Code</li>
<li>Implicit</li>
</ul>
<h3 id="endpoints-6">Endpoints</h3>
<ul>
<li>Authorization Endpoint: <code>https://www.amazon.com/ap/oa</code> （非 api.amazon.com）</li>
<li>Token Endpoint: <code>https://api.amazon.com/auth/o2/token</code></li>
</ul>
<h3 id="te-se-6">特色</h3>
<ul>
<li>與 spec 相容</li>
<li>scope 用空格分隔（標準）。</li>
<li>Client Authentication 支援 Basic Auth 或 POST （標準）。</li>
<li>特別提到 Implicit Grant Flow 最好 validate token （同 Facebook）</li>
<li>有 Refresh Token</li>
<li>使用 Bearer Token (RFC 6750)。</li>
</ul>
<p>另外文件裡面還提到安全性問題 (Security Considerations)，是從 Spec 的 Security Considerations 來的，但比 spec 的好讀……。</p>
<p>其中我特別注意到的幾點：</p>
<ul>
<li>CSRF - 建議把 state 用 HMAC 編碼過，其 secret 就是 client secret 。</li>
<li>Implicit Flow 的 Resource Owner 偽裝 - 建議要向 Server 驗證 Access Token。</li>
<li>Open Redirector - 最好不要在 Redirection Endpoint 裡面放 <code>&amp;url=xxx</code> 這種東西。</li>
<li>Code Injection - 最好把 state 驗證過沒問題再拿來用，顧客資料 (customer profile) 也要如此做。</li>
</ul>
<h2 id="bit-ly">Bit.ly</h2>
<p>文件： http://dev.bitly.com/authentication.html</p>
<h3 id="grant-types-7">Grant Types</h3>
<ul>
<li>Authorization Code (部份自製，非標準)</li>
<li>Resource Owner Password</li>
</ul>
<h3 id="endpoints-7">Endpoints</h3>
<ul>
<li>Authorization Endpoint: <code>https://bitly.com/oauth/authorize</code></li>
<li>Token Endpoint: <code>https://api-ssl.bitly.com/oauth/access_token</code></li>
</ul>
<h3 id="te-se-7">特色</h3>
<p>Bit.ly 的情況比較特別，它雖然有 Authorization Code Grant Flow ，但實作的細節卻跟 OAuth 2.0 有點不同，這個不同就導致它與 spec 不相容：</p>
<ul>
<li>發 Access Token 不用 JSON 而是用 URL-encoded string （但 Password Grant 卻是用 JSON）。</li>
<li>Client Authentication 用 POST 而不是 Basic Auth （但 Password Grant 卻是用 Basic Auth 而不是 POST）。</li>
</ul>
<p>其他特色：</p>
<ul>
<li>沒有 scope 的概念。</li>
<li>Client Authentication 用 Basic Auth （Password Grant）或 POST （Auth Code Grant），互相不能交換。</li>
<li>沒有 Refresh Token</li>
<li>Token Type 沒有明說是哪一種。</li>
</ul>
<h2 id="xin-lang-wei-bo">新浪微博</h2>
<p>文件： http://open.weibo.com/wiki/%E6%8E%88%E6%9D%83%E6%9C%BA%E5%88%B6%E8%AF%B4%E6%98%8E</p>
<h3 id="grant-types-8">Grant Types</h3>
<ul>
<li>Authorization Code</li>
<li>Implicit</li>
</ul>
<h3 id="endpoints-8">Endpoints</h3>
<ul>
<li>Authorization Endpoint: <code>https://api.weibo.com/oauth2/authorize</code></li>
<li>Token Endpoint: <code>https://api.weibo.com/oauth2/access_token</code></li>
</ul>
<h3 id="te-se-8">特色</h3>
<ul>
<li>scope 用逗號 <code>,</code> 分隔（非標準）。</li>
<li>Client Authentication 用 Basic Auth（標準）和 GET。</li>
<li>有 Refresh Token</li>
<li>有 Token 自動展期機制。</li>
<li>Token Type 不是 Bearer Token。 Call API 的時候可以用 GET 或 Header 送 token。</li>
<li>有對 Client 分等級，不同等級有不同的 Rate Limiting 和 Token 時效。</li>
</ul>
<p>另外它還提供了<a href="http://open.weibo.com/wiki/%E5%BA%94%E7%94%A8%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9">应用安全开发注意事项</a>這份文件可以參考，雖然是從 Spec 裡面的 Security Considerations 拉出來的。</p>
<h2 id="dou-ban">豆瓣</h2>
<p>文件： http://developers.douban.com/wiki/?title=oauth2</p>
<h3 id="grant-types-9">Grant Types</h3>
<ul>
<li>Authorization Code</li>
<li>Implicit</li>
</ul>
<h3 id="endpoints-9">Endpoints</h3>
<ul>
<li>Authorization Endpoint: <code>https://www.douban.com/service/auth2/auth</code></li>
<li>Token Endpoint: <code>https://www.douban.com/service/auth2/token</code></li>
</ul>
<h3 id="te-se-9">特色</h3>
<ul>
<li>scope 用逗號 <code>,</code> 分隔（非標準）。</li>
<li>Client Authentication 用 POST （非標準）。</li>
<li>有 Refresh Token</li>
<li>用 Bearer Token (RFC 6750)。</li>
<li>有對 Client 分等級，不同等級有不同的 Rate Limiting 和 Token 時效。</li>
<li>Error Response 是自己設計的標準，有自己定義錯誤代碼表，其中不少是直接從 OAuth 2.0 Spec 來的。</li>
</ul>
<h2 id="box">BOX</h2>
<p>文件： http://developers.box.com/oauth/</p>
<h3 id="grant-types-10">Grant Types</h3>
<ul>
<li>Authorization Code</li>
</ul>
<h3 id="endpoints-10">Endpoints</h3>
<ul>
<li>Authorization Endpoint: <code>https://www.box.com/api/oauth2/authorize</code></li>
<li>Token Endpoint: <code>https://www.box.com/api/oauth2/token</code></li>
</ul>
<h3 id="te-se-10">特色</h3>
<ul>
<li>沒有 scope 的概念。</li>
<li>Client Authentication 用 POST （非標準）。</li>
<li>有 Refresh Token</li>
<li>使用 Bearer Token (RFC 6750)。</li>
<li>對於「禁止轉回 Redirection Endpoint」的錯誤情況，會出現一個精美的頁面來提示錯誤。</li>
<li>有 self-revoke 的流程。</li>
</ul>
<h2 id="basecamp-37signals">Basecamp (37signals)</h2>
<p>文件： https://github.com/37signals/api/blob/master/sections/authentication.md</p>
<h3 id="grant-types-11">Grant Types</h3>
<ul>
<li>Authorization Code</li>
</ul>
<h3 id="endpoints-11">Endpoints</h3>
<ul>
<li>Authorization Endpoint: <code>https://launchpad.37signals.com/authorization/new</code></li>
<li>Token Endpoint: <code>https://launchpad.37signals.com/authorization</code></li>
</ul>
<h3 id="te-se-11">特色</h3>
<ul>
<li>沒有 scope 的概念。</li>
<li>Client Authentication 用 POST （非標準）。</li>
<li>有 Refresh Token</li>
<li>用 Bearer Token (RFC 6750)。</li>
<li>改密碼就會直接 revoke Access Tokens 。</li>
</ul>
<hr />
<h2 id="oauth-2-0-xi-lie-wen-mu-lu">OAuth 2.0 系列文目錄</h2>
<ul>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-1-introduction/">(1) 世界觀</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">(2) Client 的註冊與認證</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-3-endpoints/">(3) Endpoints 的規格</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-1-auth-code-grant-flow/">(4.1) Authorization Code Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-2-implicit-grant-flow/">(4.2) Implicit Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-3-resource-owner-credentials-grant-flow/">(4.3) Resource Owner Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-4-client-credentials-grant-flow/">(4.4) Client Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-5-issuing-tokens/">(5) 核發與換發 Access Token</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-6-bearer-token/">(6) Bearer Token 的使用方法</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-7-security-considerations/">(7) 安全性問題</a></li>
<li><strong><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-implementation-differences-among-famous-sites/">各大網站 OAuth 2.0 實作差異</a> ← You Are Here</strong></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>OAuth 2.0 筆記 (7) 安全性問題</title>
            <link>https://blog.yorkxin.org/posts/oauth2-7-security-considerations/</link>
            <pubDate>Mon, 30 Sep 2013 13:49:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/09/30/oauth2-7-security-considerations.html</guid>
            
            <description><![CDATA[
                <p>這篇整理了 OAuth 2.0 spec 裡面 Section 10 關於安全性問題的討論。關於安全性模型、分析方式、通訊協定的設計背景，可見 <a href="http://tools.ietf.org/html/rfc6819">OAuth-THREATMODEL</a>。（我沒看，而且我看到的時候已經變成 RFC6819 了）在原文裡面，如果是屬於特定主題的（如 Implicit Grant Type），則我會整理到對應的文章去。剩下來的就在這裡了。</p>
<p>以下的次標題都是我自己加的。</p>
<p>除了 Spec 裡面提到的這些，我還有找到一些 service provider 有提供安全性指南，可以參考：</p>
<ul>
<li><a href="https://developers.facebook.com/docs/facebook-login/security/">Login Security - Facebook Developers</a></li>
<li><a href="http://open.weibo.com/wiki/%E5%BA%94%E7%94%A8%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9">应用安全开发注意事项 - 新浪微博API</a></li>
<li><a href="https://images-na.ssl-images-amazon.com/images/G/01/lwa/dev/docs/website-developer-guide._TTH_.pdf">Amazon OAuth 文件的 Security Considerations</a></li>
</ul>
<span id="continue-reading"></span>
<h2 id="client-ren-zheng-de-an-quan-xing-wen-ti-section-10-1">Client 認證的安全性問題 (Section 10.1)</h2>
<p>見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">系列文第 2 篇</a>。</p>
<h2 id="wei-zhuang-cheng-bie-de-client-section-10-2">偽裝成別的 Client (Section 10.2)</h2>
<p>見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">系列文第 2 篇</a>。</p>
<h2 id="access-token-de-an-quan-xing-wen-ti-section-10-3">Access Token 的安全性問題 (Section 10.3)</h2>
<h3 id="bao-mi-chuan-shu-bao-mi-chu-cun">保密傳輸、保密儲存</h3>
<p>Access Token 的 crednetials 以及任何機密的 Access Token 屬性，都必須要保密傳輸、保密儲存，並且只能在以下角色之間流通：</p>
<ul>
<li>Authrization Server</li>
<li>Access Token 搭配的 Resource Server</li>
<li>Access Token 所核發的對象 Client</li>
</ul>
<p>Access Token credentials 必須只能經過 TLS 傳輸，還要搭配 Server Authentication （定義在 RFC 2818: HTTP over TLS）。</p>
<h3 id="implicit-flow-de-token-wai-xie-feng-xian">Implicit Flow 的 Token 外洩風險</h3>
<p>Implicit Grant Type Flow 的 Access Token 是經過 URI Fragment 傳輸的，可能會外洩給未經授權的第三方。</p>
<h3 id="fang-cai-cai-le-gong-ji">防猜猜樂攻擊</h3>
<p>Authorization Server 必須確保 Access Token 不會被未經授權的第三方自動產生、被修改、被猜出如何產生可以用的。</p>
<h3 id="jue-ding-scope-shi-ke-yi-dui-client-jie-ji-qi-shi">決定 scope 時可以對 Client 階級歧視</h3>
<p>Client 請求 Access Token 的時候，應該只請求最少需要的 scope 。Authorization Server 在選擇如何遵守所請求的 scope 的時候，應該要考慮 Client 的身份，並且可以核發少於所求的 scope 的 Access Token。（編按：亦即決定 scope 的時候，可以對 Client 實施階級歧視）</p>
<h3 id="wu-ji-zhi-lai-que-bao-access-token-de-quan-wei-xing">無機制來確保 Access Token 的權威性</h3>
<p>Spec 不為 Resource Server 提供任何方法來確保 Client 出示的 Access Token 真的是 Authorization Server 核發的。</p>
<h2 id="refresh-token-de-an-quan-xing-wen-ti-section-10-4">Refresh Token 的安全性問題 (Section 10.4)</h2>
<h3 id="he-fa-dui-xiang">核發對象</h3>
<p>Authorization Server 可以核發 Refresh Token 給這些 Clients：</p>
<ul>
<li>Web Application Clients （跑在 Server 上）</li>
<li>Native Application Clients</li>
</ul>
<p>（編按： Angular.js 之類的 client-side app 算 UA-based Clients ，不可以核發 Refresh Token）</p>
<h3 id="bao-mi-chuan-shu-bao-mi-chu-cun-1">保密傳輸、保密儲存</h3>
<p>Refresh Token 必須要保密傳輸、保密儲存，並且只能在以下角色之間流通：</p>
<ul>
<li>Authrization Server</li>
<li>Refresh Token 核發的對象 Client</li>
</ul>
<p>Authorization Server 必須把 Refersh Token 和被核發的 Client 綁定。</p>
<p>Refresh Token credentials 必須只能經過 TLS 傳輸，還要搭配 Server Authentication （定義在 RFC 2818: HTTP over TLS）。</p>
<p>（編按：除了「不能給 Resource Server」 、「要求必須綁定 Client」 ，其他跟 Access Token 的保密要求一樣）</p>
<h3 id="zi-dong-ba-jiu-de-token-che-xiao-diao">自動把舊的 Token 撤銷掉</h3>
<p>例如，Authorization Server 可以實施 Refresh Token Rotation ：</p>
<ul>
<li>每一次換發 Access Token 的時候，一併換 Rrefresh Token</li>
<li>之前的 Referesh Token 就變成無效</li>
<li>但是保留在 Authorization Server 裡面</li>
</ul>
<p>如果 Refresh Token 被偷，還隨後被壞人和合法的 Client 同時使用，那麼其中一個將會出示無效的 Refresh Token ，這樣 Authorization Server 就可以甄別兩方，壞人就會破功。</p>
<h3 id="fang-cai-cai-le-gong-ji-1">防猜猜樂攻擊</h3>
<p>Authorization Server 必須確保 Refresh Token 不會被未經授權的第三方自動產生、被修改、被猜出如何產生可以用的。</p>
<h2 id="authorization-code-de-an-quan-xing-wen-ti-section-10-5">Authorization Code 的安全性問題 (Section 10.5)</h2>
<p>見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-1-auth-code-grant-flow/">系列文第 4.1 篇</a>。</p>
<h2 id="cuan-gai-authorization-code-de-redirection-uri-section-10-6">竄改 Authorization Code 的 Redirection URI (Section 10.6)</h2>
<p>見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-1-auth-code-grant-flow/">系列文第 4.1 篇</a>。</p>
<h2 id="resource-owner-password-credentials-de-an-quan-xing-wen-ti-section-10-7">Resource Owner Password Credentials 的安全性問題 (Section 10.7)</h2>
<p>見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-3-resource-owner-credentials-grant-flow/">系列文第 4.3 篇</a>。</p>
<h2 id="request-de-jia-mi-chuan-shu-section-10-8">Request 的加密傳輸 (Section 10.8)</h2>
<p>以下這些資訊禁止沒加密就傳輸：</p>
<ul>
<li>Access Token</li>
<li>Refresh Token</li>
<li>Resource Owner Passwords</li>
<li>Client Credentials</li>
</ul>
<p>Authorization Code 最好不要沒加密就傳輸。（不強求）</p>
<p><code>state</code> 和 <code>scope</code> 參數最好不要明文內嵌關於 Client 或 Resource Onwer 的敏感資訊，因為他們可能會沒有加密傳輸、加密儲存。</p>
<p>（編按：這裡的加密傳輸我解釋為 TLS）</p>
<h2 id="que-bao-endpoint-de-quan-wei-xing-authenticity-section-10-9">確保 Endpoint 的權威性 (Authenticity) (Section 10.9)</h2>
<p>為了防止中間人攻擊，對於往 Authorization Endpoint 和 Token Endpoint 的 requests：</p>
<ul>
<li>Authorization Server 必須要有 TLS 加上伺服器認證 (RFC 2818)</li>
<li>Client 必須驗證 Server 的 TLS 憑證 (RFC 6150)，and in accordance with its requirements for server identity authentication （看不懂，保留原文…）。</li>
</ul>
<p>（編按：我理解為必須檢驗憑證的合法，以及憑證鏈的合法。如果 SSL 憑證過期、網域名稱不對、或被 revoke ，或憑證鏈不合法，則視為不合法。）</p>
<h2 id="credential-cai-cai-le-gong-ji-section-10-10">Credential 猜猜樂攻擊 (Section 10.10)</h2>
<p>Authorization Server 必須防止壞人可以猜到：</p>
<ul>
<li>Access Token （自動產生）</li>
<li>Authorization Code （自動產生）</li>
<li>Refresh Token （自動產生）</li>
<li>Resource Owner Password （使用者自己持有）</li>
<li>Client Credential （自動產生）</li>
</ul>
<p>至於猜中機率的要求：</p>
<p>對於自動產生、非使用者自己持有的資料，猜中的機率必須不高於 1/(2^128) （2 的 128 次方分之 1）、並且應該不高於 1/(2^160) （2 的 160 次方分之 1）。（編按：所謂 2 的 n 次方分之 1 ，指的是資料的可能性有 2 的 n 次方這麼多種，實務上就是長度為 n-bit 的資料。）</p>
<p>對於使用者自己持有的資料， Authorization Server 必須使用其他手段來保護之。</p>
<h2 id="diao-yu-gong-ji-section-10-11">釣魚攻擊 (Section 10.11)</h2>
<p>大範圍部署 OAuth 等類似的通訊協定，可能會導致使用者習慣於被轉向到別的網站，在那邊被要求填入密碼。如果使用者在輸入密碼之前，不謹慎確認這些網站的權威性 (authenticity) ，就可能讓壞人利用這個習慣來偷取 Resource Owner 的密碼。</p>
<p>OAuth 的服務提供者應該要嘗試教育使用者關於釣魚攻擊的風險，並且應該要提供一種機制來讓使用者可以很容易就確認正牌網站的權威性。Client 開發者也應該要考慮到安全衝擊，像是使用者會怎麼與 User-Agent 互動（內嵌或外部瀏覽器），以及使用者是否有能力核實 Authorization Server 的權威性。</p>
<p>為了降低釣魚攻擊的風險， Authorization Server 必須在每個用來與使用者互動的 Endpoints 都要求 TLS 。</p>
<h2 id="csrf-section-10-12">CSRF (Section 10.12)</h2>
<p>CSRF (Cross-site request forgery) 是一種攻擊手段，壞人讓受害使用者的 User-Agent 跟隨惡意的 URI （例如會以誤導的連結、圖片、轉址等形式提供給 User-Agent）跑到信任的伺服器（通常是透過一個合法的 session cookie 來達成）。</p>
<h3 id="dui-client-de-csrf-gong-ji">對 Client 的 CSRF 攻擊</h3>
<p>對 Client 的 Redirection URI 的 CSRF 攻擊，可以讓壞人注入他自己的 Authorization Code 或 Access Token ，這樣會導致 Client 直接使用一個連結到壞人的 Protected Resource 的 Access Token ，而不是受害者自己的 Access Token （例如，把受害者的銀行帳戶資訊儲存到壞人所控制的 Protected Resource）。</p>
<p>Client 必須在 Redirection URI 實作 CSRF 防範。通常的做法是：</p>
<ul>
<li>要求任何 request 附帶一個值，這個值綁定到 User-Agent 的一個獲授權的狀態（例如把一個用來認證 User-Agent 的 session cookie 給 hash 過之後的數值）</li>
<li>當 Client 發出 Authorization request 到 Authorization Server 的時候，Client 應該要使用 <code>state</code> 參數來傳遞這個值。</li>
<li>在使用者授權之後，Authorization Server 會把使用者的 User-Agent 轉向回 Client 並附上 <code>state</code> 參數。</li>
<li>Client 可以檢查這個綁定值是否跟 User-Agent 獲授權的狀態相同，來確認 request 的合法與否。</li>
</ul>
<p>其用來防範 CSRF 的綁定值，必須包含一個無法猜測的值（如 Section 10.10 所述），且 User-Agent 的獲授權的狀態（session cookie 、 HTML5 local storage 等）必須保存在一個只有 Client 的 User-Agent 可以存取的地方（也就是說要用 same-origin policy 保護）。</p>
<p>在 <a href="https://images-na.ssl-images-amazon.com/images/G/01/lwa/dev/docs/website-developer-guide._TTH_.pdf">Amazon 的文件</a> 裡面，是建議把 <code>state</code> 用 HMAC 簽過，secret 就用 Client Secret ，這樣可以防止人家偽造。</p>
<h3 id="dui-authorization-server-de-csrf-gong-ji">對 Authorization Server 的 CSRF 攻擊</h3>
<p>對 Authorization Endpoint 的 CSRF 攻擊，可以讓壞人在使用者未參與或未獲警告的情況下，取得給惡意 Client 的使用者授權。</p>
<p>Authorization Server 必須在 Authorization Endpoint 實作 CSRF 防範，並且確保惡意的 Client 不會在 Resource Owner 不知情、沒有明確許可的情況下取得授權，</p>
<h2 id="clickjacking-dian-ji-bang-jia-section-10-13">Clickjacking （點擊綁架） (Section 10.13)</h2>
<p>在 Clickjacking 攻擊裡面，壞人會這樣做：</p>
<ul>
<li>註冊一個合法的 Client。</li>
<li>造一個惡意網站，在這網站裡面，有一個透明的 iframe 會載入 Authorization Server 的 Authorization Endpoint 網頁。</li>
<li>iframe 覆蓋在一組假造的按鈕之上，這精心製作的按鈕會直接放在授權頁面的重要按鈕的下面。</li>
<li>當使用者看到那誤導按鈕，並且按下去，使用者實際上是按下了疊在授權頁面之上的隱藏按鈕（例如「給予授權」按鈕）。</li>
</ul>
<p>如此攻擊者可以晃點 Resource Owner ，讓他不經意授權了 Client 的存取權，而使用者卻不知情。</p>
<p>為了防範這種型式的攻擊， Native Application 在請求使用者授權的時候，最好要使用外部 browser ，而非內嵌一個 browser 到應用程式裡面。對於多數的新瀏覽器來說，可以使用 <a href="https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options">"X-Frame-Options" header （非標準）</a> 來強制要求 Authorization Server 的網頁不能從 iframe 載入。這個 header 可以有兩種值，分別是 "deny" （禁止其從任何框架頁載入）和 "sameorigin" （只允許同樣 origin 的網站透過框架頁載入）。對於舊的瀏覽器來說，<a href="http://en.wikipedia.org/wiki/Framekiller">JavaScript Frame-Busting</a> 技術可能可以用，但不見得對所有瀏覽器都有效。</p>
<p>相關資料：</p>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options">The X-Frame-Options response header - HTTP | MDN</a></li>
<li><a href="http://blogs.msdn.com/b/ieinternals/archive/2010/03/30/combating-clickjacking-with-x-frame-options.aspx">Combating ClickJacking With X-Frame-Options - IEInternals - Site Home - MSDN Blogs</a></li>
</ul>
<h2 id="cheng-shi-ma-zhu-ru-yi-ji-shu-ru-zhi-de-yan-zheng-section-10-14">程式碼注入以及輸入值的驗證 (Section 10.14)</h2>
<p>當輸入值或是其他外部參數沒有被消毒 (sanitizied) 導致改變了應用程式的內部邏輯的時候，就是發生了程式碼注入攻擊。這可能會允許壞人取得應用程式裝置或資料的存取權、造成服務阻斷 (DoS) 、或是造成更大範圍的不良副作用。</p>
<p>Authorization Server 必須對任何接收到的資料消毒（可以的話還要驗證是否正確），特別是 "state" 和 "redirection_uri" 參數。</p>
<h2 id="open-redirectors-section-10-15">Open Redirectors (Section 10.15)</h2>
<p>見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-3-endpoints/">系列文第 3 篇</a>。</p>
<h2 id="wu-yong-access-token-lai-zai-implicit-flow-li-mian-wei-zhuang-resource-owner-section-10-16">誤用 Access Token 來在 Implicit Flow 裡面偽裝 Resource Owner (Section 10.16)</h2>
<p>見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-2-implicit-grant-flow/">系列文第 4.2 篇</a>。</p>
<hr />
<h2 id="oauth-2-0-xi-lie-wen-mu-lu">OAuth 2.0 系列文目錄</h2>
<ul>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-1-introduction/">(1) 世界觀</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">(2) Client 的註冊與認證</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-3-endpoints/">(3) Endpoints 的規格</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-1-auth-code-grant-flow/">(4.1) Authorization Code Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-2-implicit-grant-flow/">(4.2) Implicit Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-3-resource-owner-credentials-grant-flow/">(4.3) Resource Owner Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-4-client-credentials-grant-flow/">(4.4) Client Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-5-issuing-tokens/">(5) 核發與換發 Access Token</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-6-bearer-token/">(6) Bearer Token 的使用方法</a></li>
<li><strong><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-7-security-considerations/">(7) 安全性問題</a> ← You Are Here</strong></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-implementation-differences-among-famous-sites/">各大網站 OAuth 2.0 實作差異</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>OAuth 2.0 筆記 (6) Bearer Token 的使用方法</title>
            <link>https://blog.yorkxin.org/posts/oauth2-6-bearer-token/</link>
            <pubDate>Mon, 30 Sep 2013 13:48:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/09/30/oauth2-6-bearer-token.html</guid>
            
            <description><![CDATA[
                <p>這篇不屬於 OAuth 2.0 規格書（RFC 6749）本身，而是屬於另一份 spec <em><a href="http://tools.ietf.org/html/rfc6750">RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage</a></em> 。我認為它存在的目的是「示範一下 Token 的用法，並且定義下來，讓大家可以參考」，因為 OAuth 2.0 規格書沒有明確規定「Token 長什麼樣子」，甚至「Resource Server 如何拒絕非法的 Token」（指 API）都沒定義，只規定了怎麼拿取、怎麼撤銷、怎麼流通。</p>
<p>實際上，即使有定義這個 Bearer Token ，各大網站的 API 也並非都使用這種 Token ，我看到有明確說明使用 Bearer Token 的像是 Twitter API，其他的要不是非使用 "Bearer" 關鍵字，就是沒有明確指出何種 Token （其實也不需要，因為在那些網站 Token 只有一種用途）。</p>
<p>不過即使如此，對於我打算實作的 API ，我也是準備使用 Bearer Token 的，因為夠 naïve 。如果你跟我一樣沒有自己刻 Token 的能力，就用 Bearer Token 就好了。</p>
<p>當然， RFC 6750 我也有轉成 <a href="https://gist.github.com/yorkxin/6591290">Markdown 好讀版</a>。</p>
<span id="continue-reading"></span>
<h2 id="bearer-token-de-yong-tu">Bearer Token 的用途</h2>
<p>OAuth 2.0 (<a href="http://tools.ietf.org/html/rfc6749">RFC 6749</a>) 定義了 Client 如何取得 Access Token 的方法。Client 可以用 Access Token 以 Resource Owner 的名義來向 Resource Server 取得 Protected Resource ，例如我 (Resource Owner) 授權一個手機 App (Client) 以我 (Resource Owner) 的名義去 Facebook (Resource Server) 取得我的朋友名單 (Protected Resource)。OAuth 2.0 定義 Access Token 是 Resource Server 用來認證的唯一方式，有了這個， Resource Server 就不需要再提供其他認證方式，例如帳號密碼。</p>
<p>然而在 RFC 6749 裡面只定義抽象的概念，細節如 Access Token 格式、怎麼傳到 Resource Server ，以及 Access Token 無效時， Resource Server 怎麼處理，都沒有定義。所以在 RFC 6750 另外定義了 Bearer Token 的用法。Bearer Token 是一種 Access Token ，由 Authorization Server 在 Resource Owner 的允許下核發給 Client ，Resource Server 只要認這個 Token 就可以認定 Client 已經經由 Resource Owner 的許可，不需要用密碼學的方式來驗證這個 Token 的真偽。關於 Token 被偷走的安全性問題，此 Spec 裡面也有提到。</p>
<p><em>本段參考 Abstract 及 Section 1</em></p>
<h2 id="bearer-token-de-ge-shi">Bearer Token 的格式</h2>
<pre><code>Bearer XXXXXXXX
</code></pre>
<p>其中 <code>XXXXXXXX</code> 的格式為 b64token ，ABNF 的定義：</p>
<pre><code>b64token = 1*( ALPHA &#x2F; DIGIT &#x2F; &quot;-&quot; &#x2F; &quot;.&quot; &#x2F; &quot;_&quot; &#x2F; &quot;~&quot; &#x2F; &quot;+&quot; &#x2F; &quot;&#x2F;&quot; ) *&quot;=&quot;
</code></pre>
<p>寫成 Regular Expression 即是：</p>
<pre><code>&#x2F;[A-Za-z0-9\-\._~\+\&#x2F;]+=*&#x2F;
</code></pre>
<p><em>本段參考 Section 2.1</em></p>
<h2 id="client-xiang-resource-server-chu-shi-access-token-de-fang-shi">Client 向 Resource Server 出示 Access Token 的方式</h2>
<p>三種</p>
<h3 id="1-fang-zai-http-header-li-mian">(1) 放在 HTTP Header 裡面</h3>
<pre><code>GET &#x2F;resource HTTP&#x2F;1.1
Host: server.example.com
Authorization: Bearer mF_9.B5f-4.1JqM
</code></pre>
<p>Resource Server 必須支援這個方式。</p>
<p><em>本段參考 Section 2.2</em></p>
<h3 id="2-fang-zai-request-body-li-mian-form-zhi-lei-de">(2) 放在 Request Body 裡面（Form 之類的）</h3>
<pre><code>POST &#x2F;resource HTTP&#x2F;1.1
Host: server.example.com
Content-Type: application&#x2F;x-www-form-urlencoded

access_token=mF_9.B5f-4.1JqM
</code></pre>
<p>前提：</p>
<ul>
<li>Header 要有 <code>Content-Type: application/x-www-form-urlencoded</code>。</li>
<li>Body 格式要符合 <a href="http://www.w3.org/TR/1999/REC-html401-19991224/interact/forms.html#h-17.13.4.1">W3C HTML 4.01 定義 application/x-www-form-urlencoded</a>。</li>
<li>Body 要只有一個 part （不可以是 multipart）。</li>
<li>Body 要編碼成只有 ASCII chars 的內容。</li>
<li>Request method 必須是一種有使用 request-body 的，也就是說不能用 <code>GET</code> 。</li>
</ul>
<p>就是送表單嘛，但不可以是 <code>multipart/form-data</code> 這種（通常用來上傳檔案）。</p>
<p>Resource Server 可以但不一定要支援這個方式。</p>
<p><em>本段參考 Section 2.3</em></p>
<h3 id="3-fang-zai-uri-li-mian-de-yi-ge-query-parameter-bu-jian-yi">(3) 放在 URI 裡面的一個 Query Parameter （不建議）</h3>
<p>規定要使用 <code>access_token</code> 這個 parameter ，例：</p>
<pre><code>GET &#x2F;resource?access_token=mF_9.B5f-4.1JqM HTTP&#x2F;1.1
Host: server.example.com
</code></pre>
<p>然而因為 URL 可以被 proxy 抄走（如 log）或存在瀏覽器的歷史記錄裡面，為了防 replay ，最好這樣做：</p>
<ul>
<li>Client 送 <code>Cache-Control: no-store</code> header</li>
<li>Server 回 <code>2xx</code> 的時候，送 <code>Cache-Control: private</code> header</li>
</ul>
<p><strong>Spec 不建議使用這種方法</strong>，如果真的沒辦法送 header 也沒辦法透過 request-body 送，再來考慮這種。</p>
<p>Resource Server 可以但不一定要支援這個方式。</p>
<p><em>本段參考 Section 2.4</em></p>
<h2 id="resource-server-xiang-client-ti-shi-ren-zheng-bu-guo-ju-jue-cun-qu-de-fang-shi">Resource Server 向 Client 提示「認證不過，拒絕存取」的方式</h2>
<p>拒絕存取的情況，例如沒給 Access Token 或是給了但不合法（如空號、過期、Resource Owner 沒許可 Client 拿取此資料），則 Resource Server 必須在回應裡包含 <code>WWW-Authenticate</code> 的 header 來提示錯誤。這個 header 定義在 <a href="http://tools.ietf.org/html/rfc2617#section-3.2.1">RFC 2617 Section 3.2.1</a>。<code>WWW-Authenticate</code> 的值，使用的 auth-scheme 是 <code>Bearer</code> ，隨後一個空格，接著要有至少一個 auth-param 。</p>
<p>範例：</p>
<pre><code>HTTP&#x2F;1.1 401 Unauthorized
WWW-Authenticate: Bearer realm=&quot;example&quot;,
                  error=&quot;invalid_token&quot;,
                  error_description=&quot;The access token expired&quot;
</code></pre>
<p>以下這些 auth-params 是 <code>WWW-Authenticate</code> 會用到的：</p>
<table><thead><tr><th>參數名</th><th>必/選</th><th>填什麼/意義</th></tr></thead><tbody>
<tr><td>realm</td><td>選用</td><td>見下文</td></tr>
<tr><td>scope</td><td>選用</td><td>提示所需權限，見下文</td></tr>
<tr><td>error</td><td>選用</td><td>有出示 Access Token 則最好有這個</td></tr>
</tbody></table>
<h3 id="realm"><code>realm</code></h3>
<p>用 <code>realm</code> 來指出需要授權才能存取的範圍，意義跟 <a href="http://tools.ietf.org/html/rfc2617">HTTP Authentication</a> 的 <code>realm</code> 一樣。<code>realm</code> 只能出現一次。</p>
<h3 id="scope"><code>scope</code></h3>
<p>用 <code>scope</code> 來指出「要拿這個 Resource 需要出示具有哪些 scope 的 Access Token 」：</p>
<ul>
<li>要區分大小寫。</li>
<li>要以空白分隔。</li>
<li>可以用哪些 scope ，是看 Authorization Server 怎麼定義，Spec 不定義，也沒有登錄中心。</li>
<li>順序不重要。</li>
<li>是給程式看的，不是設計給使用者看的。</li>
</ul>
<p><code>scope</code> 還可以在向 Authorization Server 索取新 Access Token 的時候使用。</p>
<p><code>scope</code> 值只能出現一次。實際寫在 <code>scope</code> 裡面的單一個 scope 必須只能用以下的字元，定義在 <a href="http://tools.ietf.org/html/rfc6749#appendix-A.4">RFC 6749 附錄 A.4</a> ：</p>
<pre><code>\x21, \x23-\x5b, \x5d-\x7e
</code></pre>
<p>即可見的 US-ASCII 字元裡面，除了雙引號 <code>"</code> (\x22) 和反斜線 <code>\</code> (\x5c) 以外。空格當然也不能用，因為是用來區分不同 scopes 的。</p>
<h3 id="error"><code>error</code></h3>
<p>如果 Client 出示了 Access Token 但認證失敗，則最好加上 <code>error</code> 這個 auth-param ，用來告訴 Client  為何認證失敗。此外還可以加上 <code>error_description</code> 用自然語言來告訴開發者為什麼錯誤，但這個不該給使用者看到。此外也可以加上 <code>error_uri</code> 用來提供一個網址，裡面用自然語言解釋錯誤訊息。這三個 auth-param 都只能最多出現一次。</p>
<p>如果 request 沒有出示 Access Token （例如 Client 不知道需要認證，或是使用了不支援的認證方式（例如不支援 URI parameter）），則 response 不應該帶 <code>error</code> 或任何錯誤訊息。</p>
<p><code>error</code> 的值的意義以及推薦使用的 HTTP response code 如下：</p>
<table><thead><tr><th>值</th><th>Status Code</th><th>意義/用途</th></tr></thead><tbody>
<tr><td>invalid_request</td><td>400 Bad Request</td><td>沒提供必要的參數、提供了不支援的參數、提供了錯誤的參數值、同樣的參數出現多次、使用一種以上的方法來出示 Access Token （如放在 header 裡又放在 form 裡）、或是其他無法解讀 request 的情況。</td></tr>
<tr><td>invalid_token</td><td>401 Unauthorized</td><td>Access Token 過期、被收回授權、無法解讀、或其他 Access Token 不合法的情況。這種情況下， Client 可以重新申請一個 Access Token 並且用新的 Access Token 來重試 request 。</td></tr>
<tr><td>insufficient_scope</td><td>403 Forbidden</td><td>這個 request 需要出示比 Client 出示的 Access Token 代表的 scopes 還要更多的 scopes 。這種情況下，可以另外提供 <code>scope</code> auth-param 來具體指出需要哪些 scopes 。</td></tr>
</tbody></table>
<p><code>error</code> 和 <code>error_description</code> 的值必須只能用以下的字元，定義在 <a href="http://tools.ietf.org/html/rfc6749#appendix-A.7">RFC 6749 附錄 A.7</a> 和 <a href="http://tools.ietf.org/html/rfc6749#appendix-A.8">RFC 6749 附錄 A.8</a>：</p>
<pre><code>\x20-\x21, \x23-\x5b, \x5d-\x7e
</code></pre>
<p>即空格 (\x20) 再加上可見的 US-ASCII 字元裡面，除了雙引號 <code>"</code> (\x22) 和反斜線 <code>\</code> (\x5c) 以外。</p>
<p><code>error_uri</code> 的值必須符合 <a href="http://tools.ietf.org/html/rfc3986">RFC 3986</a> 的定義，即是只能用以下的字元（同 <code>scope</code> 裡面的單一 scope）：</p>
<pre><code>\x21, \x23-\x5b, \x5d-\x7e
</code></pre>
<p>即可見的 US-ASCII 字元裡面，除了雙引號 <code>"</code> (\x22) 和反斜線 <code>\</code> (\x5c) 以外。</p>
<p><em>本段參考 Section 3 及 Section 3.1</em></p>
<h2 id="authorization-server-gei-client-he-fa-access-token-de-fan-li">Authorization Server 給 Client 核發 Access Token 的範例</h2>
<p>既然是 OAuth 2.0 的 access token ，就通常是循 <a href="http://tools.ietf.org/html/rfc6749">OAuth 2.0 的 spec</a> 來核發，範例如下：</p>
<pre><code>HTTP&#x2F;1.1 200 OK
Content-Type: application&#x2F;json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  &quot;access_token&quot;:&quot;mF_9.B5f-4.1JqM&quot;,
  &quot;token_type&quot;:&quot;Bearer&quot;,
  &quot;expires_in&quot;:3600,
  &quot;refresh_token&quot;:&quot;tGzv3JOkF0XG5Qx2TlKWIA&quot;
}
</code></pre>
<p><em>本段參考 Section 4</em></p>
<h2 id="an-quan-xing-wen-ti-yu-dui-ce">安全性問題與對策</h2>
<p>RFC 6750 是基於 OAuth 2.0 <a href="http://tools.ietf.org/html/rfc6749">RFC 6749</a> 來寫的，所以在該 spec 裡面提過的安全性問題就不再提及。</p>
<p>原文將問題與問題對策分開成 Section 5.1 和 Section 5.2 ，我為了方便筆記，所以合併在一起。以下「壞人」的原文是 <em>attacker</em>。</p>
<h3 id="yi-ban-dui-ce">一般對策</h3>
<p>大部份的安全性問題可以透過數位簽章或是 MAC (Message Authentication Code) 來防護。</p>
<p>Authorization Server 必須有實作 TLS，版本則隨時間推移而不同。 spec 作成的時候， TLS 最新版是 1.2 ，但實務上很少使用，1.0 才是最為廣泛利用的。</p>
<h3 id="wei-zao-huo-cuan-gai-access-token-de-wen-ti">偽造或竄改 Access Token 的問題</h3>
<p>壞人可能會偽造或竄改既有的 Access Token （竄改指的是修改授權範圍或授權參數），讓 Resource Server 給予 Client 不適當的存取權。例如，壞人可能會延長 Token 的過期時間。或是惡意的 Client 變造聲明來看到它不應該看到的東西，例如，告訴使用者只拿取公開的個人資料，卻在取得授權時，另外拿了朋友名單。</p>
<p><em>Section 5.1 &gt; Token manufacture/modification</em></p>
<h4 id="dui-ce">對策</h4>
<p>對於 Bearer Token ，可以只加一個參照用的 id 來間接指到真正的授權資訊，而不是直接燒在 Token 裡面。這種間接參照用的 id ，必須要難以被猜到；但使用間接參照，因為要間接檢查授權資訊，所以可能會導致 Resource Server 和 Authorization Server 之間有額外的動作※。這種機制的細節，spec 裡面沒有定義。</p>
<p>Spec 沒有定義 token 的編碼方式，所以不提及保護 Token 的完整性 (integrity) 的詳細建議。若要實作保護完整性的措施，則該實作方式必須要可以防止 Token 被竄改。</p>
<p>※：原文是 <em>"between a server and the token issuer"</em></p>
<h3 id="access-token-chuan-shu-guo-cheng-wai-xie-pu-lu-min-gan-zi-liao-de-wen-ti">Access Token 傳輸過程外洩、曝露敏感資料的問題</h3>
<p>Token 傳輸過程可能被監聽而外洩，或 Token 本身可能會包含敏感資料※。</p>
<p>※在 Section 5.1 原文提及 "token disclosure" 的時候，僅提及曝露敏感資料，沒提及傳輸過程的外洩，然而 5.2 裡面關於 "token disclosure" 的對策，有一併提及傳輸過程外洩（中間人攻擊、監聽等），所以我寫這一段時，同時提及傳輸外洩以及曝露敏感資料。</p>
<p><em>Section 5.1 &gt; Token disclosure</em></p>
<h4 id="dui-ce-1">對策</h4>
<p>為了防範 Token 在傳輸過程外洩，必須用 TLS 來實作機密防護，且該實作方式必須要使用有提供機密防護和完整性防護的加密方式，如此就能要求 Client 與 Authorization Server 和 Client 與 Resource Server 之間的通訊要有機密防護和完整性防護。因為 TLS 是這份 spec 裡面規定一定要實作的，所以利用 TLS 來達成通訊過程的機密防護和完整性防護，是比較偏好的做法。</p>
<p>如果要防止 Client 取得 Token 的內容，那麼除了 TLS 之外，還必須實作 Token 加密。</p>
<p>要進一步防範 Token 外洩，則 Client 在發 request 的時候，還必須要驗證 TLS 的憑證鏈 (certificate chain) ，包括檢查憑證有沒有被撤銷（Certificate Revocation List, RFC 5280）。</p>
<p>Cookie 通常是明文傳輸的 (in the clear)，所以任何寫在裡面的資訊都有外洩的風險。所以， Bearer Token 絕對不可以存放在明文傳輸 cookie 裡面。詳見 RFC 6265 (HTTP State Management Mechanism) 裡面關於 cookie 的安全性問題。</p>
<p>某些部署方式，好比說利用 Load Balancer 的，TLS 傳輸在抵達 Resource Server 之前就結束了。這樣子會導致 Token 在前端 Load Balaner 和後端實體 Resrouce Server 之間，沒有加密保護。這種情況下，必須實作足夠的手段※，來確保前端和後端 server 之間的資料保密。Token 加密也是一種方式。</p>
<p>※ 原文為 <em>sufficient measures</em>，我不會翻譯…</p>
<h3 id="nuo-yong-access-token-de-wen-ti">挪用 Access Token 的問題</h3>
<p>壞人可能會把某個專門給 Resource Server A 的 Access Token ，挪用到 Resource Server B ，使得 B 誤信該 Access Token 可以拿來存取 B 的資料。</p>
<p><em>Section 5.1 &gt; Token redirect</em></p>
<h4 id="dui-ce-2">對策</h4>
<p>要防止 Token 被挪用，則這件事很重要：Authorization Server 核發的 Token 裡面要附上被核發人的資訊（通常是一或多部 Resource Server）。同時，也建議限制 Token 可以使用的 scope 範圍。</p>
<h3 id="er-du-li-yong-access-token-de-wen-ti">二度利用 Access Token 的問題</h3>
<p>壞人使用之前就存在的 Access Token 來存取 Reseouce Server （即：Token 被偷去用）。</p>
<p><em>Section 5.1 &gt; Token replay</em></p>
<h3 id="dui-ce-3">對策</h3>
<p>要防止 Token 被偷走並且拿來二度利用，建議採用以下方案：</p>
<ol>
<li>Token 的存活時間必須被限制住。一種手段是在 Token 受保護的區段裡面，設一個合法期間。使用短時效的 Token （如一小時以下）可以降低 Token 外洩的風險。</li>
</ol>
<ul>
<li>Token 在 Client ↔ Authorization Server 、 Client ↔ Resource Server 之間交換的時候，必須要實作機密防護。如此一來，就算在傳輸途徑上監聽，也無法獲得 Token ，也就無法二度利用。</li>
<li>Client 要向 Resource Server 出示 Token 的時候，Client 必須驗證 Resource Server 的真實身份，如 RFC 2818 (TLS) 的 Section 3.1 裡面所述。注意，Client 必須要驗證 TLS 憑證的憑證鏈 (certificate chain)。若 Resource Server 未經授權且未通過認證，或是憑證鏈驗證失敗，這時候向它出示 Token ，會導致敵手取得 Token 並且得到未經授權的權限來存取受保護的 resource。</li>
</ul>
<h2 id="an-quan-xing-jian-yi-de-zong-jie">安全性建議的總結</h2>
<p><em>Section 5.3 Summary of Recommendations</em></p>
<h3 id="yao-cang-hao-bearer-token">要藏好 Bearer Token</h3>
<p>Client 實作必須確保 Bearer Token 不會外洩給無關人士，因為他們可以以此來存取受保護的 resources。利用 Bearer Token 時，這是首要的安全性考量，且優先於其他更細節的建議。</p>
<h3 id="yao-yan-zheng-tls-de-ping-zheng-lian">要驗證 TLS 的憑證鏈</h3>
<p>當 Client 發 request 索取受保護的 resources 的時候，Client 必須驗證 TLS 的憑證鏈。若做不到的話，可能會引發 DNS 劫持，導致 Token 被壞人偷走。</p>
<h3 id="quan-cheng-shi-yong-tls-https">全程使用 TLS (https)</h3>
<p>當 Clients 利用 Bearer Token 發 request 時，Client 必須一直使用 TLS (RFC5246) (https) 或同等的安全傳輸。若做不到的話， Token 會曝露在各種攻擊方式，讓壞人可以得到意料之外的存取權。</p>
<h3 id="bu-yao-ba-bearer-token-cun-zai-cookie">不要把 Bearer Token 存在 Cookie</h3>
<p>絕對不可以把 Bearer Token 存在可以明文傳輸 (sent in the clear) 的 Cookie 裡面（明文傳輸是 cookie 傳輸的預設方式）。若存在 Cookie 裡面，必須要小心 Cross-Site Request Forgery 。</p>
<h3 id="yao-he-fa-duan-shi-xiao-de-bearer-token">要核發短時效的 Bearer Token</h3>
<p>核發 Token 的伺服器最好是核發短時效的 Bearer Token （一小時以內），尤其是發給跑在瀏覽器裡面的 Client ，或是其他容易發生資訊外洩的場合。利用短時效的 Bearer Token 可以降低 Token 外洩時的衝擊。</p>
<h3 id="yao-he-fa-you-qu-fen-shi-yong-fan-wei-de-bearer-token">要核發有區分使用範圍的 Bearer Token</h3>
<p>Token 伺服器最好要核發包含 audience restriction, scoping their use to the intended replying party, or set of relying party 的 Token 。</p>
<h3 id="bu-yao-yong-page-url-lai-chuan-song-bearer-token">不要用 Page URL 來傳送 Bearer Token</h3>
<p>Bearer Token 最好不要從 URL 來傳送（例如 query parameter），而最好是從有保密措施的 HTTP header 或是 body 來傳輸※。瀏覽器、伺服器等軟體可能不會把歷史記錄或資料結構給妥善加密。如果 Bearer Token 透過 URL 傳輸，則壞人就有可能可以從歷史記錄取得之。</p>
<p>※ <em>"be passed in HTTP message headers or message bodies for which confidentiality measures are taken"</em></p>
<hr />
<h2 id="oauth-2-0-xi-lie-wen-mu-lu">OAuth 2.0 系列文目錄</h2>
<ul>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-1-introduction/">(1) 世界觀</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">(2) Client 的註冊與認證</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-3-endpoints/">(3) Endpoints 的規格</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-1-auth-code-grant-flow/">(4.1) Authorization Code Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-2-implicit-grant-flow/">(4.2) Implicit Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-3-resource-owner-credentials-grant-flow/">(4.3) Resource Owner Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-4-client-credentials-grant-flow/">(4.4) Client Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-5-issuing-tokens/">(5) 核發與換發 Access Token</a></li>
<li><strong><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-6-bearer-token/">(6) Bearer Token 的使用方法</a> ← You Are Here</strong></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-7-security-considerations/">(7) 安全性問題</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-implementation-differences-among-famous-sites/">各大網站 OAuth 2.0 實作差異</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>OAuth 2.0 筆記 (5) 核發與換發 Access Token</title>
            <link>https://blog.yorkxin.org/posts/oauth2-5-issuing-tokens/</link>
            <pubDate>Mon, 30 Sep 2013 13:47:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/09/30/oauth2-5-issuing-tokens.html</guid>
            
            <description><![CDATA[
                <p>本文整理核發 Access Token (issuing) 與換發 Access Token (refreshing) 的規格。本來 Spec 裡面是分別寫在 Section 5 和 Section 6 的，不過因為 Endpoint 都是 Token Endpoint （除了 Implicit Grant Type），而且 Response 的規格相同，可以說是 Token Endpoint 的規格，所以我把他們寫在一起。</p>
<span id="continue-reading"></span>
<h2 id="response-de-gui-ge">Response 的規格</h2>
<p>Status Code: <code>200 OK</code></p>
<p>Header 裡面一定要有這些：</p>
<ul>
<li><code>Pragma: no-cache</code></li>
<li><code>Cache-Control: no-store</code> （如果有 Token 、有 Credential 或是其他敏感資料）</li>
</ul>
<p>回應的是 <a href="http://www.json.org/">JSON</a>，所以：</p>
<ul>
<li>參數要編碼成 JSON 格式</li>
<li>編碼完成的 JSON 放在 Response Body 裡面</li>
<li>字串 (String) 要符合 JSON String 格式（好比說有些字元要 escape）</li>
<li>數字 (Number) 要符合 JSON Number 格式</li>
</ul>
<p>每一個參數都放在 JSON 的第一層。</p>
<h2 id="he-fa-access-token">核發 Access Token</h2>
<p>如果 Access Token Request 合法且經授權，則 Authorization Server 會核發 Access Token 以及 Refresh Token （不一定有，看 Access Token 支援以及流程允許與否，例如 Implicit Grant Type 就禁止核發 Refresh Token）。</p>
<p>如果 Access Token Request 失敗，像是 Client 認證不過，或是 Request 不正確，那麼 Authorization Server 會回傳 Error ，本文最末提及。</p>
<p>以下是核發 Access Token 驗證程序都正確、要核發的時候的規格。</p>
<h3 id="can-shu">參數</h3>
<p>Client 收到不認識的參數必須忽略之。參數的長度（含 token）在 spec 裡面都不定義，Client 不可以自行瞎猜，Authorization Server 的文件裡面應該要提到。</p>
<table><thead><tr><th>參數名</th><th>必/選</th><th>填什麼/意義</th></tr></thead><tbody>
<tr><td>access_token</td><td>必</td><td>由 Authorization Server 核發的 Access Token 。</td></tr>
<tr><td>token_type</td><td>必</td><td>Token 的類型，例如 <code>Bearer</code> （見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-6-bearer-token/">系列文第 6 篇</a>）。</td></tr>
<tr><td>expires_in</td><td>建議有</td><td>幾秒過期，如 3600 表示 1 小時。若要省略，最好在文件裡註明。</td></tr>
<tr><td>scope</td><td>必*</td><td>Access Token 的授權範圍 (scopes)。</td></tr>
<tr><td>refresh_token</td><td>選</td><td>就是 Refresh Token</td></tr>
</tbody></table>
<p>其中 scope 如果和申請的不同則要附上，如果一樣的話就不必附上。</p>
<p>其中 refresh_token ，可以用來申請新的 Access Token。不一定會有，甚至 Client Credentials Grant Type 就建議不要發。</p>
<h3 id="fan-li">範例</h3>
<pre><code>HTTP&#x2F;1.1 200 OK
Content-Type: application&#x2F;json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  &quot;access_token&quot;:&quot;2YotnFZFEjr1zCsicMWpAA&quot;,
  &quot;token_type&quot;:&quot;example&quot;,
  &quot;expires_in&quot;:3600,
  &quot;refresh_token&quot;:&quot;tGzv3JOkF0XG5Qx2TlKWIA&quot;,
  &quot;example_parameter&quot;:&quot;example_value&quot;
}
</code></pre>
<h2 id="huan-fa-access-token">換發 Access Token</h2>
<p>換發 (Refreshing) Access Token ，指的是目前的 Access Token 過期、權限不足，而需要取得新的 Token 。可以換發的前提，的前提，是 Authorization Server 之前有核發 Refresh Token 。如果沒有，就不行。Client 可以自動做這件事（像是已知 Token 過期）。</p>
<p>Refresh Token 通常是會存在很久的 token ，且是用來拿取新的 Access Token 的，所以要綁定到被核發的 Client。</p>
<p>換發新的 Access Token 的時候，可以一併核發新的 Refresh Token ，這樣子的話 Client 必須把舊的 Refresh Token 丟掉，換成新的。同時， Authorization Server 也可以撤銷舊的 Refresh Token 。需注意新的 Refresh Token 其 scope 也要跟舊的 Refresh Token 一致。</p>
<p>換發 Access Token 的 Request 是發到 Token Endpoint ，用 POST。</p>
<h3 id="can-shu-1">參數</h3>
<table><thead><tr><th>參數名</th><th>必/選</th><th>填什麼/意義</th></tr></thead><tbody>
<tr><td>grant_type</td><td>必</td><td><code>refresh_token</code></td></tr>
<tr><td>refresh_token</td><td>必</td><td>就填 Refresh Token</td></tr>
<tr><td>scope</td><td>選</td><td>申請的存取範圍</td></tr>
</tbody></table>
<p>其中 scope 絕對不可以包含之前 Resource Owner 沒有授權過的。如果沒給這個參數，則直接沿用之前授權過的那些。</p>
<h3 id="authorization-server-de-chu-li-cheng-xu">Authorization Server 的處理程序</h3>
<p>這個 Request 進來的時候， Authorization Server 要做這些事：</p>
<ol>
<li>要求 Client 認證自己（如果是 Confidential Client 或有拿到 Client Credentials）</li>
</ol>
<ul>
<li>如果 Client 有出示認證資料，就認證它，細節見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">系列文第 2 篇</a></li>
<li>確定 Refresh Token 是發給 Client 的</li>
<li>驗證 Refresh Token 正確</li>
</ul>
<p>如果都沒正確，就照核發 Access Token 的方式回 Response （本文「核發 Access Token」一段）。</p>
<h3 id="fan-li-1">範例</h3>
<p>Client 向 Token Endpoint 換發新的 Access Token</p>
<pre><code>POST &#x2F;token HTTP&#x2F;1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application&#x2F;x-www-form-urlencoded

grant_type=refresh_token&amp;refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
</code></pre>
<h2 id="fa-sheng-cuo-wu-shi-de-hui-ying-fang-shi">發生錯誤時的回應方式</h2>
<p>Status Code: <code>400 Bad Request</code> （若無特別規定就用這個）</p>
<p>Header 和 Format (JSON) 跟核發的時候一樣。</p>
<h3 id="can-shu-2">參數</h3>
<p>基本上跟 Authorization Code Grant Flow 裡面的 Authorization Endopint 的錯誤參數一樣，差別在於 <code>error</code> 的錯誤代碼可以用的不一樣。</p>
<table><thead><tr><th>參數名</th><th>必/選</th><th>填什麼/意義</th></tr></thead><tbody>
<tr><td>error</td><td>必</td><td>錯誤代碼，其值後述。</td></tr>
<tr><td>error_description</td><td>選</td><td>人可讀的錯誤訊息，給 Client 開發者看的，不是給 End User 看的。ASCII 可見字元，除了雙引號和反斜線之外。</td></tr>
<tr><td>error_uri</td><td>選</td><td>一個 URI ，指向載有錯誤細節的網頁，要符合 URI 的格式。</td></tr>
</tbody></table>
<p>而 <code>error</code> 的值是以下的其中一個：</p>
<table><thead><tr><th>值</th><th>意義/用途</th></tr></thead><tbody>
<tr><td>invalid_request</td><td>欠缺必要的參數、有不正確的參數、有重複的參數、或其他原因導致無法解讀。</td></tr>
<tr><td>invalid_client</td><td>Client 認證失敗，如 Client 未知、沒送出 Client 認證、使用了 Server 不支援的認證方式。</td></tr>
<tr><td>invalid_grant</td><td>提出的 Grant 或是 Refresh Token 不正確、過期、被撤銷、Redirection URI 不符、不是給你這個 Client。</td></tr>
<tr><td>unauthorized_client</td><td>Client 沒有被授權可以使用這種方法來取得 Authorization Code。</td></tr>
<tr><td>unsupported_grant_type</td><td>Authorization Server 不支援使用這種 Grant Type （例如不支援 MAC）。</td></tr>
<tr><td>invalid_scope</td><td>所要求的 scope 不正確、未知、無法解讀。</td></tr>
</tbody></table>
<p>其中 <code>invalid_client</code> ：</p>
<ul>
<li>Status code 可以用 <code>401 Unauthorized</code></li>
<li>如果 Client 是用 <code>Authorization</code> header 來提交認證的，則回應必須用 <code>401</code> 加上 <code>WWW-Authenticate</code> ，其 value 要符合 Client 使用的 auth scheme （如 <code>Bearer</code>）</li>
</ul>
<p>與 Authorization Endpoint 的差別：</p>
<ul>
<li>多了 <code>invalid_client</code> ，因為有 Client 認證這個動作，而 Authorization Endpoint 則沒有。</li>
<li>多了 <code>invalid_grant</code> ，顯然，因為有傳 Grant 進來。</li>
<li>沒有 <code>unsupported_response_type</code> ，多了 <code>unsupported_grant_type</code> ，顯然，理由同上。</li>
<li>沒有 <code>server_error</code> 和 <code>temporarily_unavailable</code> ， Authorization Endpoint 會有，是因為 5xx 沒辦法轉址，而那個 Endpoint 需要轉址。在 Token Endpoint 沒有轉址的動作，所以不需要特別定義這兩個 error code 來提示伺服器錯誤，直接噴 5xx 就行了。（這是我個人的理解，不是寫在 spec 裡面）</li>
</ul>
<h3 id="fan-li-2">範例</h3>
<pre><code>HTTP&#x2F;1.1 400 Bad Request
Content-Type: application&#x2F;json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  &quot;error&quot;:&quot;invalid_request&quot;
}
</code></pre>
<hr />
<h2 id="oauth-2-0-xi-lie-wen-mu-lu">OAuth 2.0 系列文目錄</h2>
<ul>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-1-introduction/">(1) 世界觀</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">(2) Client 的註冊與認證</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-3-endpoints/">(3) Endpoints 的規格</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-1-auth-code-grant-flow/">(4.1) Authorization Code Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-2-implicit-grant-flow/">(4.2) Implicit Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-3-resource-owner-credentials-grant-flow/">(4.3) Resource Owner Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-4-client-credentials-grant-flow/">(4.4) Client Credentials Grant Flow 細節</a></li>
<li><strong><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-5-issuing-tokens/">(5) 核發與換發 Access Token</a> ← You Are Here</strong></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-6-bearer-token/">(6) Bearer Token 的使用方法</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-7-security-considerations/">(7) 安全性問題</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-implementation-differences-among-famous-sites/">各大網站 OAuth 2.0 實作差異</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>OAuth 2.0 筆記 (4.4) Client Credentials Grant Flow 細節</title>
            <link>https://blog.yorkxin.org/posts/oauth2-4-4-client-credentials-grant-flow/</link>
            <pubDate>Mon, 30 Sep 2013 13:46:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/09/30/oauth2-4-4-client-credentials-grant-flow.html</guid>
            
            <description><![CDATA[
                <p>即 Client ID + Client Secret 。適用於跑在 Server 的 Client 。</p>
<p>如果是以下情況的話，就可以使用這個流程：</p>
<ul>
<li>Client 自己就是 Resource Owner ，Client 取用的是自己擁有的 Protected Resources</li>
<li>Client is requesting access to protected resources based on an authorization previously arranged with the authorization server. （這個我看不懂，所以保留原文，求解釋…）</li>
</ul>
<p>這個流程只能用在 Confidential Client 。</p>
<p>這是 OAuth 2.0 內建的四個流程之一。相對於別的流程來說簡單很多。本文整理自 Section 4.4。</p>
<span id="continue-reading"></span>
<h2 id="liu-cheng-tu">流程圖</h2>
<pre><code>+---------+                                  +---------------+
|         |                                  |               |
|         |&gt;--(A)- Client Authentication ---&gt;| Authorization |
| Client  |                                  |     Server    |
|         |&lt;--(B)---- Access Token ---------&lt;|               |
|         |                                  |               |
+---------+                                  +---------------+

                Figure 6: Client Credentials Flow
</code></pre>
<p>(A) Client 向 Authorization Server 認證自己，並且發 Request 到 Token Endpoint</p>
<p>(B) Authorization Server 認證 Client ，如果正確的話，核發 Access Token。</p>
<h2 id="a-access-token-request">(A) Access Token Request</h2>
<p>【Client】POST ▶ 【Token Endpoint】</p>
<h3 id="can-shu">參數</h3>
<table><thead><tr><th>參數名</th><th>必/選</th><th>填什麼/意義</th></tr></thead><tbody>
<tr><td>grant_type</td><td>必</td><td><code>client_credentials</code></td></tr>
<tr><td>scope</td><td>選</td><td>申請的存取範圍</td></tr>
</tbody></table>
<h3 id="authorization-server-de-chu-li-cheng-xu">Authorization Server 的處理程序</h3>
<p>這個 Request 進來的時候， Authorization Server 必須認證 Client，細節見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">系列文第 2 篇</a>。（跟 Authorization Code Grant Type / Resource Owner Credentials Grant Type 不同，這個強制要求認證）</p>
<h3 id="fan-li">範例</h3>
<pre><code>POST &#x2F;token HTTP&#x2F;1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application&#x2F;x-www-form-urlencoded

grant_type=client_credentials
</code></pre>
<h2 id="b-access-token-response">(B) Access Token Response</h2>
<p>【Client】 ◀ 【Token Endpoint】</p>
<p>若 Access Token Request 合法且有經過授權，則核發 Access Token，但是最好不要核發 Refresh Token。如果 Client 認證失敗，或 Request 不合法，則依照 Section 5.2 的規定回覆錯誤。</p>
<p>詳細核發 Access Token 的細節寫在<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-5-issuing-tokens/">系列文第 5 篇</a>。</p>
<p>（除了「建議不要發 Refresh Token」這一點之外，大致上同 Authorization Code Grant Flow。）</p>
<h3 id="fan-li-1">範例</h3>
<pre><code>HTTP&#x2F;1.1 200 OK
Content-Type: application&#x2F;json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  &quot;access_token&quot;:&quot;2YotnFZFEjr1zCsicMWpAA&quot;,
  &quot;token_type&quot;:&quot;example&quot;,
  &quot;expires_in&quot;:3600,
  &quot;example_parameter&quot;:&quot;example_value&quot;
}
</code></pre>
<hr />
<h2 id="oauth-2-0-xi-lie-wen-mu-lu">OAuth 2.0 系列文目錄</h2>
<ul>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-1-introduction/">(1) 世界觀</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">(2) Client 的註冊與認證</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-3-endpoints/">(3) Endpoints 的規格</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-1-auth-code-grant-flow/">(4.1) Authorization Code Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-2-implicit-grant-flow/">(4.2) Implicit Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-3-resource-owner-credentials-grant-flow/">(4.3) Resource Owner Credentials Grant Flow 細節</a></li>
<li><strong><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-4-client-credentials-grant-flow/">(4.4) Client Credentials Grant Flow 細節</a> ← You Are Here</strong></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-5-issuing-tokens/">(5) 核發與換發 Access Token</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-6-bearer-token/">(6) Bearer Token 的使用方法</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-7-security-considerations/">(7) 安全性問題</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-implementation-differences-among-famous-sites/">各大網站 OAuth 2.0 實作差異</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>OAuth 2.0 筆記 (4.3) Resource Owner Password Credentials Grant Flow 細節 </title>
            <link>https://blog.yorkxin.org/posts/oauth2-4-3-resource-owner-credentials-grant-flow/</link>
            <pubDate>Mon, 30 Sep 2013 13:45:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/09/30/oauth2-4-3-resource-owner-credentials-grant-flow.html</guid>
            
            <description><![CDATA[
                <p>在 Resource Owner Password Credentials Grant Flow 流程裡， Resource Owner 自己的帳號密碼會直接用來當做 Authorization Grant ，並傳遞給 Authorization Server 來取得 Access Token 。這種流程只有在以下情況才能使用：</p>
<ul>
<li>Resource Owner 高度信賴 Client ，例如作業系統內建的應用程式（好比說 OS X 的 Twitter 整合）或是官方應用程式。</li>
<li>其他別的流程都不適用。</li>
</ul>
<p>而就算 Client 可以直接拿到 Resource Owner 的帳號密碼，也只會使用一次，用來取得 Access Token 。Spec 裡面定義的流程，會要求 Client 不儲存帳號密碼，而是隨後以長時效的 Access Token 或 Refresh Token 取代之。</p>
<p>Authorization Server 應該要特別小心開放這種流程，並且要在別的流程都行不通的時候才使用這種。</p>
<p>這種流程適用於可以取得 Resource Owner 帳號密碼的 Client （通常是透過一個輸入框）。也可以用來把以前的帳號密碼認證，遷移到 OAuth 認證。</p>
<p>最後拿到的除了 Access Token 之外，還會拿到 Refresh Token （Authorization Server 有支援的話）。</p>
<p>這是 OAuth 2.0 內建的四個流程之一。本文整理自 Section 4.3。</p>
<span id="continue-reading"></span>
<h3 id="liu-cheng-tu">流程圖</h3>
<pre><code>+----------+
| Resource |
|  Owner   |
|          |
+----------+
     v
     |    Resource Owner
    (A) Password Credentials
     |
     v
+---------+                                  +---------------+
|         |&gt;--(B)---- Resource Owner -------&gt;|               |
|         |         Password Credentials     | Authorization |
| Client  |                                  |     Server    |
|         |&lt;--(C)---- Access Token ---------&lt;|               |
|         |    (w&#x2F; Optional Refresh Token)   |               |
+---------+                                  +---------------+

       Figure 5: Resource Owner Password Credentials Flow
</code></pre>
<p>(A) Resource Owner 向 Client 提供真正的帳號密碼。</p>
<p>(B) Client 用 Resource Owner 的帳號密碼，向 Authorization Server 的 Token Endpoint 申請 Access Token。這個時候 Client 還要向 Authorization Server 認證自己。</p>
<p>(C) Authorization Server 認證 Client 、驗證 Resource Owner 的帳號密碼，如果正確的話，核發 Access Token。</p>
<h2 id="a-authorization-request-response">(A) Authorization Request &amp; Response</h2>
<p>在這個流程裡面， Authorization Grant 就是 Resource Owner 的帳號密碼，所以在 Step (A) 裡面直接向 Resource Onwer 索取，沒有經過網路來取得 Authorization。</p>
<p>Spec 不規定 Client 要怎麼拿到帳號密碼，但是 Client 取得 Access Token 之後，必須把 Resource Owner 的帳號密碼給銷毀掉。</p>
<h2 id="b-access-token-request">(B) Access Token Request</h2>
<p>【Client】POST ▶ 【Token Endpoint】</p>
<h3 id="can-shu">參數</h3>
<table><thead><tr><th>參數名</th><th>必/選</th><th>填什麼/意義</th></tr></thead><tbody>
<tr><td>grant_type</td><td>必</td><td><code>password</code></td></tr>
<tr><td>username</td><td>必</td><td>Resource Owner 的帳號</td></tr>
<tr><td>password</td><td>必</td><td>Resource Owner 的密碼</td></tr>
<tr><td>scope</td><td>選</td><td>申請的存取範圍</td></tr>
</tbody></table>
<h3 id="authorization-server-de-chu-li-cheng-xu">Authorization Server 的處理程序</h3>
<p>這個 Request 進來的時候， Authorization Server 要做這些事：</p>
<ol>
<li>要求 Client 認證自己（如果是 Confidential Client 或有拿到 Client Credentials）</li>
</ol>
<ul>
<li>如果 Client 有出示認證資料，就認證它，細節見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">系列文第 2 篇</a></li>
<li>驗證 Resource Owner 的帳號密碼（以既有的驗證方式）</li>
</ul>
<h3 id="shen-fang-bao-li-po-jie">慎防暴力破解</h3>
<p>因為牽涉到帳號密碼，所以 Authorization Server 要可以防 Endpoint 被暴力破解，具體實施的方法像是 Rate Limiting 或是發出警告。</p>
<h3 id="fan-li">範例</h3>
<pre><code>POST &#x2F;token HTTP&#x2F;1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application&#x2F;x-www-form-urlencoded

grant_type=password&amp;username=johndoe&amp;password=A3ddj3w
</code></pre>
<h2 id="c-access-token-response">(C) Access Token Response</h2>
<p>（同 Authorization Code Grant Flow。）</p>
<p>【Client】 ◀ 【Token Endpoint】</p>
<p>若 Access Token Request 合法且有經過授權，則核發 Access Token，同時可以核發 Refresh Token （非必備）。如果 Client 認證失敗，或 Request 不合法，則依照 Section 5.2 的規定回覆錯誤。</p>
<p>詳細核發 Access Token 的細節寫在<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-5-issuing-tokens/">系列文第 5 篇</a>。</p>
<h3 id="fan-li-1">範例</h3>
<p>發給 Access Token：</p>
<pre><code>HTTP&#x2F;1.1 200 OK
Content-Type: application&#x2F;json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  &quot;access_token&quot;:&quot;2YotnFZFEjr1zCsicMWpAA&quot;,
  &quot;token_type&quot;:&quot;example&quot;,
  &quot;expires_in&quot;:3600,
  &quot;refresh_token&quot;:&quot;tGzv3JOkF0XG5Qx2TlKWIA&quot;,
  &quot;example_parameter&quot;:&quot;example_value&quot;
}
</code></pre>
<h2 id="an-quan-xing-wen-ti-section-10-7">安全性問題 (Section 10.7)</h2>
<h3 id="zhang-hao-mi-ma-wai-xie">帳號密碼外洩</h3>
<p>Resource Owner Password Credentials Grant Type 通常是用在老舊 Client ，或是遷移舊的認證機制到 OAuth。雖然這種流程降低了在 Client 裡面儲存帳號密碼所引來的風險，但是沒有消除把帳號密碼給 Client 看的必要性。（編按：第一步還是需要 Resonrce Owner 提供帳號密碼）</p>
<p>這個流程的風險比起其他流程還要高，因為它保留了使用密碼的 anti-pattern，而這個卻是 OAuth spec 致力避免的。Client 可能會濫用密碼，或是密碼會不經意地洩漏給壞人（例如 Log 或是其他 Client 保存的記錄）。</p>
<h3 id="resource-owner-wu-fa-kong-zhi-shou-quan-quan-xian-yu-cun-qu-fan-wei">Resource Owner 無法控制授權權限與存取範圍</h3>
<p>此外，因為 Resource Owner 沒辦法控制授權的流程（Resource Owner 只參與到輸入帳號密碼），Client 可以取得比 Resource Owner 期望的權限 (scopes) 還要更多的權限。Authorization Server 在透過這種流程合法 Access Token 的時候應該要慎重考慮 scope 和時效的問題。</p>
<p>Authorization Server 和 Client 應該要儘量不使用這種流程，改用其他流程。</p>
<hr />
<h2 id="oauth-2-0-xi-lie-wen-mu-lu">OAuth 2.0 系列文目錄</h2>
<ul>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-1-introduction/">(1) 世界觀</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">(2) Client 的註冊與認證</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-3-endpoints/">(3) Endpoints 的規格</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-1-auth-code-grant-flow/">(4.1) Authorization Code Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-2-implicit-grant-flow/">(4.2) Implicit Grant Flow 細節</a></li>
<li><strong><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-3-resource-owner-credentials-grant-flow/">(4.3) Resource Owner Credentials Grant Flow 細節</a> ← You Are Here</strong></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-4-client-credentials-grant-flow/">(4.4) Client Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-5-issuing-tokens/">(5) 核發與換發 Access Token</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-6-bearer-token/">(6) Bearer Token 的使用方法</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-7-security-considerations/">(7) 安全性問題</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-implementation-differences-among-famous-sites/">各大網站 OAuth 2.0 實作差異</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>OAuth 2.0 筆記 (4.2) Implicit Grant Flow 細節</title>
            <link>https://blog.yorkxin.org/posts/oauth2-4-2-implicit-grant-flow/</link>
            <pubDate>Mon, 30 Sep 2013 13:44:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/09/30/oauth2-4-2-implicit-grant-flow.html</guid>
            
            <description><![CDATA[
                <p>在 Implicit Grant Flow 裡，Authorization Server 直接向 Client 核發 Access Token ，而不像 Authorization Code Grant Flow ，先核發 Grant ，再另外去拿 Access Token。</p>
<p>Authorization Server 核發 Access Token 的時候，不認證 Client （其實也無法認證），在某些情況下，可以用 Redirection URI 來確保 Access Token 只發給正確的 Client 。這種流程依賴 Resource Owner 本人的存在，以及事先設定的 Redirection URI。</p>
<p>這種流程是專門為特定的 Public Client 來優化的，例如跑在 Browser 裡面的應用程式。但也因此有外洩風險，例如：</p>
<ul>
<li>Resource Owner 可以看到 Access Token</li>
<li>其他可以存取 User-Agent 的應用程式，也可以看到 Access Token</li>
<li>Access Token 傳輸時，會直接出現在 Redirection URI 裡面，所以 Resource Owner 以及同一台設備的應用程式可以看到</li>
</ul>
<p>因為需要實施轉址，所以 Client 要可以跟 Resource Owner 的 User-Agent (Browser) 互動，也要可以接收從 Authorization Server 來的 Redirection Request。（同 Authorization Code Grant Flow）</p>
<p>最後拿到的只有 Access Token ，不會拿到 Refresh Token （禁止核發 Refresh Token）。</p>
<p>這是 OAuth 2.0 內建的四個流程之一。本文整理自 Section 4.2。</p>
<span id="continue-reading"></span>
<h2 id="liu-cheng-tu">流程圖</h2>
<pre><code>+----------+
| Resource |
|  Owner   |
|          |
+----------+
     ^
     |
    (B)
+----|-----+          Client Identifier     +---------------+
|         -+----(A)-- &amp; Redirection URI ---&gt;|               |
|  User-   |                                | Authorization |
|  Agent  -|----(B)-- User authenticates --&gt;|     Server    |
|          |                                |               |
|          |&lt;---(C)--- Redirection URI ----&lt;|               |
|          |          with Access Token     +---------------+
|          |            in Fragment
|          |                                +---------------+
|          |----(D)--- Redirection URI ----&gt;|   Web-Hosted  |
|          |          without Fragment      |     Client    |
|          |                                |    Resource   |
|     (F)  |&lt;---(E)------- Script ---------&lt;|               |
|          |                                +---------------+
+-|--------+
  |    |
 (A)  (G) Access Token
  |    |
  ^    v
+---------+
|         |
|  Client |
|         |
+---------+

註: (A), (B) 這兩步的線拆成兩段，因為會經過 user-agent

                    Figure 4: Implicit Grant Flow
</code></pre>
<p>(A) Client 把 Resource Owner 的 User-Agent 轉到 Authorization Endpoint 來啟動流程。Client 會傳送：</p>
<ul>
<li>Client ID</li>
<li>申請的 scopes</li>
<li>內部 state</li>
<li>Redirection URI，申請結果下來之後 Authorization Server 要轉址過去。</li>
</ul>
<p>(B) Authorization Server 通過 User-Agent 認證 Resource Owner，並確定 Resource Onwer 許可或駁回Client 的存取申請。</p>
<p>(C) 假設 Resource Owner 許可了存取申請， Authorization Server 會把 User-Agent 轉回去先前指定的 Redirection URI ，其中包含 Access Token ，放在 Fragment Component 裡面。</p>
<p>(D) User-Agent 跟隨轉址的指示，發出 Request 到 Web-Hosted Client Resource ，這個 Request 裡面不會有剛剛拿到的 Fragment ， User-Agent 自己保留 Fragment 。（註）</p>
<p>(E) Web-Hosted Client Resource 回傳一個網頁（HTML &amp; JavaScript），這個網頁可以拿到完整的 Redirection URI （含先前 User-Agent 保留的 Fragment）、把 Fragment 裡面的 Access Token 和其他參數給解出來。（註）</p>
<p>(F) User-Agent 執行從 Web-Hosted Client Resource 來的 Script 把 Access Token 解出來。</p>
<p>(G) User-Agent 把 Access Token 傳給 Client。</p>
<p>註： (D) 之後的有點抽象，我的理解是這樣：</p>
<ul>
<li>Web-Hosted Client Resource 當作你自己架的 App Server ，在上面開 Redirection Endpoint ，所以這個流程其實 Client 本體 (JavaScript App) 沒有 Endpoint ，Endpoint 是開在一個 HTTP(s) Server 上面。</li>
<li>Browser 事實上在 Access <code>http://example.com/cb#access_token=123</code> 的時候，只會發送 <code>http://example.com/cb</code> 的 request ，在 Request 裡面不會有 <code>#access_token=123</code></li>
<li>所以 (D) 所謂「Request 不含 Fragment，User-Agent 自己保留 Fragment」這一步是 User-Agent 自動做的， Client 開發者不需要用 JavaScript 特別處理，只要把 Redirection Endpoint 指定給自己的 App Server 就可以了。</li>
<li>而 (E) 所謂「回傳一個網頁來解出 User-Agent 保留的 Fragment」，就是說 User-Agent 打 Request 到 Redirection URI （含 Fragment，但不會傳送到 Server）的時候，他的 response 裡面包含 JavaScript ，而上面說了，Fragment 是自動保留在 User-Agent 的，所以這個 Response 在 Server 那邊不會知道有 Fragment 的存在，也就不會知道 Access Token 的存在，而是 User-Agent 才知道。</li>
<li>所以 (F) 就是跑這個 script 解出 Access Token 和參數，(G) 把 (F) 的執行結果塞給 Client (JavaScript App)。</li>
</ul>
<p>也就是說其實是設計給不能聽 Redirection Endpoint 的 In-Browser JavaScript App 的解法。我看到的用法是 <a href="https://developers.google.com/accounts/docs/OAuth2UserAgent">Google 的 OAuth 2.0 for Client-side JavaScript</a>。</p>
<h2 id="a-authorization-request">(A) Authorization Request</h2>
<p>【User-Agent】GET ▶【Authorization Endpoint】</p>
<p>第一步是 Client 產生一個 URL 連到 Authorization Endpoint ，要 Resource Owner 打開（點擊）這個 URL ，從而產生「向 Authorization Endpoint 發送 GET request」的操作。</p>
<p>把參數包在 URI 的 query component 裡面。</p>
<h3 id="can-shu">參數</h3>
<table><thead><tr><th>參數名</th><th>必/選</th><th>填什麼/意義</th></tr></thead><tbody>
<tr><td>response_type</td><td>必</td><td><code>token</code></td></tr>
<tr><td>client_id</td><td>必</td><td>自己的 Client ID</td></tr>
<tr><td>state</td><td>建議有</td><td>內部狀態</td></tr>
<tr><td>redirect_uri</td><td>選</td><td>申請結果下來之後要轉址去哪裡</td></tr>
<tr><td>scope</td><td>選</td><td>申請的存取範圍</td></tr>
</tbody></table>
<p>其中的 state， Authorization Server 轉回 Client 的時候會附上。可以防範 CSRF ，所以最好是加上這個值，詳見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-7-security-considerations/">系列文第 7 篇</a>關於 CSRF 的安全性問題。</p>
<h3 id="authorization-server-de-chu-li-cheng-xu">Authorization Server 的處理程序</h3>
<p>因為 Implicit Grant Flow 是直接在 Authorization Endpoint 發 Access Token ，所以資料驗證和授權都在這一步處理。所以這個 Request 進來的時候， Authorization Server 要做這些事：</p>
<ol>
<li>驗證所有必須給的參數都有給且合法</li>
</ol>
<ul>
<li>Redirection URI 與預先在 Authorization Server 設定的相符。</li>
</ul>
<p>如果沒問題，就詢問 Resource Owner 是否授權，即 (B) 步驟。</p>
<h2 id="c-authorization-response">(C) Authorization Response</h2>
<p>【Client】 ◀ 302【Authorization Endpoint】</p>
<p>是 Resource Owner 在 (B) 決定授權與否之後回應的 Response。</p>
<p>在 (B) 裡面， Resource Owner 若同意授權，這個「同意授權」的 request 會往 Authorization Endpoint 發送，接著會收到 302 的轉址 response ，裡面帶有「前往 Client 的 Redirection Endpoint 的 URL」的轉址 (Location header)，從而產生「向 Redirection URI 發送 GET Request」的操作。</p>
<p>參數要用 URL Encoding 編起來，放在 Fragment Component 裡面。</p>
<p>若 Access Token Request 合法且有經過授權，則核發 Access Token。如果 Client 認證失敗，或 Request 不合法，則依照 Section 5.2 的規定回覆錯誤。</p>
<p>特別注意 Implicit Grant Type <strong>禁止</strong> 核發 Refresh Token。</p>
<p>某些 User-Agent 不支援 Fragment Redirection ，這種情況可以使用間接轉址，即是轉到一個頁面，放一個 "Continue" 的按鈕，按下去連到真正的 Redirection URI 。</p>
<h3 id="can-shu-1">參數</h3>
<table><thead><tr><th>參數名</th><th>必/選</th><th>填什麼/意義</th></tr></thead><tbody>
<tr><td>access_token</td><td>必</td><td>即 Access Token</td></tr>
<tr><td>expires_in</td><td>建議有</td><td>幾秒過期，如 3600 表示 10 分鐘。若要省略，最好在文件裡註明效期。</td></tr>
<tr><td>scope</td><td>必*</td><td>Access Token 的授權範圍 (scopes)。</td></tr>
<tr><td>state</td><td>必*</td><td>原內部狀態。</td></tr>
</tbody></table>
<p>其中 scope 如果和 (A) 申請的不同則要附上，如果一樣的話就不必附上。</p>
<p>其中 state 如果 (A) 的時候有附上，則 Resopnse 裡面必須有，完全一致的原值。如果原本就沒有，就不需要回傳。</p>
<p>Access Token 的長度由 Authorization Server 定義，應寫在文件中， Client 不可以瞎猜。</p>
<p>Client 遇到不認識的參數必須忽略。</p>
<h3 id="fan-li">範例</h3>
<pre><code>HTTP&#x2F;1.1 302 Found
Location: http:&#x2F;&#x2F;example.com&#x2F;cb#access_token=2YotnFZFEjr1zCsicMWpAA
          &amp;state=xyz&amp;token_type=example&amp;expires_in=3600
</code></pre>
<h3 id="cuo-wu-fa-sheng-shi-de-chu-li-fang-shi">錯誤發生時的處理方式</h3>
<p>跟 Authorization Code Grant Flow 相同，差別在於錯誤的內容是放在 Fragment Component 而不是 Query Component。請參考<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-1-auth-code-grant-flow/">系列文第 4.1 篇</a>關於 Authorization Code Grant Flow 的 Access Token Request 錯誤處理原則。</p>
<p>例如：</p>
<pre><code>HTTP&#x2F;1.1 302 Found
Location: https:&#x2F;&#x2F;client.example.com&#x2F;cb#error=access_denied&amp;state=xyz
</code></pre>
<h2 id="an-quan-xing-wen-ti">安全性問題</h2>
<p>在 spec 裡面提及的安全性問題寫在 Section 10.3 和 10.16 ，其中 10.3 只是特別提到 Implicit Grant Type 「透過 URI Fragment 來傳 Access Token ，所以可能會外洩」，而 10.16 則是針對 Implicit Grant Type 可能會有偽造 Resource Owner 的安全性問題。其中 10.3 關於 Access Token 保密的問題，見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-7-security-considerations/">系列文第 7 篇</a>。</p>
<h3 id="wu-yong-access-token-lai-zai-implicit-flow-li-mian-wei-zhuang-resource-owner-section-10-16">誤用 Access Token 來在 Implicit Flow 裡面偽裝 Resource Owner (Section 10.16)</h3>
<p>這個 Section 的原文我看不太懂，似乎是在說，這流程裡面會有漏洞讓壞人可以置換 Access Token ，原本是要給 A Client 的 Token 到了 B Client 的手上。<a href="https://images-na.ssl-images-amazon.com/images/G/01/lwa/dev/docs/website-developer-guide._TTH_.pdf">Amazon 的文件</a> 裡面有提到，他的建議是，在真的拿 Token 來用之前，要去 Authorization Server 問一下是不是真是給這個 Client 用的，不是的話就不能用。</p>
<p>新浪微博 API 的<a href="http://open.weibo.com/wiki/%E5%BA%94%E7%94%A8%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9#.E7.94.A8.E6.88.B7.E8.BA.AB.E4.BB.BD.E4.BC.AA.E9.80.A0">「用户身份伪造」</a>應該也是在講類似的事。</p>
<hr />
<h2 id="oauth-2-0-xi-lie-wen-mu-lu">OAuth 2.0 系列文目錄</h2>
<ul>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-1-introduction/">(1) 世界觀</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">(2) Client 的註冊與認證</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-3-endpoints/">(3) Endpoints 的規格</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-1-auth-code-grant-flow/">(4.1) Authorization Code Grant Flow 細節</a></li>
<li><strong><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-2-implicit-grant-flow/">(4.2) Implicit Grant Flow 細節</a> ← You Are Here</strong></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-3-resource-owner-credentials-grant-flow/">(4.3) Resource Owner Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-4-client-credentials-grant-flow/">(4.4) Client Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-5-issuing-tokens/">(5) 核發與換發 Access Token</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-6-bearer-token/">(6) Bearer Token 的使用方法</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-7-security-considerations/">(7) 安全性問題</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-implementation-differences-among-famous-sites/">各大網站 OAuth 2.0 實作差異</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>OAuth 2.0 筆記 (4.1) Authorization Code Grant Flow 細節</title>
            <link>https://blog.yorkxin.org/posts/oauth2-4-1-auth-code-grant-flow/</link>
            <pubDate>Mon, 30 Sep 2013 13:43:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/09/30/oauth2-4-1-auth-code-grant-flow.html</guid>
            
            <description><![CDATA[
                <p>在 Authorization Grant Code Flow 裡，Client 不直接向 Resource Owner 要求許可，而是把 Resource Owner 導去 Authorization Server 要求許可， Authorization Server 再透過轉址來告訴 Client 授權許可的代碼 (code) 。在轉址回去之前， Authorization Server 會先認證 Resource Owner 並取得授權。因為 Resource Owner 只跟 Authorization Server 認證，所以 Client 絕對不會拿到 Resource Owner 的帳號密碼。</p>
<p>在這種流程裡， Authorization Grant Code 會以一個字串 (string) 具體存在，並且傳遞給 Client ，做為 Authorization Grant （Resource Owner 的授權許可）。在取得 Grant 之後，還沒有取得 Access Token ，Client 要再自己去向 Authorization Server 取得 Access Token 。</p>
<p>這個流程是專門為在 Server 執行的 Confidential Client 優化的。</p>
<p>因為需要實施轉址，所以 Client 要可以跟 Resource Owner 的 User-Agent (Browser) 互動，也要可以接收從 Authorization Server 來的 Redirection Request。</p>
<p>最後拿到的除了 Access Token 之外，還會拿到 Refresh Token （Authorization Server 有支援的話）。</p>
<p>這是 OAuth 2.0 內建的四個流程之一。本文整理自 Section 4.1。</p>
<span id="continue-reading"></span>
<h2 id="liu-cheng-tu">流程圖</h2>
<pre><code>+----------+
| Resource |
|   Owner  |
|          |
+----------+
     ^
     |
    (B)
+----|-----+          Client Identifier      +---------------+
|         -+----(A)-- &amp; Redirection URI ----&gt;|               |
|  User-   |                                 | Authorization |
|  Agent  -+----(B)-- User authenticates ---&gt;|     Server    |
|          |                                 |               |
|         -+----(C)-- Authorization Code ---&lt;|               |
+-|----|---+                                 +---------------+
  |    |                                         ^      v
 (A)  (C)                                        |      |
  |    |                                         |      |
  ^    v                                         |      |
+---------+                                      |      |
|         |&gt;---(D)-- Authorization Code ---------&#x27;      |
|  Client |          &amp; Redirection URI                  |
|         |                                             |
|         |&lt;---(E)----- Access Token -------------------&#x27;
+---------+       (w&#x2F; Optional Refresh Token)

註: (A), (B), (C) 這三步的線拆成兩段，因為會經過 user-agent

                  Figure 3: Authorization Code Flow
</code></pre>
<p>(A) Client 把 Resource Owner 的 User-Agent 轉到 Authorization Endpoint 來啟動流程。Client 會傳送：</p>
<ul>
<li>Client ID</li>
<li>申請的 scopes</li>
<li>內部 state</li>
<li>Redirection URI，申請結果下來之後 Authorization Server 要轉址過去。</li>
</ul>
<p>(B) Authorization Server 通過 User-Agent 認證 Resource Owner，並確定 Resource Onwer 許可或駁回Client 的存取申請。</p>
<p>(C) 假設 Resource Owner 許可了存取申請， Authorization Server 會把 User-Agent 轉回去先前指定的 Redirection URI ，其中包含了：</p>
<ul>
<li>Authorization Code</li>
<li>許可的 scopes （如果跟申請的不一樣才會附上）</li>
<li>先前提供的內部 state （原封不動，如果先前有提供才會附上）</li>
</ul>
<p>(D) Client 向 Authorization Server 的 Token Endpoint 要求 Access Token，申請時會傳送：</p>
<ul>
<li>先前取得的 Authorization Code</li>
<li>Redirection URI，用來驗證和之前 (C) 時的一致。</li>
<li>Client 的認證資料</li>
</ul>
<p>(E) Authorization Server 認證 Client 、驗證 Authorization Code、並確認 Redirection URI 和之前 (C) 轉址的一致。都符合的話，Authorization Server 會回傳 Access Token ，以及可選的 Refresh Token。</p>
<h2 id="a-authorization-request">(A) Authorization Request</h2>
<p>【User-Agent】GET ▶【Authorization Endpoint】</p>
<p>第一步是 Client 產生一個 URL 連到 Authorization Endpoint ，要 Resource Owner 打開（點擊）這個 URL ，從而產生「向 Authorization Endpoint 發送 GET request」的操作。</p>
<p>把參數包在 URI 的 query components 裡面。</p>
<h3 id="can-shu">參數</h3>
<table><thead><tr><th>參數名</th><th>必/選</th><th>填什麼/意義</th></tr></thead><tbody>
<tr><td>response_type</td><td>必</td><td><code>code</code></td></tr>
<tr><td>client_id</td><td>必</td><td>自己的 Client ID</td></tr>
<tr><td>state</td><td>建議有</td><td>內部狀態</td></tr>
<tr><td>redirect_uri</td><td>選</td><td>申請結果下來之後要轉址去哪裡</td></tr>
<tr><td>scope</td><td>選</td><td>申請的存取範圍</td></tr>
</tbody></table>
<p>其中的 state， Authorization Server 轉回 Client 的時候會附上。可以防範 CSRF ，所以最好是加上這個值，詳見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-7-security-considerations/">系列文第 7 篇</a>關於 CSRF 的安全性問題。</p>
<h3 id="fan-li">範例</h3>
<pre><code>GET &#x2F;authorize?response_type=code&amp;client_id=s6BhdRkqt3&amp;state=xyz
    &amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP&#x2F;1.1
Host: server.example.com
</code></pre>
<h2 id="c-authorization-response">(C)  Authorization Response</h2>
<p>【Authorization Endpoint】 302 Response ▷ 【User-Agent】▶ GET 【Client: Redirection Endpoint】</p>
<p>是 Resource Owner 在 (B) 決定授權與否之後回應的 Response。</p>
<p>在 (B) 裡面， Resource Owner 若同意授權，這個「同意授權」的 request 會往 Authorization Endpoint 發送，接著會收到 302 的轉址 response ，裡面帶有「前往 Client 的 Redirection Endpoint 的 URL」的轉址 (Location header)，從而產生「向 Redirection URI 發送 GET Request」的操作。</p>
<h3 id="can-shu-1">參數</h3>
<table><thead><tr><th>參數名</th><th>必/選</th><th>填什麼/意義</th></tr></thead><tbody>
<tr><td>code</td><td>必</td><td>Authorization Code</td></tr>
<tr><td>state</td><td>必*</td><td>原內部狀態</td></tr>
</tbody></table>
<p>其中 state 如果 (A) 的時候有附上，則 Resopnse 裡面必須有，完全一致的原值。如果原本就沒有，就不需要回傳。</p>
<p>其中 Authorization Code：</p>
<ul>
<li>必須是短時效的，建議最長 10 分鐘。</li>
<li>Client 只能使用一次，如果重複使用，Authorization Server 必須拒絕，並且建議撤銷之前透過這個 Grant 核發過的 Tokens</li>
<li>要綁定 Code ↔ Client ID ↔ Redirection URI 的關係</li>
<li>長度由 Authorization Server 定義，應寫在文件中， Client 不可以瞎猜。</li>
</ul>
<p>Client 遇到不認識的參數必須忽略。</p>
<h3 id="fan-li-1">範例</h3>
<pre><code>HTTP&#x2F;1.1 302 Found
Location: https:&#x2F;&#x2F;client.example.com&#x2F;cb?code=SplxlOBeZQQYbYS6WxSbIA
          &amp;state=xyz
</code></pre>
<h3 id="cuo-wu-fa-sheng-shi-de-hui-ying-fang-shi">錯誤發生時的回應方式</h3>
<p>如果發生的錯誤是：</p>
<ul>
<li>Redirection URI 沒給、不正確、沒註冊過</li>
<li>Client ID 沒給、不正確</li>
</ul>
<p>則 Authorization Server 應該告知 Resource Owner 這個錯誤，並且絕對不可以自動轉址到錯誤的 Redirection URI。</p>
<p>如果發生的錯誤是因為 Resource Owner 拒絕授權或是因為除了 Redirection URI 不正確的原因，那麼 Authorization Server 要告知 Client ，方法是把錯誤內容放在 Redireciton URI 的 Query Component 裡面，用 URL Encoding 編碼過，可用的參數為：</p>
<table><thead><tr><th>參數名</th><th>必/選</th><th>填什麼/意義</th></tr></thead><tbody>
<tr><td>error</td><td>必</td><td>錯誤代碼，其值後述。</td></tr>
<tr><td>error_description</td><td>選</td><td>人可讀的錯誤訊息，給 Client 開發者看的，不是給 End User 看的。<br>ASCII 可見字元，除了雙引號和反斜線之外。</td></tr>
<tr><td>error_uri</td><td>選</td><td>一個 URI ，指向載有錯誤細節的網頁，要符合 URI 的格式。</td></tr>
<tr><td>state</td><td>必*</td><td>原內部狀態</td></tr>
</tbody></table>
<p>其中 state 如果 (A) 的時候有附上，則 Resopnse 裡面必須有，完全一致的原值。如果原本就沒有，就不需要回傳。</p>
<p>而 <code>error</code> 的值是以下的其中一個：</p>
<table><thead><tr><th>值</th><th>意義/用途</th></tr></thead><tbody>
<tr><td>invalid_request</td><td>欠缺必要的參數、有不正確的參數、有重複的參數、或其他原因導致無法解讀。</td></tr>
<tr><td>unauthorized_client</td><td>Client 沒有被授權可以使用這種方法來取得 Authorization Code。</td></tr>
<tr><td>access_denied</td><td>Resource Owner 或 Authorization Owner 拒絕授權的申請。</td></tr>
<tr><td>unsupported_response_type</td><td>Authorization Server 不支援使用這種方法取得 Authorization Code。</td></tr>
<tr><td>invalid_scope</td><td>所要求的 scope 不正確、未知、無法解讀。</td></tr>
<tr><td>server_error</td><td>Authorization Server 遇到意外的情況而無法處理請求。</td></tr>
<tr><td>temporarily_unavailable</td><td>Authorization Server 因為過載或維修中而暫時無法處理請求。</td></tr>
</tbody></table>
<p>其中 server_error 和 temporarily_unavailable 有必要，因為 5xx 系列的 status code 不能轉址。</p>
<h2 id="d-access-token-request">(D) Access Token Request</h2>
<p>【Client】POST ▶ 【Token Endpoint】</p>
<h3 id="can-shu-2">參數</h3>
<table><thead><tr><th>參數名</th><th>必/選</th><th>填什麼/意義</th></tr></thead><tbody>
<tr><td>grant_type</td><td>必</td><td><code>authorization_code</code></td></tr>
<tr><td>code</td><td>必</td><td>在 (C) 拿到的 Authorization Code</td></tr>
<tr><td>redirect_uri</td><td>必</td><td>如果 (A) 有提供，則必須提供一模一樣的。</td></tr>
<tr><td>client_id</td><td>必*</td><td>自己的 Client ID （Public Client 才要填）。</td></tr>
</tbody></table>
<p>其中 client_id 只有 Public Client 才需要提供，如果是 Confidential Client 或有拿到 Client Credentials ，就必須進行 Client 認證，細節見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">系列文第 2 篇</a>。</p>
<h3 id="authorization-server-de-chu-li-cheng-xu">Authorization Server 的處理程序</h3>
<p>這個 Request 進來的時候， Authorization Server 要做這些事：</p>
<ol>
<li>要求 Client 認證自己（如果是 Confidential Client 或有拿到 Client Credentials）</li>
</ol>
<ul>
<li>如果 Client 有出示認證資料，就認證它，細節見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">系列文第 2 篇</a></li>
<li>確定 Authorization Code 是發給 Client 的
<ul>
<li>Confidential: 用 Client 的認證過程來證明</li>
<li>Public: 用 Client ID 來證明</li>
</ul>
</li>
<li>驗證 Authorization Code 正確</li>
<li>如果 (A) 有給 Redirection URI 的話，確定這次給的 Redirection URI 與 (A) 時的一模一樣。</li>
</ul>
<h3 id="fan-li-2">範例</h3>
<pre><code>POST &#x2F;token HTTP&#x2F;1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application&#x2F;x-www-form-urlencoded

grant_type=authorization_code&amp;code=SplxlOBeZQQYbYS6WxSbIA
&amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
</code></pre>
<h2 id="e-access-token-response">(E) Access Token Response</h2>
<p>【Client】 ◀ 【Token Endpoint】</p>
<p>若 Access Token Request 合法且有經過授權，則核發 Access Token，同時可以核發 Refresh Token （非必備）。如果 Client 認證失敗，或 Request 不合法，則依照 Section 5.2 的規定回覆錯誤。</p>
<p>詳細核發 Access Token 的細節寫在<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-5-issuing-tokens/">系列文第 5 篇</a>。</p>
<h3 id="fan-li-3">範例</h3>
<p>發給 Access Token：</p>
<pre><code>HTTP&#x2F;1.1 200 OK
Content-Type: application&#x2F;json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  &quot;access_token&quot;:&quot;2YotnFZFEjr1zCsicMWpAA&quot;,
  &quot;token_type&quot;:&quot;example&quot;,
  &quot;expires_in&quot;:3600,
  &quot;refresh_token&quot;:&quot;tGzv3JOkF0XG5Qx2TlKWIA&quot;,
  &quot;example_parameter&quot;:&quot;example_value&quot;
}
</code></pre>
<h2 id="an-quan-xing-wen-ti">安全性問題</h2>
<h3 id="authorization-code-de-an-quan-xing-wen-ti-section-10-5">Authorization Code 的安全性問題 (Section 10.5)</h3>
<h4 id="authorization-code-bei-tou">Authorization Code 被偷</h4>
<p>Authorization Code 的傳輸應該要經由安全通道，特別是如果 Client 的 Redirection URI 是指向網路資源（根據 scheme），那麼應該要求其使用 TLS。</p>
<p>另外，因為 Authorization Code 是經由 User-Agent 的轉址來傳輸的，所以可能從 User-Agent 的歷史記錄和 Referrer header 裡面找到。</p>
<h4 id="cong-authorization-code-lai-ren-zheng-reosurce-owner">從 Authorization Code 來認證 Reosurce Owner</h4>
<p>Authorization Code 做為一個純文字的 bearer credential （代表持有者的 credential）來運作，這個 credential 用來驗證：在 Authorization Server 上面授予權限的 Resource Owner = 返回 Client 要完成程序的 Resource Owner。所以，如果 Client 依賴予 Authorization Code 來認證 Resource Owner ，那麼 Client 端的 Redirection Endpoint 必須使用 TLS。</p>
<h4 id="authorization-code-bei-er-du-li-yong">Authorization Code 被二度利用</h4>
<p>Authorization Code 必須要是短時效、單次使用。如果 Authorization Server 檢測到多次的請求來把一個 Authorization Code 換成 Access Token ，那麼 Authorization Server 應該要試著撤銷所有之前使用該 Authrization Code 來取得的 Access Token 。</p>
<h4 id="ren-zheng-client-fang-zhi-wu-fa-authorization-code">認證 Client 防止誤發 Authorization Code</h4>
<p>如果對 Client 的認證可行，那麼 Authorization Server 必須認證該 Client ，並且確保 Authorization Code 核發給同一個 Client 。</p>
<h3 id="cuan-gai-authorization-code-de-redirection-uri-section-10-6">竄改 Authorization Code 的 Redirection URI (Section 10.6)</h3>
<p>使用 Authorization Code Grant Type 要求授權的時候，Client 可以用 "redirect_uri" 來指定 Redirection URI。如果壞人可以竄改 Redirection URI 的值，他就可以讓 Authorization Server 把 Resource Owner 轉向到壞人控制的 URI ，並且拿到 Authorization Code。</p>
<p>步驟如下：</p>
<ol>
<li>壞人在合法的 Client 建立一個帳號，並起始授權流程。</li>
</ol>
<ul>
<li>當壞人的 User-Agent 被傳送到 Authorization Server 來取得存取權限的時候，壞人取得由 Client 提供的 Authorization URI 並且把 Client 的 Redirection URI 取代成壞人控制的 URI。</li>
<li>壞人接著晃點 (trick) 受害者去跟隨修改過的連結來授權合法 Client 的存取權限。</li>
<li>在 Authorization Server，受害者會得到一個正常的、正確的 Request ，其 Request 代表合法的、受信任的 Client，並且授權其存取。</li>
<li>受害者接著會被轉向壞人控制的 Endpoint ，還附上 Authorization Code。</li>
<li>壞人接著把 Authorization Code 送到原先 Client 提供的真正的 Redirection URI 來完成授權流程。</li>
<li>Client 把 Authorization Code 換成 Access Token 並且連結到壞人的帳號，而這個 Access Token 可以用來透過 Client 存取受害者的 Protected Resource 。</li>
</ul>
<p>防範方式：</p>
<p><strong>確認 Redirection URI 一致</strong> ：Authorization Server 必須確保之前用來拿取 Authorization Code 的 Redirection URI ，跟之後透過 Authorization Code 拿取 Access Token 時的 Redirection URI 一模一樣。</p>
<p><strong>事先設定 Redirection URI 並驗證</strong> ：Authorization Server 必須要求 Public Clients 並且最好要要求 Confidential Clients 事先指定 Redirection URIs。如果有一個 Redirection URI 附在 request 裡面，那麼 Authorization Server 必須驗證其符合事先指定的 URIs。</p>
<hr />
<h2 id="oauth-2-0-xi-lie-wen-mu-lu">OAuth 2.0 系列文目錄</h2>
<ul>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-1-introduction/">(1) 世界觀</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">(2) Client 的註冊與認證</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-3-endpoints/">(3) Endpoints 的規格</a></li>
<li><strong><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-1-auth-code-grant-flow/">(4.1) Authorization Code Grant Flow 細節</a> ← You Are Here</strong></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-2-implicit-grant-flow/">(4.2) Implicit Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-3-resource-owner-credentials-grant-flow/">(4.3) Resource Owner Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-4-client-credentials-grant-flow/">(4.4) Client Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-5-issuing-tokens/">(5) 核發與換發 Access Token</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-6-bearer-token/">(6) Bearer Token 的使用方法</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-7-security-considerations/">(7) 安全性問題</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-implementation-differences-among-famous-sites/">各大網站 OAuth 2.0 實作差異</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>OAuth 2.0 筆記 (3) Endpoints 的規格</title>
            <link>https://blog.yorkxin.org/posts/oauth2-3-endpoints/</link>
            <pubDate>Mon, 30 Sep 2013 13:42:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/09/30/oauth2-3-endpoints.html</guid>
            
            <description><![CDATA[
                <p>在 OAuth 2.0 裡面，Endpoints （資料傳輸接點）共有三種：</p>
<ul>
<li>Authorization Server 的 <strong>Authorization Endpoint</strong></li>
<li>Client 的 <strong>Redirection Endpoint</strong></li>
<li>Authorization Server 的 <strong>Token Endpoint</strong></li>
</ul>
<p>其規格如下文，不能只看名稱來判別其用途，像是 Authorization Endpoint 其實是一點多用，在某些流程裡會發 Authorization Grant Code ，有些流程會直接發 Access Token ，有些流程會略過之。</p>
<p>使用的順序大致上是 Authorization Endpoint → Redirection Endpoint → Token Endpoint 。</p>
<p>可以發現到 Resource Server 上面並沒有定義任何 Endpoints ，這是因為取得 Access Token 的流程與 Resource Server 無關， Resource Server 只需要認 Access Token 並且向 Authorization Server 驗證 Token 合法就行了。</p>
<span id="continue-reading"></span>
<h2 id="authorization-endpoint-authorization-server">Authorization Endpoint (Authorization Server)</h2>
<p>Authorization Endpoint 主要是給 Client 從 Resource Owner 取得 Authorization Grant 用的，其過程會透過 User-Agent 轉向。</p>
<p>在內建的四種流程中，只有 Authorization Code Grant Flow 和 Implicit Grant Flow 才會使用到。</p>
<p>處理過程中，必須先認證 Resource Owner ，但認證方法在 spec 裡面不明確定義，或許是帳密、或許是 session cookie 。也就是說要登入這個使用者。</p>
<p>Client 得知 Authorization Endpoint 的方法不定義，通常是直接寫在服務的文件裡面。</p>
<h3 id="uri-yao-qiu">URI 要求</h3>
<ul>
<li>URI 裡面可以有 Query Component（如 <code>?xxx=yyy</code>），但是當要加入其他 parameter 的時候，必須保留既有的 parameters 。</li>
<li>URI 裡面不可以有 Fragment Component （<code>#zzz</code>）。</li>
</ul>
<h3 id="http-method">HTTP Method</h3>
<p>必須支援 GET。可以支援 POST（非必備）。</p>
<h3 id="chuan-di-de-can-shu">傳遞的參數</h3>
<table><thead><tr><th>參數名</th><th>必/選</th><th>意義</th></tr></thead><tbody>
<tr><td>response_type</td><td>必</td><td>會 switch 到不同的 flow ，見下文 Response Type 段</td></tr>
<tr><td>state</td><td>建議有</td><td>通用參數，用來維持最初狀態，見下文</td></tr>
<tr><td>scope</td><td>選</td><td>通用參數，用來指定存取範圍，見下文</td></tr>
</tbody></table>
<h4 id="response-type">Response Type</h4>
<p>Response Type 透過 <code>response_type</code> 參數來指定，其值定義如下：</p>
<table><thead><tr><th>值</th><th>意義</th></tr></thead><tbody>
<tr><td><code>code</code></td><td>求 Authorization Code (Authorization Code Flow)</td></tr>
<tr><td><code>token</code></td><td>求 Access Token (Implicit Flow)</td></tr>
<tr><td>（其他）</td><td>為 extension ，若有多個，可以以空格分開</td></tr>
</tbody></table>
<p>若 <code>response_type</code> 欠缺或認不得，必須回錯誤，見下文。</p>
<h4 id="can-shu-jie-xi-yuan-ze">參數解析原則</h4>
<ul>
<li>留空的參數要當做沒有提供 (omitted) 。例如 <code>response_type=code&amp;state=</code> 要當做沒給 <code>state</code> 參數。</li>
<li>認不得的參數必須忽略之。</li>
<li>每個參數只能出現一次，不可重覆出現。若有重覆，則要回傳錯誤。</li>
</ul>
<h3 id="tls-https-yao-qiu">TLS (https) 要求</h3>
<p>必須經過 TLS ，因為 response 裡面有 credentials 會被看到。</p>
<h3 id="yu-dao-cuo-wu-de-shi-hou-de-chu-li-fang-shi">遇到錯誤的時候的處理方式</h3>
<p>所謂錯誤，像是參數錯誤、Client 不被授權使用這種 Authorization 、Server 不支援這種 Authorization 等等。雖然 Spec 裡面的四種流程，有用到 Authorization Endpoint 的（其實就是 Authorization Code 和 Implicit），其處理方式都一樣，但是只使用於內建流程， Extension 可能會使用更多的，所以不寫在這篇裡面。</p>
<p>你只要知道 spec 裡面的內建流程，對於 Authorization Endpoint 的錯誤處理方式是一模一樣的就好了。</p>
<p><em>Section 3.1, 3.1.1</em></p>
<h2 id="redirection-endpoint-client">Redirection Endpoint (Client)</h2>
<p>Authorization Server 在完成與 Resource Owner 的互動之後（認證 Resource Owner 、提示 Client 要請求授權之類的），會把 Resource Owner 的 User-Agent 轉回 Cilent ，這個轉回去的目標就是 Redirection Endopoint 。</p>
<p>在內建的四種流程中，只有 Authorization Code Flow 和 Implicit Flow 才會使用到。</p>
<p>註：本文省略關於多重 Redirection URI 和動態設置 (Dynamic Configuration) 的 spec 的解說，因為一來我看不懂，二來我沒用過要指定多個 Redirection URI 的 OAuth 服務，所以不清楚它的用途，我想對於入門來說應該是不需要說明（我也沒能力說明）。有興趣的同學可以看 spec 的 Section 3.1.2.3 。</p>
<h3 id="client-she-ding-redirection-endpoint">Client 設定 Redirection Endpoint</h3>
<p>Redirection Endpoint 可以在 Client 註冊的時候設定，或是在發出 Authorization Request 的時候指定。</p>
<p>以下這些類型的 Clients 必須設定 Redirection Endpoint：</p>
<ul>
<li>Public Client</li>
<li>Confidential Client 且利用 Implicit Grant Type</li>
</ul>
<p>Authorization Server 應該要要求所有 Clients 在使用 Authorization Endpoint 之前，都設定 Redirection Endpoint。會要求設定 Redirection Endpoint，是為了防止壞人利用 Authorization Endopint 做為 open redirector 。詳見本文最末段，關於安全性的問題。</p>
<h3 id="authorize-shi-redirection-uri-bu-zheng-que-de-chu-li-fang-shi">Authorize 時，Redirection URI 不正確的處理方式</h3>
<p>如果 Authorization Server 驗證 Redirection URI 失敗（沒註冊、不相符等情況），則 Authorization Server 應該提示錯誤，並且 <strong>不可以自動轉回</strong> 錯誤的 Redirection URI 。</p>
<h3 id="uri-de-yao-qiu">URI 的要求</h3>
<ul>
<li>必須是 Absolute URI （就是下圖的 scheme + hierarchical + query (選用)）。定義在 <a href="http://tools.ietf.org/html/rfc3986#section-4.3">RFC3986 的 Section 4.3</a>。</li>
<li>URI 裡面可以有 Query Component（如 <code>?xxx=yyy</code>），但是當要加入其他 parameter 的時候，必須保留既有的 parameters 。</li>
<li>URI 裡面不可以有 Fragment Component （<code>#zzz</code>）。</li>
</ul>
<p>若無法指定完整的 URI （像是不能指定 Query Component），則應該要求指定 URI 的 scheme 、 authority 、 path 這三個部份（見下圖）。</p>
<p>就我的理解可以給出以下範例：</p>
<table><thead><tr><th>OK?</th><th>Example</th><th>Reason</th></tr></thead><tbody>
<tr><td>◯</td><td><code>https://www.example.com/oauth/callback</code></td><td></td></tr>
<tr><td>◯</td><td><code>https://www.example.com/oauth/callback?origin=facebook</code></td><td></td></tr>
<tr><td>✕</td><td><code>https://www.example.com</code></td><td>沒有 path part</td></tr>
</tbody></table>
<p><a href="http://en.wikipedia.org/wiki/URI_scheme">根據維基百科的解釋</a>，即是 Query Component 之前的所有部份，亦即僅允許 Client 自訂 Query Component：</p>
<pre><code>  foo:&#x2F;&#x2F;username:password@example.com:8042&#x2F;over&#x2F;there&#x2F;index.dtb?type=animal&amp;name=narwhal#nose
  \_&#x2F;   \_______________&#x2F; \_________&#x2F; \__&#x2F;            \___&#x2F; \_&#x2F; \______________________&#x2F; \__&#x2F;
   |           |               |       |                |    |            |                |
   |       userinfo         hostname  port              |    |          query          fragment
   |    \________________________________&#x2F;\_____________|____|&#x2F; \__&#x2F;        \__&#x2F;
   |                    |                          |    |    |    |          |
scheme              authority                    path   |    |    interpretable as keys
 name   \_______________________________________________|____|&#x2F;       \____&#x2F;     \_____&#x2F;
                             |                          |    |          |           |
                     hierarchical part                  |    |    interpretable as values
                                                        |    |
                                interpretable as filename    interpretable as extension
</code></pre>
<h3 id="tls-https-de-yao-qiu">TLS (HTTPS) 的要求</h3>
<p>Redirection Endpoint 在以下任一種情況，應該要有 TLS ：</p>
<ul>
<li>發出 Authorization Request 時的 Response Type 為 <code>code</code> 或 <code>token</code> （= 內建流程的每一種）。</li>
<li>重新轉向的時候會經由公開網路傳遞敏感資料。</li>
</ul>
<p>然而這個並不強求，因為現階段有些 Client 實作 TLS 有困難。因此，若重新轉向的目標並不是 TLS (https) ，則 Authorization Server 應該要向 Resource Owner 提出警告。</p>
<h3 id="guan-yu-redirection-endpoint-de-nei-rong-de-jian-yi">關於 Redirection Endpoint 的內容的建議</h3>
<p>所謂的內容，就是當 User-Agent 打開 Redirection Endpoint URI 的時候看到的內容，通常是 HTML ，所以如果 HTML 直接在 Redirection Request 輸出的話，任何 script 都可以拿到 Redirection URI 及包含在其中的 credentials 。</p>
<p>因此有這些建議：</p>
<ul>
<li>Client 應該直接從 URI 裡面解出 credentials ，並且馬上 redirect 到別的地方以防外洩。</li>
<li>Client 不應該在 Redirection Response 裡面載入第三方 script （Analytics 、社交網站、廣告等）。</li>
<li>若第三方 script 無法避免，則 Client  必須確保自己的 script 先跑，先把 credentials 解出來，並且移除 credentials 。</li>
</ul>
<p>從 Rails 的實作方式來說，就是直接在 Controller 裡面解出 credentials ，存進 Model ，然後 redirect 到別的 path 就行了。</p>
<p><em>Section 3.1.2 - 3.1.2.5</em></p>
<h2 id="token-endpoint-authorization-server">Token Endpoint (Authorization Server)</h2>
<p>Token Endpoint 是 Client 用來拿取 Access Token 的。拿取的時候，要出示 Authorization Grant （第一次拿 Access Token）或 Refresh Token （舊的 Access Token 不能用，要重新拿新的）。</p>
<p>在內建的四種流程裡，只有 Implicit Grant Type 不使用之，因為這個流程的 Access Token 是直接在 Authorization Endpoint 那邊就直接給了。</p>
<p>在 Token Endpoint 處理的流程中，有一步是認證 Client ，用來確認 Client 的身份。詳見「關於認證 Client 的說明」一段。</p>
<p>Client 得知 Token Endpoint 的方法不定義，通常是直接寫在服務的文件裡面。（同 Authorization Endpoint）</p>
<h3 id="http-method-1">HTTP Method</h3>
<p>必須使用 POST。Client 在發送 Token Request 的時候，也必須使用 POST 。</p>
<h3 id="uri-yao-qiu-1">URI 要求</h3>
<p>（同 Authorization Endpoint）</p>
<ul>
<li>URI 裡面可以有 Query Component（如 <code>?xxx=yyy</code>），但是當要加入其他 parameter 的時候，必須保留既有的 parameters 。</li>
<li>URI 裡面不可以有 Fragment Component （<code>#zzz</code>）。</li>
</ul>
<h3 id="chuan-di-de-can-shu-1">傳遞的參數</h3>
<p>雖然在 Token Endpoint 的 Spec 裡面沒有寫到必備的參數，但我整理了四種內建流程以及換發 Token 的流程之後，總結出「一定要有 Grant Type」這個事實，所以在這裡寫下來。還有其他參數，但是會根據流程的不同而不同。</p>
<table><thead><tr><th>參數名</th><th>必/選</th><th>意義</th></tr></thead><tbody>
<tr><td>grant_type</td><td>必</td><td>會 switch 到不同的 flow ，見下文 Grant Type 段</td></tr>
<tr><td>state</td><td>建議有</td><td>通用參數，用來維持最初狀態，見下文</td></tr>
<tr><td>scope</td><td>選</td><td>通用參數，用來指定存取範圍，見下文</td></tr>
</tbody></table>
<h4 id="grant-type">Grant Type</h4>
<p>Grant Type 透過 <code>grant_type</code> 參數來指定，其值定義如下：</p>
<table><thead><tr><th>值</th><th>意義</th></tr></thead><tbody>
<tr><td><code>authorization_code</code></td><td>用 Authorization Code 求 Access Token <br>(Authorization Code Grant Flow)。</td></tr>
<tr><td><code>password</code></td><td>用 Resorce Owner Password Credentials 求 Access Token <br>(Resource Owner Password Credentials Grant Flow)。</td></tr>
<tr><td><code>client_credentials</code></td><td>用 Client Credentials 求 Access Token <br>(Client Credentials Grant Flow)。</td></tr>
<tr><td><code>refresh_token</code></td><td>用 Refresh Token 換發 Access Token。</td></tr>
</tbody></table>
<p>至於更多 Grant Types 可以參考 Section 8.3。</p>
<p>Grant Type 沒給，或不認得的時候，回應錯誤的方式見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-5-issuing-tokens/">系列文第 5 篇</a>。</p>
<h4 id="can-shu-jie-xi-yuan-ze-1">參數解析原則</h4>
<p>（同 Authorization Endpoint）</p>
<h3 id="tls-https-yao-qiu-1">TLS (https) 要求</h3>
<p>必須經過 TLS ，因為 request 和 response 裡面都有 credentials 會被看到。</p>
<h3 id="response-de-fang-shi">Response 的方式</h3>
<p>由於這個 Endpoint 是專門用來核發 Access Token 的，其 Response 的方式以及錯誤回應方式，我寫在<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-5-issuing-tokens/">系列文第 5 篇</a>。</p>
<h3 id="guan-yu-ren-zheng-client-de-shuo-ming">關於認證 Client 的說明</h3>
<p>在 Token Endpoint 的流程裡面，有一步是要認證 Client ，需要認證的 Clients 是 Confidential Clients 或是有發給 Client Credentials 的 Clients 。</p>
<p>實際認證的機制，規定在 Section 2.3 裡面（<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">系列文第 2 篇</a>）。簡單來說，要用 HTTP Basic Auth 來認證，設 Client Credential 為：ID  <code>s6BhdRkqt3</code> 、 Secret <code>7Fjfp0ZBr1KtDRbnfVdmIw</code> ，那麼在 Client 往 Token Endpoint 發 Request 的時候， Request Header 裡面要有這個：</p>
<pre><code>Authorization: Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3
</code></pre>
<p>認證 Client 是為了：</p>
<ul>
<li>強化「Refresh Token ↔ 核發的對象 Client」與「Authorization Code ↔ 授予的對象 Client」之間的關係。當 Authorization Code 要透過不安全通道傳到 Redirection Endpoint 的時候，或是 Redirection URI 沒有全部註冊的時候（動態組態，本文略），Client 認證就顯得很重要。</li>
<li>用來復原被駭掉的 Client ，做法是禁用或更改它的 Credentials ，這樣子可以防止壞人濫用被偷走的 Refresh Token。替換單獨一組 client credential 比撤銷整組 Refresh Tokens 還要來得快。</li>
<li>實施認證管理的最佳實踐 (Best Practice)，即是要求定期更換 credentials。更換所有的 Refresh Token 是很難做到的，而定期更換單獨一組 credential 卻很容易。</li>
</ul>
<p>在某些流程裡，Client 會用 <code>client_id</code> 參數來識別自己。像是在 Authorization Code Grant 流程裡面，發 Request 到 Access Token 的時候，沒有認證的 Client （如 Public Client）就必須用 <code>client_id</code> 來避免收到給別的 Client 的 Access Token，Authorization Server 也可以藉此防止 Client 自己置換 Authorization Code。需注意這個方法並不會為 Proteected Resource 帶來額外的保護。</p>
<p><em>Section 3.2 - 3.2.1</em></p>
<h2 id="endpoints-tong-yong-de-can-shu">Endpoints 通用的參數</h2>
<h3 id="scope-zhi-ding-cun-qu-fan-wei">scope: 指定存取範圍</h3>
<p>Authorization Endpoint 和 Token Endpoint 允許 Client 指定申請 Access Token 的時候所要的 scopes （存取範圍）。</p>
<p>參數名稱是 <code>scope</code>。格式是一串 scopes 用空格 (U+0020) 分開，區分大小寫。每一個 scope 的值是由 Authorization Server 定義的，格式是 ASCII 可見字元，排除雙引號 <code>"</code> (U+0022) 和反斜線 <code>\</code> (U+005C)。順序不重要。每出現一個 scope 值，就代表要多加一個新的 scope。</p>
<p>根據 Authorization Server 制定的政策，以及 Resource Owner 的指示，可以完全或部份忽略某些 scopes。在這種情況下，<code>scope</code> 值也會在 Endpoint Response 裡面回傳，也就是當真正授予的 scopes 與原本要求的不同的時候告訴 Client。所以可能比原本要求的 scopes 還要少，也可能還要多。</p>
<p>假如 Client 在申請 Authorization 的時候，沒有給 scope 值，則 Authorization Server 必須做以下之中的一件事：</p>
<ul>
<li>用預設值處理（若有）。</li>
<li>回報錯誤，提示 scope 不合法。</li>
</ul>
<p>處理方式、預設值、 scope 的要求，應該要寫在 Authorization Server 文件裡。</p>
<h4 id="shi-wu-shang-de-scope">實務上的 scope</h4>
<p>實務上大部份網站的 OAuth 2.0 實作方式， scope 都是用逗號 (<code>,</code>, U+002C) 分隔的，有的甚至不存在 scope 這種東西，一授權就是 full access。之後會寫一篇文章來整理。</p>
<p><em>Section 3.3</em></p>
<h3 id="state-wei-chi-endpoint-zhi-jian-de-cao-zuo-zhuang-tai">state: 維持 Endpoint 之間的操作狀態</h3>
<p>Endpoint 之間可以用 <code>state</code> 參數來維持操作狀態，例如這種情況：</p>
<ul>
<li>我打開 A 網站的 X 頁面</li>
<li>我按下「登入」按鈕來登入 A 網站，用 Facebook 帳號來登入</li>
<li>Facebook 完成登入流程之後，回到 A 網站</li>
<li>我希望我看到的是 A 網站的 X 頁面←【目標】</li>
</ul>
<p>要達到這個目標，就可以用 <code>state</code> 參數來維持「使用者之前在看 X 頁面」這個狀態。</p>
<p><code>state</code> 參數在傳遞的過程中會原封不動，所以 Client 最後一定會得到原本的 <code>state</code> 。</p>
<p>又，因為 <code>state</code> 可能會放在 URI 裡面，所以如果裡面有敏感資料，則可能會留下痕跡（像是 Log 、 Proxy 的 Log 等等），所以最好是 Client 有內建加解密機制，這樣子在傳遞的過程就不會被抄走。</p>
<p><code>state</code> 也可以應用在防止 CSRF ，見 Section 10.12 （<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-7-security-considerations/">系列文第 7 篇</a> ）。</p>
<h2 id="an-quan-xing-wen-ti">安全性問題</h2>
<h3 id="open-redirectors-section-10-15">Open Redirectors (Section 10.15)</h3>
<p>Open Redirector 指的是 Endpoint 使用某參數來自動把 User-Agent 轉向到該參數所指定的位置，而沒有經過事先驗證。Auothorization Server 、 Authorization Endpoint 、 Client Redirection Endpoint 可能會因為設定的不好所以變成 Open Redirector。</p>
<p>Open Redirectors 會被利用在釣魚攻擊，或是讓壞人得以偽造 URI 的 authority part 讓它看起來很像可信任的網站，引導使用者前往惡意網站。此外，如果 Authorization Server 允許 Client 只事先指定 Redirection URI 的一部分，那麼壞人可以利用 Client 操作的 Open Redirector 來建立一個 Redirection URI ，這個 URI 跳過 Authorization Server 驗證，但是會把 Authorization Code 或 Access Token 傳送到壞人所控制的 endpoint。</p>
<p>在<a href="https://images-na.ssl-images-amazon.com/images/G/01/lwa/dev/docs/website-developer-guide._TTH_.pdf">Amazon 的文件</a>裡面，提出了 Open Redirector 常有的 pattern ：</p>
<ul>
<li>example.com/go.php?url=</li>
<li>example.com/search?q=user+search+keywords&amp;url=</li>
<li>example.com/coupon.jsp?code=ABCDEF&amp;url=</li>
<li>example.com/login?url=</li>
</ul>
<p>這種「疑似可以手動指定之後要再轉去別的地方」的參數容易變成 Open Redirector。</p>
<hr />
<h2 id="oauth-2-0-xi-lie-wen-mu-lu">OAuth 2.0 系列文目錄</h2>
<ul>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-1-introduction/">(1) 世界觀</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">(2) Client 的註冊與認證</a></li>
<li><strong><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-3-endpoints/">(3) Endpoints 的規格</a> ← You Are Here</strong></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-1-auth-code-grant-flow/">(4.1) Authorization Code Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-2-implicit-grant-flow/">(4.2) Implicit Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-3-resource-owner-credentials-grant-flow/">(4.3) Resource Owner Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-4-client-credentials-grant-flow/">(4.4) Client Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-5-issuing-tokens/">(5) 核發與換發 Access Token</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-6-bearer-token/">(6) Bearer Token 的使用方法</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-7-security-considerations/">(7) 安全性問題</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-implementation-differences-among-famous-sites/">各大網站 OAuth 2.0 實作差異</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>OAuth 2.0 筆記 (2) Client 的註冊與認證</title>
            <link>https://blog.yorkxin.org/posts/oauth2-2-cilent-registration/</link>
            <pubDate>Mon, 30 Sep 2013 13:41:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/09/30/oauth2-2-cilent-registration.html</guid>
            
            <description><![CDATA[
                <p>在 OAuth 2.0 的 spec 裡面，關於註冊 Client (Registration) 這件事，只定義了抽象的概念、類型 (profiles) 與要求，以及基於保密能力把 Clients 分成兩類： <strong>confidential</strong> 和 <strong>public</strong>。</p>
<p>而認證 (Authentication) 的流程則是有規定需要傳送的資料。所謂的認證，就是 Client 要向 Authorization Server 證明自己的身份，若把 Client 比喻為人類使用者的話，就像是打帳號密碼之類的動作。在 spec 內建的流程中，需要認證 Client 的地方只有 Token Endpoint，就是「發給你 Token 的時候」認證。其中 Implicit Flow 並沒有認證 Client （也沒有經過 Token Endpoint）。</p>
<span id="continue-reading"></span>
<h2 id="client-de-zhu-ce">Client 的註冊</h2>
<p>Spec 不規定 Client 如何向 Authorization Server 註冊自己，通常是用 HTML 界面。註冊時， Authorization Server 與 Client 之間不需要有直接互動，如果 Authorization Server 支援的話，註冊的過程可以依賴其他的手段來建立互相的信任、取得 Client 的註冊資料（Redirection URI 、Client Type 等）。例如，可以透過內部的通道來搜尋 Client 。</p>
<p>註冊的時候， Client 的開發人員應該要做這些事：</p>
<ul>
<li>指定 Client Type （見下文）</li>
<li>指定 Redirection URL （如 Section 3.1.2 所述）</li>
<li>提供其他 Authorization Server 要求的資料（名稱、網站、Logo 等）</li>
</ul>
<p><em>Section 2</em></p>
<h3 id="client-types">Client Types</h3>
<p>在 spec 裡面，根據有沒有能力保密 client 的 credentials （帳號密碼），定義了兩種 Client Types：</p>
<p><strong>confidential</strong>：Client 可以自我保密 client 的 credentials（例如跑在 Server 上面，且可以限制 credentials 的存取），或是可以用別的手段來確保認證過程的安全性。</p>
<p><strong>public</strong>：Client 無法保密 credentials （Native App 或是跑在 Browser 裡面的 App），或是無法用任何手段來保護 client 的認證。</p>
<p>Authorization Server 不應該自行猜測 Client 屬於何種。（不過現實卻不是這樣，見下文。）</p>
<p>單一的 Client 可能會分離成不同的組件 (components) ，如一個跑在 Server 、一個跑在 Client 。若 Authorization Server 沒有支援這種 Client ，或沒有指南文件，則開發人員時必須為各個組件註冊不同的 Clients。</p>
<p><em>Section 2.1</em></p>
<h3 id="client-profiles">Client Profiles</h3>
<p>OAuth 2.0 的 spec 是為以下這些類型的 Clients 來設計的：</p>
<p><strong>Web Application</strong>：</p>
<ul>
<li>屬於 confidential</li>
<li>跑在 Web Server 上面。</li>
<li>Client Credentials 及 Access Token 儲存在 Server 上面，於 Resource Owner 不可見。</li>
</ul>
<p><strong>User-Agent-based Application</strong></p>
<ul>
<li>屬於 public</li>
<li>Client 的程式是從 Web Server 下載到 Resource Owner 的 User-Agent 來執行的。</li>
<li>通訊協定過程的數據以及 credentials 可以很容易被 Resource Owner 取得（而且通常看得到）。</li>
<li>也因為這種 app 直接跑在 User-Agent 裡面，所以可以在取得 Authorizations 的時候無縫接軌。</li>
</ul>
<p><strong>Native Application</strong></p>
<ul>
<li>屬於 public</li>
<li>安裝在 Resource Owner 的設備上，也在其上執行。</li>
<li>通訊協定過程的數據與 credentials 可以被 Resource Owner 取得。</li>
<li>任何包在 app 裡面的 Client Credentials 都要假設可以被解出來。</li>
<li>相對而言，動態取得的 credentials ，像是 Access Token 、 Refresh Token ，可以得到某種程度的保護。至少，如果把這些 credentials 存放在 Client 會使用的伺服器上，也可以得到保護。</li>
<li>在某些平台上，這些 credentials 可能會被保護起來，從而不讓其他在同一台設備上的其他 apps 取得。（OS X 的 Keychain Access 就是這種機制）</li>
<li>關於 Native Application 有更多實作上的考量，請見後文。</li>
</ul>
<p>舉例：</p>
<table><thead><tr><th>應用程式</th><th>Profile</th><th>Type</th></tr></thead><tbody>
<tr><td>自動抓 Facebook 照片的某個伺服器程式</td><td>Web Application</td><td>Confidential</td></tr>
<tr><td>可以連結 Facebook 帳號的 Firefox Add-On</td><td>UA-based Application</td><td>Public</td></tr>
<tr><td>iPhone 版的 Facebook 即時通訊程式</td><td>Native App</td><td>Public</td></tr>
</tbody></table>
<p><em>出自 Section 2.1，範例除外</em></p>
<h3 id="xian-shi-zhong-de-client-registration">現實中的 Client Registration</h3>
<p>雖然規定註冊時要填寫 Client Type ，實務上好像沒什麼網站會要求填寫 Client Type 的，甚至 Client Profile，在整份 API 裡面，即使會區分 client ，也是把 client 依其 Profiles 區分。</p>
<h3 id="client-identifier-shi-bie-hao">Client Identifier （識別號）</h3>
<p>Client 的惟一識別號，註冊時取得，對 Authorization Server 也是惟一的。</p>
<p>會被 Resource Owner 看到，所以絕對不可以在 Client Authentication 的時候單獨使用。</p>
<p>其長度 spec 並不規定。Client 不可自己猜測。Authorization Server 應該要在文件裡提及。</p>
<p><em>Section 2.2</em></p>
<h2 id="ren-zheng-client-de-fang-shi-authentication">認證 Client 的方式 (Authentication)</h2>
<p><strong>confidential</strong>：要有認證流程，其流程要符合 Authorization Server 的安全規範。通常是用事先核發的 credentials （如 Password 、非對稱式金鑰）。</p>
<p><strong>public</strong>：可以有認證方式（不必備），但絕對不能以 Public Client 的認證方式來識別 (identify) 個別的 client 。</p>
<p>Client 每一次 request 只能用一種方式來認證。</p>
<p><em>Section 2.3</em></p>
<h3 id="yong-client-password-lai-ren-zheng">用 Client Password 來認證</h3>
<h4 id="fang-fa-1-http-basic-auth">方法 (1): HTTP Basic Auth</h4>
<p>持有 Password 的 Client 可以用 HTTP Basic Auth 來認證（見 RFC 2617）。帳密要先用 urlencode 編過。</p>
<p>例如 ID 是 <code>s6BhdRkqt3</code> 、 Secret 是 <code>7Fjfp0ZBr1KtDRbnfVdmIw</code> ，則步驟如下：</p>
<p>Step 1: 根據 Basic Auth 的規則，把 ID 和 Secret 連起來，中間用冒號 <code>:</code> 分開，變成這樣：</p>
<pre><code>s6BhdRkqt3:7Fjfp0ZBr1KtDRbnfVdmIw
</code></pre>
<p>Step 2: 用 base64 編過，變成這樣：</p>
<pre><code>czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3
</code></pre>
<p>Step 3: 加上 <code>Basic</code> 前綴：</p>
<pre><code>Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3
</code></pre>
<p>Step 4: 最後得到的 HTTP Auth 的 header 就是：</p>
<pre><code>Authorization: Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3
</code></pre>
<h4 id="fang-fa-2-post">方法 (2): POST</h4>
<p>此外還有另一種方法（不建議使用）是使用 POST 發送以下資料：</p>
<table><thead><tr><th>參數名</th><th>必/選</th><th>註</th></tr></thead><tbody>
<tr><td>client_id</td><td>必</td><td></td></tr>
<tr><td>client_secret</td><td>必</td><td>若本來就是空白的密碼，則可留空。</td></tr>
</tbody></table>
<p>注意事項：</p>
<ul>
<li><strong>這種方法不建議使用</strong></li>
<li>應該限制在無法使用 Basic Auth 或其他 HTTP Authentication 方式的 Client 來使用。</li>
<li>不可以把參數放在 URI 裡面。</li>
<li>要經過 TLS (https) 。</li>
<li>因為牽涉到密碼，所以要防暴力破解。</li>
</ul>
<p>範例：（換發 Access Token 時，Client 要認證自己）</p>
<pre><code>POST &#x2F;token HTTP&#x2F;1.1
Host: server.example.com
Content-Type: application&#x2F;x-www-form-urlencoded

grant_type=refresh_token&amp;refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
&amp;client_id=s6BhdRkqt3&amp;client_secret=7Fjfp0ZBr1KtDRbnfVdmIw
</code></pre>
<p><em>Section 2.3.1</em></p>
<h3 id="qi-ta-ren-zheng-fang-shi">其他認證方式</h3>
<p>沒規定不能有別的，只要 Authorization Server 沒有安全上的疑慮就好了。</p>
<p>不過，如果要做別的認證方式，必須要建一張表記錄認證方式跟 Client ID 。</p>
<h3 id="shi-ji-shang-de-li-yong-fang-shi">實際上的利用方式</h3>
<p>理想與現實總是有段差距。實務上，許多大網站只支援 POST ，Basic Auth 則不一定支援。而 Facebook 則是只支援 GET （不符合「不可以放在 URI 裡面」的要求）。</p>
<h2 id="wei-zhu-ce-de-clients">未註冊的 Clients</h2>
<p>沒規定不能有這種 Client 存在，spec 裡面也不討論。</p>
<h2 id="guan-yu-native-application">關於 Native Application</h2>
<p>Native Application 指的是在 Resource Owner 的設備上面安裝、執行的 Client （即桌面應用程式、手機 App），需要特別考慮安全性、平台相容性、整體的 User Experience 。</p>
<p>Authorzation Endpoint 會要求 Client 和 Resource Owner 的 User-Agent 之間的互動。Native Application 可以調用外部的 User-Agent 或是內嵌一個在應用程式裡面。用法如下。</p>
<h3 id="wai-bu-user-agent">外部 User-Agent：</h3>
<ul>
<li>用 Redirection URI 捉到來自 Authorization Server 的 response ，這個 URI 的 Scheme 要事先向 OS 註冊，才能讓 Client 成為 Scheme 的處理程式（如 <code>facebook:</code>）。</li>
<li>手動複製貼上 credentials 。</li>
<li>跑在本機的 Web Server。</li>
<li>安裝一個 User-Agent 的擴充套件。</li>
<li>提供一個 Redirection URI 來識別出一個放在 Server 上的、由 Client 控制的 resurce ，讓 resource 可以被 Native Application 取得（例如 Facebook 有一個固定的 Redirection URI）。</li>
</ul>
<h3 id="nei-qian-user-agent">內嵌 User-Agent:</h3>
<ul>
<li>直接監視其狀態變化來得到 response （例如看到網址變成事先指定的，就表示得到了 redirection）</li>
<li>直接存取其 cookie。</li>
</ul>
<h3 id="wai-bu-huo-nei-qian-user-agent-de-xuan-ze">外部或內嵌 User-Agent 的選擇</h3>
<p>在選擇要用哪一種 User-Agent 的時候，請考慮以下這些事：</p>
<ul>
<li>外部的 User-Agent 會增加達成率 (completion rate) ，因為 Resource Owner 可能已經登入到 Authorization Server 了，如此就可以免去重新登入的麻煩，從而讓使用者無縫接軌（不需要重新登入）。Resource Owner 也可能會依賴 User-Agent 特有的功能來協助登入（如自動填寫密碼、二步驗證）。</li>
<li>內嵌的 User-Agent 也許會增進使用的方便性，因為這樣就不需要切換到另一個視窗。</li>
<li>內嵌的 User-Agent 會導致安全上的挑戰，因為 Resource Owner 要在一個來歷不明的視窗裡面填入帳號密碼，如果是一般的外部 User-Egent，可以有別的視覺指引來辯認（如 URI、SSL Certificate）。內嵌的 User-Agent 會教育使用者去相信來歷不明的認證請求，進而讓釣魚攻擊更容易執行。</li>
</ul>
<h3 id="grant-flow-de-xuan-ze">Grant Flow 的選擇</h3>
<p>Native Application 可以用的流程是 Implicit Grant 和 Authorization Code Grant 。在選擇要用哪一種的時候，請考慮以下這些事：</p>
<ul>
<li>使用 Authorization Code Grant 的 Native Application 最好不要兼使用 client credentials ，因為 Native App 無法保密這些資料。</li>
<li>使用 Implicit Grant 的時候，不會拿到 Refresh Token 不會，這樣子一旦過期，就需要重覆認證的流程。</li>
</ul>
<p><em>Section 9</em></p>
<h2 id="an-quan-xing-wen-ti">安全性問題</h2>
<p>這裡的安全性問題出自 Section 10 ，因為跟 Client Authentcation 有關，所以直接放進來。</p>
<h3 id="client-ren-zheng-de-an-quan-xing-wen-ti-section-10-1">Client 認證的安全性問題 (Section 10.1)</h3>
<p>Authorization Server 設立 Client credentials 是為了認證 Client。建議 Authorization Server 考慮使用比 Client password 更強的認證方式。Web Application Clients 必須確保 Client password 和其他 credentaisl 的保密。</p>
<p>Authorization Server 不可以為了認證 Client 而核發 Client Passwords 或是其他 credentials 給 Native Application 或是 User-Agent-Based Application Clients ，但是可以核發給特定設備上面的 Native Applciation 。</p>
<p>如果 Client 認證無法實施，Authorization Server 應該使用別的方式來驗證 Client 的身份。例如，要求 Client 預先設定 Redirection URI，或是讓 Resource Owner 來確認 Client 的身份。雖然，在詢問 Resource Owner 的授權的時候，即使有正確的 Redirection URI ，也不足以驗證 Client 的身份， 但是可以防止 credentails 傳遞到假的 Client 。</p>
<p>對於未經認證的 Clients （如 Public Clients），Authorization Server 必須考慮與其互動時會引發的安全性衝擊，且核發給這種 Clients 的其他 credentials 有外洩的可能（例如 Refresh Token），應致力降低其可能性。</p>
<h3 id="wei-zhuang-cheng-bie-de-client-section-10-2">偽裝成別的 Client (Section 10.2)</h3>
<p>Client 可能會被駭，像是 credentials 外洩。這種情況發生時，惡意的 Client 可以偽裝成被駭的 Client 並取得存取 Protected Resource 的權限。</p>
<p>Authorization Server 必須儘可能認證 Client 。如果 Authorization Server 因為 Client 的性質而不能認證之（例如 In-Browser App 就不能認證），那麼 Authorization Server 必須要求其預先設定 Redirection URI 來接收授權的 response，並且應該利用其他手段來保護 Resource Owner 使用這種潛在的惡意 Client 。</p>
<p>Authorizatino Server 應該要強化明確的 Resource Owner 授權流程，並且提供 Resource Owner 關於 Client 要求的授權範圍 (scope) 以及時效。Resource Owner 在當下的 Client 的環境裡面，要不要檢閱這些資訊、要不要授權或拒絕請求，則是他的自由。</p>
<p>當 Authorization Server 收到重複的授權請求的時候，要是 Resource Owner 本人沒有與 Authorization Server 互動、Client 沒有認證自己、又沒有其他方法可以確定重複的請求是來自真正的 Client 而不是偽裝的，則 Authorization Server 不應該處理重複的授權請求。</p>
<h3 id="open-redirector">Open Redirector</h3>
<p>沒要求 Client 事先指定 Redirection URI ，可能會使得 Authorization Endpoint 變成 Open Redirector 。詳見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-3-endpoints/">系列文第 3 篇</a>關於 Open Redirector 的安全性問題。</p>
<hr />
<h2 id="oauth-2-0-xi-lie-wen-mu-lu">OAuth 2.0 系列文目錄</h2>
<ul>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-1-introduction/">(1) 世界觀</a></li>
<li><strong><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">(2) Client 的註冊與認證</a> ← You Are Here</strong></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-3-endpoints/">(3) Endpoints 的規格</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-1-auth-code-grant-flow/">(4.1) Authorization Code Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-2-implicit-grant-flow/">(4.2) Implicit Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-3-resource-owner-credentials-grant-flow/">(4.3) Resource Owner Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-4-client-credentials-grant-flow/">(4.4) Client Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-5-issuing-tokens/">(5) 核發與換發 Access Token</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-6-bearer-token/">(6) Bearer Token 的使用方法</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-7-security-considerations/">(7) 安全性問題</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-implementation-differences-among-famous-sites/">各大網站 OAuth 2.0 實作差異</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>OAuth 2.0 筆記 (1) 世界觀</title>
            <link>https://blog.yorkxin.org/posts/oauth2-1-introduction/</link>
            <pubDate>Mon, 30 Sep 2013 13:40:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/09/30/oauth2-1-introduction.html</guid>
            
            <description><![CDATA[
                <p>最近需要實作 OAuth 2 認證，不是接別人的 OAuth 2 ，而是自己製作出可以讓別人接我們的 OAuth 2 的服務（俗稱 Provider）。但看到既有的 OAuth 2 server library 如 <a href="https://github.com/nov/rack-oauth2">rack-oauth2</a> 卻都看不懂，所以花了很久的時間來研讀 <a href="http://tools.ietf.org/html/rfc6749">RFC 6749</a> 這份 OAuth 2.0 的 spec ，讀完之後總算懂 library 在幹嘛了。老闆建議我寫懶人包，所以就寫了這篇，一來筆記，二來讓別人可以透過這份懶人包來快速入門 OAuth 2。（不過說懶人包其實也不懶人，完全就是把 spec 翻譯出來啊……。）</p>
<p>以下文字盡量註明 RFC 6749 原文的出處。有些原文我可能會省略，例如與 OAuth 1.0 的差異（spec 裡面有些段落有提及）、擴充 OAuth 2.0 的功能 (Extension)，這是為了讓懶人包 focus 在 OAuth 2.0 的基本使用方式。專有名詞基本上不翻譯，只適度加註中文，這是為了可以和 library 裡面常用的變數名稱保持一致。</p>
<p>另外，我有把 spec 原文的 txt <a href="https://gist.github.com/yorkxin/6590756">轉成 Markdown</a> 來方便閱讀。</p>
<h2 id="oauth-2-0-xi-lie-wen-mu-lu">OAuth 2.0 系列文目錄</h2>
<ul>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-1-introduction/">(1) 世界觀</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">(2) Client 的註冊與認證</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-3-endpoints/">(3) Endpoints 的規格</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-1-auth-code-grant-flow/">(4.1) Authorization Code Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-2-implicit-grant-flow/">(4.2) Implicit Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-3-resource-owner-credentials-grant-flow/">(4.3) Resource Owner Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-4-client-credentials-grant-flow/">(4.4) Client Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-5-issuing-tokens/">(5) 核發與換發 Access Token</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-6-bearer-token/">(6) Bearer Token 的使用方法</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-7-security-considerations/">(7) 安全性問題</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-implementation-differences-among-famous-sites/">各大網站 OAuth 2.0 實作差異</a></li>
</ul>
<span id="continue-reading"></span>
<h2 id="jian-jie-oauth-2-0">簡介 OAuth 2.0</h2>
<p>在傳統的 Client-Server 架構裡， Client 要拿取受保護的資源 (Protected Resoruce) 的時候，要向 Server 出示使用者 (Resource Owner) 的帳號密碼才行。為了讓第三方應用程式也可以拿到這些 Resources ，則 Resource Owner 要把帳號密碼給這個第三方程式，這樣子就會有以下的問題及限制：</p>
<ul>
<li>第三方程式必須儲存 Resource Owner 的帳號密碼，通常是明文儲存。</li>
<li>Server 必須支援密碼認證，即使密碼有天生的資訊安全上的弱點。</li>
<li>第三方程式會得到幾乎完整的權限，可以存取 Protected Resources ，而 Resource Owner 沒辦法限制第三方程式可以拿取 Resource 的時效，以及可以存取的範圍 (subset)。</li>
<li>Resource Owner 無法只撤回單一個第三方程式的存取權，而且必須要改密碼才能撤回。</li>
<li>任何第三方程式被破解 (compromized)，就會導致使用該密碼的所有資料被破解。</li>
</ul>
<p>OAuth 解決這些問題的方式，是引入一個認證層 (authorization layer) ，並且把 client 跟 resource owner 的角色分開。在 OAuth 裡面，Client 會先索取存取權，來存取 Resource Owner 擁有的資源，這些資源會放在 Resource Server 上面，並且 Client 會得到一組不同於 Resource Owner 所持有的認證碼 (credentials) 。</p>
<p>Client 會取得一個 Access Token 來存取 Protected Resources ，而非使用 Resource Owner 的帳號密碼。Access Token 是一個字串，記載了特定的存取範圍 (scope) 、時效等等的資訊。Access Token 是從 Authorization Server 拿到的，取得之前會得到 Resource Owner 的許可。Client 用這個 Access Token 來存取 Resource Server 上面的 Protected Resources 。</p>
<p>實際使用的例子：使用者 (Resource Owner) 可以授權印刷服務 (Client) 去相簿網站 (Resource Server) 存取他的私人照片，而不需要把相簿網站的帳號密碼告訴印刷服務。這個使用者會直接授權透過一個相簿網站所信任的伺服器 (Authorization Server) ，核發一個專屬於該印刷服務的認證碼 (Access Token)。</p>
<p>OAuth 是設計來透過 HTTP 使用的。透過 HTTP 以外的通訊協定來使用 OAuth 則是超出 spec 的範圍。</p>
<p><em>Section 1</em></p>
<h2 id="oauth-2-0-de-jiao-se-ding-yi">OAuth 2.0 的角色定義</h2>
<ul>
<li><strong>Resource Owner</strong> - 可以授權別人去存取 Protected Resource 。如果這個角色是人類的話，則就是指使用者 (end-user)。</li>
<li><strong>Resource Server</strong> - 存放 Protected Resource 的伺服器，可以根據 Access Token 來接受 Protected Resource 的請求。</li>
<li><strong>Client</strong> - 代表 Resource Owner 去存取 Protected Resource 的應用程式。 "Client" 一詞並不指任何特定的實作方式（可以在 Server 上面跑、在一般電腦上跑、或是在其他的設備）。</li>
<li><strong>Authorization Server</strong> - 在認證過 Resource Owner 並且 Resource Owner 許可之後，核發 Access Token 的伺服器。</li>
</ul>
<p>Authorization Server 和 Resource Server 的互動方式不在本 spec 的討論範圍內。Authorization Server 跟 Resource Server 可以是同一台，也可以分開。單一台 Authorization Server 核發的 Access Token ，可以設計成能被多個 Resource Server 所接受。</p>
<p><em>Section 1.1</em></p>
<h2 id="ji-ben-liu-cheng-gai-guan-yu-zi-liao-ding-yi">基本流程概觀與資料定義</h2>
<p>以下是抽象化的流程概觀，以比較宏觀的角度來描述，不是實際程式運作的流程（圖出自 Spec 的 Figure 1）：</p>
<pre><code>+--------+                               +---------------+
|        |--(A)- Authorization Request -&gt;|   Resource    |
|        |                               |     Owner     |
|        |&lt;-(B)-- Authorization Grant ---|               |
|        |                               +---------------+
|        |
|        |                               +---------------+
|        |--(C)-- Authorization Grant --&gt;| Authorization |
| Client |                               |     Server    |
|        |&lt;-(D)----- Access Token -------|               |
|        |                               +---------------+
|        |
|        |                               +---------------+
|        |--(E)----- Access Token ------&gt;|    Resource   |
|        |                               |     Server    |
|        |&lt;-(F)--- Protected Resource ---|               |
+--------+                               +---------------+

                Figure 1: Abstract Protocol Flow
</code></pre>
<p>上圖描述四個角色的互動方式：</p>
<p>(A): Client 向 Resource Owner 請求授權。這個授權請求可以直接向 Resource Owner 發送（如圖），或是間接由 Authorization Server 來請求。</p>
<p>(B) Client 得到來自 Resource Owner 的 Authorization Grant （授權許可）。這個 Grant 是用來代表 Resource Owner 的授權，其表達的方式是本 spec 裡定義的四種類別 (grant types) 的其中一種（可以擴充）。要使用何種類別，則是依 Client 請求授權的方法、 Authorization Server 支援的類別而異。</p>
<p>(C): Client 向 Authorization Server 請求 Access Token ，Client 要認證自己，並出示 Authorization Grant。</p>
<p>(D): Authorization Server 認證 Client 並驗證 Authorization Grant 。如果都合法，就核發 Access Token 。</p>
<p>(E): Client 向 Resource Server 請求 Protected Resource ，Client 要出示 Access Token。</p>
<p>(F): Resource Server 驗證 Access Token ，如果合法，就處理該請求。</p>
<p><em>Section 1.2</em></p>
<h3 id="authorization-grant-shou-quan-xu-ke">Authorization Grant （授權許可）</h3>
<p>Authorization Grant 代表了 Resource Owner 授權 Client 可以去取得 Access Token 來存取 Protected Resource 。Grant 不一定是具體的資料，依 spec 裡面定義的四種內建流程，有對應不同的 grant type ，甚至在某些流程裡面會省略之，不經過 Client。</p>
<p>Client 從 Resource Owner 取得 Authorization Grant 的方式（前段圖中的 (A) 和 (B) 流程）會比較偏好透過 Authorization Server 當作中介。見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-3-endpoints/">系列文第 3 篇</a>的流程圖。</p>
<h3 id="access-token">Access Token</h3>
<p>Access Token 用來存取 Protected Resource ，是一個具體的字串（string），其代表特定的 scope （存取範圍）、時效。概念上是由 Resoruce Owner 授予，Resource Server 和 Authorization Server 遵循之 (enforced)。</p>
<p>Access Token 可以加上用來取得授權資訊的 identifier （編號或識別字等），或內建可以驗證的授權資訊（如數位簽章）。也就是說，可以由 Authorization Server 間接判定這個 Access Token 的 scope 及時效，也可以嵌在 Token 裡面，但為了防止竄改，要以加密演算法來實作資料的驗證。</p>
<p>Spec 裡面只定義抽象層，代替傳統的帳密認證，並且 Resource Server 只需要知道此一 Access Token ，不需要知道其他的認證方式。Access Token 可以有不同的格式、使用方式（如內建加密屬性）。Access Token 的內容，以及如何用它來存取 Protected Resource ，則定義在別的文件，像是 RFC 6750 (Bearer Token Usage) 。</p>
<p><em>Section 1.4</em></p>
<h4 id="access-token-type">Access Token Type</h4>
<p>Client 要認得 Access Token Type 才能使用之，若拿到認不得的 Type ，則不可以使用之。例如 RFC 6750 定義的 Bearer Token 的用法就是這樣：</p>
<pre><code>GET &#x2F;resource&#x2F;1 HTTP&#x2F;1.1
Host: example.com
Authorization: Bearer mF_9.B5f-4.1JqM
</code></pre>
<p><em>Section 7.1</em></p>
<h3 id="refresh-token">Refresh Token</h3>
<p>用來向 Authorization Server 重新取得一個新的 Access Token 的 Token ，像是現有的 Access Token 過期而無效，或是權限不足，需要更多 scopes 才能存取別的 Resource。在概念上，Refresh Token 代表了 Resource Owner 授權 Client 重新取得新的 Access Token 而不需要再度請求 Resource Owner 的授權。Client 可以自動做這件事，例如 Access Token 過期了，自動拿新的 Token，來讓應用程式的流程更順暢。</p>
<p>需注意新取得的 Access Token 時效可能比以前短、或比 Resource Owner 給的權限更少。</p>
<p>Authorization Server 不一定要核發 Refresh Token ，但若要核發，必須在核發 Access Token 的時候一併合發。某些內建流程會禁止核發 Refresh Token。</p>
<p>Refresh Token 應該只遞交到 Authorization Server ，不該遞交到 Resource Server 。</p>
<p>Refresh Token 的流程圖：</p>
<pre><code>+--------+                                           +---------------+
|        |--(A)------- Authorization Grant ---------&gt;|               |
|        |                                           |               |
|        |&lt;-(B)----------- Access Token -------------|               |
|        |               &amp; Refresh Token             |               |
|        |                                           |               |
|        |                            +----------+   |               |
|        |--(C)---- Access Token ----&gt;|          |   |               |
|        |                            |          |   |               |
|        |&lt;-(D)- Protected Resource --| Resource |   | Authorization |
| Client |                            |  Server  |   |     Server    |
|        |--(E)---- Access Token ----&gt;|          |   |               |
|        |                            |          |   |               |
|        |&lt;-(F)- Invalid Token Error -|          |   |               |
|        |                            +----------+   |               |
|        |                                           |               |
|        |--(G)----------- Refresh Token -----------&gt;|               |
|        |                                           |               |
|        |&lt;-(H)----------- Access Token -------------|               |
+--------+           &amp; Optional Refresh Token        +---------------+

        Figure 2: Refreshing an Expired Access Token
</code></pre>
<p>(A) Client 向 Authorizatino Server 出示 Authorization Grant ，來申請 Access Token 。</p>
<p>(B) Authorization Server 認證 Client 並驗證 Authorization Grant 。如果都合法，就核發 Access Token 。</p>
<p>(C) Client 向 Resource Server 請求 Protected Resource ，Client 要出示 Access Token。</p>
<p>(D) Resource Server 驗證 Access Token ，如果合法，就處理該請求。</p>
<p>(E) 步驟 (C) 和 (D) 一直重覆，直到 Access Token 過期。如果 Client 自己知道 Access Token 過期，就跳到 (G)；如則，就發送另一個 Protected Request 的請求。</p>
<p>(F) 因為 Access Token 不合法，Resource Server 回傳 Token 不合法的錯誤。</p>
<p>(G) Client 向 Authorization Server 請求 Access Token ，Client 要認證自己，並出示 Refresh Token。Client 認證的必要與否，端看 Client Type 以及 Authorization Server 的政策。</p>
<p>(H) Authorization Server 認證 Client 、驗證 Refresh Token ，如果合法，就核發新的 Access Toke （也可以同時核發新的 Refresh Token）</p>
<p>步驟 (C), (D), (E), (F) 關於 Resource Server 如何處理 request 、檢查 Access Token 的機制，不在本 spec 的範圍內，跟 Token 的格式有關。RFC 6750 的 Bearer Token 有定義，見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-6-bearer-token/">系列文第 6 篇</a>。</p>
<p><em>Section 1.5</em></p>
<h2 id="si-zhong-nei-jian-shou-quan-liu-cheng-grant-flows">四種內建授權流程 (Grant Flows)</h2>
<p>Spec 裡面定義了四種流程，分別是:</p>
<ul>
<li>Authorization Code Grant Type Flow</li>
<li>Implicit Grant Type Flow</li>
<li>Resource Owner Password Credentials Grant Type Flow</li>
<li>Client Credentials Grant Type Flow</li>
</ul>
<p>此外還可以擴充。根據流程的不同，有不同的實作細節。Client 的類型也會限制可以實作的流程，例如 Native App 就不准使用 Client Credentials ，因為這些密碼會外洩。</p>
<p>實務上不需要實作所有流程。我看了許多大網站的 OAuth 2 API，大部份會支援 Authorization Code Grant Flow，其他的則不一定。之後寫一篇文章整理。</p>
<p>這裡提一下 Clients 的類型，分成 Public 和 Confidential 兩種，根據能不能保密 Client Credentials 來區分，可以的就是 Confidential （如 Server 上的程式），不行的就是 Public （如 Native App、In-Browser App）。詳見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">系列文第 2 篇</a>。</p>
<h3 id="1-authorization-code-grant-flow">(1) Authorization Code Grant Flow</h3>
<ul>
<li>要向 Authorization Server 先取得 Grant Code 再取得 Access Token （兩步）。</li>
<li>適合 Confidential Clients ，如部署在 Server 上面的應用程式。</li>
<li>可以核發 Refresh Token。</li>
<li>需要 User-Agent Redirection。</li>
</ul>
<h3 id="2-implicit-grant-flow">(2) Implicit Grant Flow</h3>
<ul>
<li>Authorization Server 直接向 Client 核發 Access Token （一步）。</li>
<li>適合非常特定的 Public Clients ，例如跑在 Browser 裡面的應用程式。</li>
<li>Authorization Server 不必（也無法）驗證 Client 的身份。</li>
<li>禁止核發 Refresh Token。</li>
<li>需要 User-Agent Redirection。</li>
<li>有資料外洩風險。</li>
</ul>
<h3 id="3-resource-owner-password-credentials-grant-flow-shi-yong-zhe-de-zhang-hao-mi-ma">(3) Resource Owner Password Credentials Grant Flow （使用者的帳號密碼）</h3>
<ul>
<li>Resource Owner 的帳號密碼直接拿來當做 Grant。</li>
<li>適用於 Resource Owner 高度信賴的 Client （像是 OS 內建的）或是官方應用程式。</li>
<li>其他流程不適用時才能用。</li>
<li>可以核發 Refresh Token。</li>
<li>沒有 User-Agent Redirection。</li>
</ul>
<h3 id="4-client-credentials-grant-flow-client-de-zhang-hao-mi-ma">(4) Client Credentials Grant Flow （Client 的帳號密碼）</h3>
<ul>
<li>Client 的 ID 和 Secret 直接用來當做 Grant</li>
<li>適用於跑在 Server 上面的 Confidential Client</li>
<li>不建議核發 Refresh Token 。</li>
<li>沒有 User-Agent Redirection。</li>
</ul>
<h2 id="ji-shu-yao-qiu">技術要求</h2>
<h3 id="bi-xu-quan-cheng-shi-yong-tls-https">必須全程使用 TLS (HTTPS)</h3>
<p>因為資料在網路上面傳遞會被看見，所以 Spec 裡面規定全程必須使用 TLS ，而因為 OAuth 是基於 HTTP 的，所以就是 <strong>統統要使用 https</strong> 。實務上是定義在 Endpoints 。在某些 Client 無法實作有 TLS 的 Endpoint ，則會適度放寬限制。所以雖然這段標題寫的是「全程使用」，實際上是只有一些地方有規定需要經過 TLS ，但這個「一些」就包含了幾乎所有經過網路的地方，所以我就直接寫全程了。</p>
<p>至於 TLS 的版本，在 spec 寫成的時候，最新版是 TLS 1.2 ，但實務上利用最廣泛的卻是 TLS 1.0 。所以在 Spec 裡似乎沒有明確定義 TLS 的版本。</p>
<p><em>Section 1.6</em></p>
<h3 id="user-agent-yao-zhi-yuan-http-redirection">User-Agent 要支援 HTTP Redirection</h3>
<p>OAuth 2 用 HTTP 轉址 (Redirection) 用很兇， Client 或 Authorization Server 用轉址來把 Resource Owner 的 User-Agent 轉到別的地方。另外雖然 spec 裡面的範例都是 302 轉址，若要用別的方式來轉址也行，這屬於實作細節。</p>
<p><em>Section 1.7</em></p>
<h2 id="cun-qu-protected-resource-de-fang-shi">存取 Protected Resource 的方式</h2>
<p>關於 Client 如何利用 Access Token 存取 Protected Resource 的方式，在 OAuth 2.0 的 spec 裡面只有定義概念，具體的機制沒有定義：</p>
<ul>
<li>Client 要出示 Access Token 來向 Resource Server 存取 Protected Resource。</li>
<li>具體出示機制不定義，通常是用 Authorization header 搭配該 Access Token 定義的 auth scheme ，如 Bearer Token (RFC 6750) ，見<a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-6-bearer-token/">系列文第 6 篇</a>。</li>
<li>Resource Server 必須驗證 Access Token 並確認其尚未過期、確認其 scope 包含所要存取的 resource 。</li>
<li>具體驗證機制不規定，通常是 Authorization Server 和 Resource Server 之間互相傳輸資料 (interaction) 以及同步化 (coordination)。</li>
</ul>
<p><em>Section 7</em></p>
<h3 id="cuo-wu-de-hui-ying-fang-shi">錯誤的回應方式</h3>
<p>Spec 裡面也不定義機制，只定義了概念以及基本的共用協定：</p>
<ul>
<li>要是 Resource Request 失敗，則 Server 最好要提示錯誤 。至於 error code ，登記的方式規定在 Section 11.4。</li>
<li>任何新定義的 Authentication Scheme （如 Bearer Token）都最好要定義一個機制來提示錯誤，其 value 要使用 OAuth 2.0 spec 裡面規定的方式定義。</li>
<li>新定義的 Scheme 可能會只使用子集。</li>
<li>如果 error code 用具名參數（如 JSON 之類的 dictionary）回傳，則其參數名稱必須使用 <code>error</code>。</li>
<li>要是有個 Scheme 可以用在 OAuth 但不是專門設計給 OAuth ，則可以用一樣的方式來把它裡面的 error code 清單拿進來用 ※。</li>
<li>新定義的 Scheme 可以用 <code>error_description</code> 和 <code>error_uri</code> ，其意義要跟 OAuth 定義的一致。</li>
</ul>
<p>※ <em>MAY bind their error values to the registry in the same manner</em></p>
<p><em>Section 7.2</em></p>
<p>Section 11.4 裡面有規定怎麼提出新 error code 的 proposal ，有興趣的同學就看一下吧。</p>
<hr />
<h2 id="oauth-2-0-xi-lie-wen-mu-lu-1">OAuth 2.0 系列文目錄</h2>
<ul>
<li><strong><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-1-introduction/">(1) 世界觀</a> ← You Are Here</strong></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-2-cilent-registration/">(2) Client 的註冊與認證</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-3-endpoints/">(3) Endpoints 的規格</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-1-auth-code-grant-flow/">(4.1) Authorization Code Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-2-implicit-grant-flow/">(4.2) Implicit Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-3-resource-owner-credentials-grant-flow/">(4.3) Resource Owner Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-4-client-credentials-grant-flow/">(4.4) Client Credentials Grant Flow 細節</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-5-issuing-tokens/">(5) 核發與換發 Access Token</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-6-bearer-token/">(6) Bearer Token 的使用方法</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-7-security-considerations/">(7) 安全性問題</a></li>
<li><a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-implementation-differences-among-famous-sites/">各大網站 OAuth 2.0 實作差異</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>台灣時區變換的八卦</title>
            <link>https://blog.yorkxin.org/posts/time-zone-in-taiwan/</link>
            <pubDate>Mon, 26 Aug 2013 14:40:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/08/26/time-zone-in-taiwan.html</guid>
            
            <description><![CDATA[
                <p><a href="http://blog.yorkxin.org/2013/08/26/ruby-ambiguous-time/">先前有個程式</a>遇到了古時候台灣時區轉換而導致的 bug ，在那之後，稍微考據了一下台灣的時區過渡過程，發現了一些有趣的事。</p>
<p>不過要先聲明：我沒有研究歷史的專業，不知道怎麼找到最正確的資料，以下除非有列出參考資料，否則都是我自己認為的，可能不正確，歡迎指正。</p>
<h2 id="guan-yu-shi-qu-zhuan-huan-de-li-shi-shi-jian">關於時區轉換的歷史事件</h2>
<h3 id="tai-wan-de-guan-fang-shi-qu-zhi-ding-yu-1896-zao-yu-zhong-guo-da-lu">台灣的官方時區制定於 1896 ，早於中國大陸</h3>
<p>首先，大清國直到 1911 年滅亡，都沒有實施現代意義的時區，中國大陸直到中華民國統治的 1918 才打算設置時區，根據維基百科<a href="http://zh.wikipedia.org/wiki/%E4%B8%AD%E5%9C%8B%E6%99%82%E5%8D%80">〈中國時區〉</a>條目的記載：</p>
<blockquote>
<p>1912年之前，中國各地並沒有統一的標準時間。在王朝時代，國家的標準曆法由皇庭頒布，稱為「奉正朔」。</p>
<p>民國7年（1918年），中央觀象台提出將全國劃分為5個標準時區。</p>
</blockquote>
<p>所以說，在 1895 年就由大清國割讓給日本的台灣與澎湖，直到 1945 由中華民國接收以前，自然沒有使用到中華民國制定的時區。那麼台灣是什麼時候開始有官方制定的時區？</p>
<span id="continue-reading"></span>
<p>答案是 1896 年的日治初年。在割讓當年的 1895 年底，日本明治天皇頒佈了<a href="http://ja.wikisource.org/wiki/%E6%A8%99%E6%BA%96%E6%99%82%E3%83%8B%E9%97%9C%E3%82%B9%E3%83%AB%E4%BB%B6_(%E5%85%AC%E5%B8%83%E6%99%82)">敕令 167 號</a>，從明治 29 年（1896 年）起，將大日本帝國分成兩個時區，一個叫做中央標準時，一個叫西部標準時，前者以 135°E 為基準，即現代的 UTC+9 ，後者以 120°E 為基準，即現代的 UTC+8 。大日本帝國向來使用的時區稱為中央標準時，西部標準時則是在台灣、澎湖、一部份珫球群島使用。這份敕令的內文可以在 Wikisource 的 <a href="http://ja.wikisource.org/wiki/%E6%A8%99%E6%BA%96%E6%99%82%E3%83%8B%E9%97%9C%E3%82%B9%E3%83%AB%E4%BB%B6_(%E5%85%AC%E5%B8%83%E6%99%82)">〈標準時ニ關スル件 (公布時)〉</a> 找到：</p>
<blockquote>
<p>第一條　 帝國從來ノ標準時ハ自今之ヲ中央標準時ト稱ス</p>
<p>第二條　 東經百二十度ノ子午線ノ時ヲ以テ臺灣及澎湖列島竝ニ八重山及宮古列島ノ標準時ト定メ之ヲ西部標準時ト稱ス</p>
<p>第三條　 本令ハ明治二十九年一月一日ヨリ施行ス</p>
</blockquote>
<p>這之後的故事，上過《認識台灣：歷史篇》這本課本的應該都有印象，日本人教育台灣人守時的觀念，那張<a href="http://zh.wikipedia.org/wiki/%E6%99%82%E7%9A%84%E7%B4%80%E5%BF%B5%E6%97%A5">「時的紀念日」</a>海報是我印象最深的圖片。</p>
<h3 id="er-ci-da-zhan-qi-jian-gai-yong-yu-ri-ben-nei-di-xiang-tong-de-utc-9">二次大戰期間，改用與日本內地相同的 UTC+9</h3>
<p>西部標準時一直用到二次大戰爆發，在昭和 12 年（1937 年，即日本侵華戰爭開始），日本昭和天皇頒佈了<a href="http://ja.wikisource.org/wiki/%E6%98%8E%E6%B2%BB%E4%BA%8C%E5%8D%81%E5%85%AB%E5%B9%B4%E5%8B%85%E4%BB%A4%E7%AC%AC%E7%99%BE%E5%85%AD%E5%8D%81%E4%B8%83%E8%99%9F%E6%A8%99%E6%BA%96%E6%99%82%E3%83%8B%E9%97%9C%E3%82%B9%E3%83%AB%E4%BB%B6%E4%B8%AD%E6%94%B9%E6%AD%A3%E3%83%8E%E4%BB%B6">敕令 529 號</a>，廢止西部標準時 (UTC+8)，也就是全國改用中央標準時 (UTC+9) ，包括外地（台灣、澎湖、之後佔領的香港）。</p>
<p>戰爭結束之後，顯然這些原本刻意與日本內地同步的時區都必須還原，在維基百科以及我隨便找的資料裡面，都只是及「國民政府接收之後，改用中原標準時間 UTC+8」，但真的是光復當天或光復之後才改時區嗎？日本投降是 8 月 15 日，到 10 月 25 日的接收儀式之間，台灣在名義上還是屬於大日本帝國，又，使用了西部標準時間的還包括琉球群島的一部份，這些地區呢？什麼時候還原的？</p>
<p>我在<a href="http://subtpg.tpg.gov.tw/og/q1.asp">臺灣省政府公報網路查詢系統</a>使用類似的關鍵字沒有找到切換時區的公文，只有實施夏令時間的公文。有兩種可能：①在光復之前就切換到 UTC+8 ，所以國民政府不需要有任何行政流程，或是②有切換，但不重要，所以沒記載。……怎麼可能咧。</p>
<p>就在我搜尋的過程中，我在國史館的網站找到一份<a href="http://163.29.208.22:8080/govsaleShowImage/connect_img.php?s=00101738900090036&amp;e=00101738900090037">文獻</a>，根據推友 <a href="https://twitter.com/FreeLeaf">@FreeLeaf</a> 的<a href="https://twitter.com/FreeLeaf/status/355651015292362752">翻譯</a>，是台灣總督府專賣局收到的內部公文，要在 9 月 21 日起改回使用西部標準時（更正啟事：先前我寫錯成「專賣局發的公文」，感謝 <a href="https://twitter.com/FreeLeaf/status/372228698209931264">@FreeLeaf 再度指正</a>）：</p>
<p><a href="http://163.29.208.22:8080/govsaleShowImage/connect_img.php?s=00101738900090036&amp;e=00101738900090037"><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2013/2013-08-26-time-zone-in-taiwan/%E7%B8%BD%E4%BA%A4%E7%AC%AC506%E8%99%9F.jpg" alt="" /></a></p>
<p><em>出處：<a href="http://163.29.208.22:8080/govsaleShowImage/connect_img.php?s=00101738900090036&amp;e=00101738900090037">國史館網站</a></em></p>
<p>又在<a href="http://www.ncku.edu.tw/~ncku70/menu/001/01_01.htm">成功大學的某個校簡史的網頁</a>也提到：</p>
<blockquote>
<p>1945（昭和20年）</p>
<p>8月15日：大詔宣布戰爭結束。</p>
<p>9月21日：本日開始，全部改為西部標準時。</p>
</blockquote>
<p>這樣可以說是 9 月 21 日當天改時區嗎？但是，更關鍵的官方文件（像是台灣總督府的公文、公告等）我卻沒有找到，也不知道上哪裡找。但我比較傾向相信在 1945 年的時候，並非國民政府接收台灣之後才改時區，而是最晚 9 月 21 日就改了時區。</p>
<p><strong>Update</strong> (2014/07/03) 終於找到了官方的文件，在<a href="http://db2.th.gov.tw/db2/view/index.php">臺灣總督府（官）報資料庫</a>找到的<a href="http://db2.th.gov.tw/db2/view/showDataForm.php?CollectionNo=0072031018a005">檔案（需註冊）</a>、以及其<a href="http://db2.th.gov.tw/db2/view/viewImg.php?imgcode=0072031018a&amp;num=19&amp;bgn=019&amp;end=019&amp;otherImg=&amp;type=gener">圖檔（免登入）</a></p>
<p><a href="http://db2.th.gov.tw/db2/view/viewImg.php?imgcode=0072031018a&amp;num=19&amp;bgn=019&amp;end=019&amp;otherImg=&amp;type=gener"><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2013/2013-08-26-time-zone-in-taiwan/q2uLpUyGRc6vqKqclQl1_jpeg.jpg" alt="jpeg.jpg" /></a></p>
<blockquote>
<p>告示第三百八十六號</p>
<p>昭和十二年告示第二百七號（臺灣ノ標準時ニ關スル件）ハ之ヲ廢止シ昭和二十年九月二十一日午前一時ヲ以テ昭和二十年九月二十一日午前零時トス</p>
<p>昭和二十年九月十九日　臺灣總督　安藤　利吉</p>
<p><q>收錄於《臺灣總督府官報》昭和二十年九月十九日　第千十八號</q></p>
</blockquote>
<p>簡單來說，就是廢止昭和 12 年總督府的告示第 207 號，而其為公告實施敕令 529 號，廢止西部標準時，那麼負負得正，就是回歸到西部標準時了。感謝 <a href="https://twitter.com/rail02000">@rail02000</a> 協助翻譯。</p>
<p><em>（2014/07/03 更新結束）</em></p>
<p>至於琉球群島西部是否也曾經改回 UTC+8 ，後來又再改成現在的 UTC+9 ，我很是好奇，但就沒再研究了……。</p>
<p>這裡稍微岔一下題。一開始提到，會研究這個是因為之前遇到一個神秘的程式 bug ，這個世界上有一個資料庫叫做 <a href="http://www.iana.org/time-zones">TZ Database</a> ，這是幾乎是所有電腦作業系統都會內建一份的時區資料庫，其中當然有記載台灣的時區，比較特別的是，我今天看到的版本 2013d ，香港的時區資料，有記載香港日佔時期使用 UTC+9 時區的史實，但台灣的卻沒有。以下取自 ftp://ftp.iana.org/tz/data/asia ：</p>
<pre><code># Zone  NAME            GMTOFF  RULES   FORMAT  [UNTIL]
Zone    Asia&#x2F;Hong_Kong  7:36:42 -       LMT     1904 Oct 30
                        8:00    HK      HK%sT   1941 Dec 25
                        9:00    -       JST     1945 Sep 15
                        8:00    HK      HK%sT

Zone    Asia&#x2F;Taipei     8:06:00 -       LMT     1896 # or Taibei or T&#x27;ai-pei
                        8:00    Taiwan  C%sT
</code></pre>
<p>不過事實上，這份資料庫是公共維護的，並不是所有的資料來源都具有權威性，維護者建議，如果你知道更詳細的，可以告訴他們。在我挖到二戰期間台灣用 UTC+9 的史實之後，我寄信給維護者，不過過了一個多月，還沒有回信……。</p>
<h2 id="guan-yu-tai-wan-shi-shi-xia-ri-jie-yue-shi-jian-dst-de-ba-gua">關於台灣實施夏日節約時間 (DST) 的八卦</h2>
<p><strong>Update</strong> 2014/07/11 考據了所有關於日光節約時間的公文，整理在〈<a href="http://blog.yorkxin.org/posts/2014/07/11/dst-in-taiwan-study">台灣日光節約時間之考據</a>〉這篇新文章。</p>
<p>根據<a href="http://www.cwb.gov.tw/V7/knowledge/astronomy/cdata/summert.htm">中央氣象局的網頁</a>，中華民國的夏日時間制從民國 34 年到 68 年，斷斷續續實施了好幾次。但其中民國 34 年的那一次沒有在台灣實施。為什麼呢？很明顯嘛， 1945 年實施的日期是 5 月 1 日到 9 月 30 日，台灣光復是 10 月 25 日，怎麼可能實施呢？</p>
<p>但這個簡單的邏輯謬誤，卻被許多網頁複製貼上，稱其「台灣的日光節約時間」，卻沒有仔細考據。</p>
<p>在考據日光節約時間的時候，又挖到<a href="http://subtpg.tpg.gov.tw/og/image2.asp?f=0360310AKZ431">臺灣省政府的公報</a>，其中一份公文指出，民國 36 年的夏令時間，結束日期從原本的 9 月 30 日延長到 10 月 31 日，也就是說氣象局的網頁上所說的 34 年 到 40 年間，實施日期到 9 月 30 止，在 36 年這一年要再更正。原文抄錄如下，在下圖的右下角：</p>
<blockquote>
<p>臺灣省政府代電　參陸申？府人甲字第六○○二一號　中華民國卅六年九月十五日（不另行文）</p>
<p>事由：奉電爲本年夏令時間延長至十月三十一日午夜二十四時止等因電？？</p>
<p>本府所屬各機關學校：頃奉行政院卅六人字第三五一一七號代電開：「奉國府調
令，本年夏令時間定自四月十五日零時起，至九月二十日午夜二十四時止，前經
由寄通飭遵照在案。茲為勵行節約消費起見，再將是項夏令時間延長，其通用期
間至本年十月三十一日午夜二十四時爲止。等因；除分電外，仰卽遵照，幷飭爲
遵照。」等因；奉此，自應遵辦，合行電希遵照，並轉飭屬遵照。臺灣省政府
卅六申（刪）府人甲</p>
</blockquote>
<p><a href="http://subtpg.tpg.gov.tw/og/image2.asp?f=0360310AKZ431"><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2013/2013-08-26-time-zone-in-taiwan/image2.gif" alt="" /></a></p>
<p>這些 bug 我已經寄信去告知中央氣象局網站了，他們給我的回信內容是，肯定它是 bug ，會再研究，但為免誤導，先把原本的網頁下架。我現在已經看不到<a href="http://www.cwb.gov.tw/V7/astronomy/cdata/summert.htm">原本那個網頁</a>了，但 Google 又可以找到<a href="http://www.cwb.gov.tw/V7/knowledge/astronomy/cdata/summert.htm">另一個存在錯誤的網頁</a>……。</p>
<h2 id="fan-wai-pian-guan-yu-xiang-gang-ri-zhan-shi-qi-shi-shi-xia-ri-jie-yue-shi-jian-de-ba-gua">番外篇：關於香港日佔時期實施夏日節約時間（？）的八卦</h2>
<p>研究以上這些東西難免會去找到香港的資料，最具權威的當然應該要是香港天文台，不過<a href="http://www.hko.gov.hk/gts/time/Summertimec.htm">香港天文台介紹夏令時間的網頁</a>竟然是這樣寫的：</p>
<ul>
<li>1941: 4 月 1 日至 9 月 30 日</li>
<li>1942: 全年</li>
<li>1943: 全年</li>
<li>1944: 全年</li>
<li>1945: 全年</li>
<li>1946: 4 月 20 日至 12 月 1 日</li>
</ul>
<p><strong>全年</strong>！！這不明擺著有問題的嘛！！夏季都不夏季了啊！！不可以把因為香港被日本佔領所以改時區成 UTC+9 稱做夏令時間啊！！！</p>
<p>哪位香港網友看到的麻煩去函指正一下吧……即使是恥辱的歷史，也要正視啊（正色）。</p>
<p>這個 bug 也有人在 TZ Database 裡面提到：</p>
<blockquote>
<p>From Lee Yiu Chung (2009-10-24):
I found there are some mistakes for the...DST rule for Hong Kong. [According] to the DST record from Hong Kong Observatory [...], there are some missing and incorrect rules. [...]</p>
<p>From Arthur David Olson (2009-10-28):
Here are the dates given at http://www.hko.gov.hk/gts/time/Summertime.htm as of 2009-10-28:</p>
<p>Year        Period
1941        1 Apr to 30 Sep
1942        Whole year
1943        Whole year
1944        Whole year
1945        Whole year
1946        20 Apr to 1 Dec
[...]</p>
<p>The Japanese occupation of Hong Kong began on 1941-12-25.
The Japanese surrender of Hong Kong was signed 1945-09-15.</p>
</blockquote>
<hr />
<h2 id="can-kao-zi-liao">參考資料</h2>
<ul>
<li><a href="http://zh.wikipedia.org/wiki/%E5%9C%8B%E5%AE%B6%E6%A8%99%E6%BA%96%E6%99%82%E9%96%93">國家標準時間 - 維基百科，自由的百科全書</a></li>
<li><a href="http://en.wikipedia.org/wiki/Time_in_Japan">Japan Standard Time - Wikipedia, the free encyclopedia</a></li>
<li><a href="http://ja.wikisource.org/wiki/%E6%A8%99%E6%BA%96%E6%99%82%E3%83%8B%E9%97%9C%E3%82%B9%E3%83%AB%E4%BB%B6_(%E5%85%AC%E5%B8%83%E6%99%82)">標準時ニ關スル件 (公布時) - Wikisource</a></li>
<li><a href="http://ja.wikisource.org/wiki/%E6%98%8E%E6%B2%BB%E4%BA%8C%E5%8D%81%E5%85%AB%E5%B9%B4%E5%8B%85%E4%BB%A4%E7%AC%AC%E7%99%BE%E5%85%AD%E5%8D%81%E4%B8%83%E8%99%9F%E6%A8%99%E6%BA%96%E6%99%82%E3%83%8B%E9%97%9C%E3%82%B9%E3%83%AB%E4%BB%B6%E4%B8%AD%E6%94%B9%E6%AD%A3%E3%83%8E%E4%BB%B6">明治二十八年勅令第百六十七號標準時ニ關スル件中改正ノ件 - Wikisource</a></li>
<li><a href="http://163.29.208.22:8080/govsaleShowImage/connect_img.php?s=00101738900090036&amp;e=00101738900090037">台灣總督府專賣局的公文（國史館）</a></li>
<li><a href="http://www.ncku.edu.tw/~ncku70/menu/001/01_01.htm">世紀回眸（成功大學網站）</a></li>
<li><a href="http://subtpg.tpg.gov.tw/og/image2.asp?f=0360310AKZ431">臺灣省政府公報 民國 36 年秋</a></li>
<li><a href="http://subtpg.tpg.gov.tw/og/q1.asp">臺灣省政府公報網路查詢系統</a></li>
<li><a href="http://db2.th.gov.tw/db2/view/showDataForm.php?CollectionNo=0072031018a005">臺灣ノ標準時ニ關スル件</a> （需登入）、<a href="http://db2.th.gov.tw/db2/view/viewImg.php?imgcode=0072031018a&amp;num=19&amp;bgn=019&amp;end=019&amp;otherImg=&amp;type=gener">其圖檔</a>（免登入）</li>
<li><a href="http://db2.th.gov.tw/db2/view/">臺灣總督府府官報資料庫</a>（需登入）</li>
<li><del><a href="http://www.cwb.gov.tw/V7/knowledge/astronomy/cdata/summert.htm">中央氣象局的網頁</a></del> 已下架</li>
</ul>
<h2 id="yan-shen-yue-du">延伸閱讀</h2>
<ul>
<li>程式設計界的前輩良葛格所寫的時間與日期基本知識<a href="http://www.codedata.com.tw/java/jodatime-jsr310-2-time-abc/">【Joda-Time 與 JSR310 】（2）時間的 ABC by caterpillar | CodeData</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>Ruby: &quot;Ambiguous Time: 1895-12-31 23:59:59&quot;</title>
            <link>https://blog.yorkxin.org/posts/ruby-ambiguous-time/</link>
            <pubDate>Mon, 26 Aug 2013 14:30:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2013/08/26/ruby-ambiguous-time.html</guid>
            
            <description><![CDATA[
                <p>在<a href="http://www.techbang.com">T客邦</a>的某站某個網址，有時候會出現這個 Exception ：</p>
<pre><code>TZInfo::AmbiguousTime: 1895-12-31 23:59:59 UTC is an ambiguous local time.
</code></pre>
<p>出現的機率很低，大概數個月才一次， User Agent 都是搜尋引擎的 bot ，所以也不能算是惡意 request 。這個 exception 不會影響網站的運作，但就是很好奇為什麼會出現。</p>
<p>前些日子終於找到時間來深入研究了，結果是 TZInfo 這個時區 gem 的問題。</p>
<span id="continue-reading"></span>
<h2 id="ambiguous-time-cheng-shi-wu-fa-zheng-que-zhuan-huan-dang-di-shi-jian-dao-utc">Ambiguous Time: 程式無法正確轉換當地時間到 UTC</h2>
<p>根據文件， <a href="http://tzinfo.github.io/">TZInfo</a> 裡面的 <a href="http://tzinfo.rubyforge.org/doc/TZInfo/AmbiguousTime.html">AmbiguousTime</a> exception ，只會從 <a href="http://tzinfo.rubyforge.org/doc/TZInfo/Timezone.html#method-i-period_for_local">Timezone.period_for_local</a> 和 <a href="http://tzinfo.rubyforge.org/doc/TZInfo/Timezone.html#method-i-local_to_utc">Timezone.local_to_utc</a> 裡面 raise 出來，表示「這個本地時間在 UTC 可能有多個對應的時間，所以不知道怎麼轉成 UTC」。示例：</p>
<pre><code>&gt; time = Time.new(1895,12,31,23,59,59)
 =&gt; 1895-12-31 23:59:59 +0800

&gt; TZInfo::Timezone.get(&quot;Asia&#x2F;Taipei&quot;).local_to_utc(time)
 =&gt; TZInfo::AmbiguousTime: Time: 1895-12-31 23:59:59 UTC is an ambiguous local time.

&gt; TZInfo::Timezone.get(&quot;Asia&#x2F;Shanghai&quot;).local_to_utc(time)
 =&gt; 1895-12-31 15:54:02 UTC
</code></pre>
<p>上述示例使用的 TZInfo 版本是 <a href="https://rubygems.org/gems/tzinfo/versions/0.3.37">0.3.37</a> 。</p>
<p>如果升級到 <a href="https://rubygems.org/gems/tzinfo/versions/1.0.1">TZInfo 1.0.1</a> 就沒有這個問題，但是， TZInfo 1.0 系列的時區資料，是直接讀 OS 內建的 <a href="http://www.iana.org/time-zones">IANA TZ Database</a> ，而不是像以前的版本，是將 IANA TZ Database parse 出來變成 Ruby representation ，包成 gem ，載入 tzinfo 的時候將資料結構塞到記憶體裡面。示例：</p>
<pre><code>&gt; time = Time.new(1895,12,31,23,59,59)
 =&gt; 1895-12-31 23:59:59 +0800

&gt; TZInfo::Timezone.get(&quot;Asia&#x2F;Taipei&quot;).local_to_utc(time)
 =&gt; 1895-12-31 15:59:59 UTC

&gt; TZInfo::Timezone.get(&quot;Asia&#x2F;Shanghai&quot;).local_to_utc(time)
 =&gt; 1895-12-31 15:54:02 UTC
</code></pre>
<p>原本在 0.3.x 使用的資料庫，抽出成 <a href="https://rubygems.org/gems/tzinfo-data">tzinfo-data</a> ，在 TZInfo 1.0.x ，如果 gemset 裡面有安裝了 tzinfo-data ， TZInfo 就會改用 tzinfo-data 並讀取 Ruby representation ，這樣的話，就會發生同樣的錯誤。</p>
<p><del>要排除這個問題，最簡單的做法當然就是升級 TZInfo 到 1.x 系列，因為顯然這個問題只有在 0.3.x 才有。不過 Rails 綁 <code>tzinfo ~&gt; 0.3.37</code> （3.2 系列綁在 ActiveRecord ， 4.0 系列綁在 ActiveSupport），所以寫 App 的人也沒辦法直接 override 。最後我們決定 wontfix。</del></p>
<p><strong>更正啟事</strong>：<a href="https://github.com/tzinfo/tzinfo-data/issues/1">根據 TZInfo gem 維護者的說法</a>，會 raise AmbiguousTime 才是正確的行為，因為根據 TZ Database 的規則，把這樣的時區轉換視為「在 UTC 時間到 1895 年 12 月 31 日 15:54:00 的時候，在 Asia/Taipei 時區的時鐘，要往回撥 6 分鐘」，因此，從當地時間轉到 UTC 就有兩種選擇，所以才會是 AmbiguousTime 。至於為什麼採用 OS 內建的資料庫就沒事，請見下文。</p>
<p>他提出的 Workaround 是，既然程式不知道要選哪一個時間，就幫他選一個吧，例如，第一個：</p>
<pre><code>	&gt; TZInfo::Timezone.get(&quot;Asia&#x2F;Taipei&quot;).local_to_utc(time) {|periods| periods.first }
	=&gt; 1895-12-31 15:53:59 UTC
</code></pre>
<h2 id="zheng-zheng-6-fen-zhong-de-ambiguoustime">整整 6 分鐘的 AmbiguousTime</h2>
<p>嘗試窮舉哪些時間會有問題：</p>
<pre data-lang="ruby" class="language-ruby "><code class="language-ruby" data-lang="ruby">require &#x27;tzinfo&#x27;

tpe = TZInfo::Timezone.get(&quot;Asia&#x2F;Taipei&quot;)

time = Time.new(1895,12,31,0,0,0)

while time.year &lt; 1896
  begin
    tpe.local_to_utc time
  rescue TZInfo::AmbiguousTime =&gt; e
    puts &quot;Conversion failed: #{time}&quot;
  ensure
    time += 1
  end
end
</code></pre>
<p>這結果是從 23:54:00 到 23:59:59 每一秒都有問題：</p>
<pre><code>Conversion failed: 1895-12-31 23:54:00 +0800
# ...
Conversion failed: 1895-12-31 23:59:59 +0800
</code></pre>
<p>但只有整整 6 分鐘。</p>
<p>去翻了 IANA TZ Database ，發現到有定義 Local Mean Time 是 <strong>GMT+8:00:06</strong> ，並且這個定義直到 1896 年才廢止，改用時區 UTC+8 ，符合史實：日本統治台灣之後，在 1896 年訂台灣和一部份珫球群島採西部標準時，以現在的說法就是 UTC+8 。見 <a href="http://en.wikipedia.org/wiki/Time_in_Japan">http://en.wikipedia.org/wiki/Time_in_Japan</a> 。</p>
<pre><code># Zone  NAME        GMTOFF  RULES   FORMAT  [UNTIL]
Zone    Asia&#x2F;Taipei 8:06:00 -       LMT     1896 # or Taibei or T&#x27;ai-pei
                    8:00    Taiwan  C%sT
</code></pre>
<p>所謂「整整 6 分鐘都無法轉換」可能跟這個 LMT 的定義有關。</p>
<p><del>目前只能猜想是 TZInfo library 的問題，可能是 0.3.x 的轉換程式沒有考慮到某些極端狀況。若你的程式很在意這個「從無到有」的過渡時期，請考慮改用 TZInfo 1.0.x 。</del></p>
<p><strong>更正啟事：</strong> <a href="https://github.com/tzinfo/tzinfo-data/issues/1">根據 TZInfo gem 維護者的說法</a>，<strong>Ambiguous Time 是預期的行為</strong>，至於為什麼使用 OS 內建的 TZ Database 和使用 tzdata-info 的結果不同，可能是 OS 內建的版本不支援 64-bit timestamp。問完之後會再整理成一篇，圍觀網址： <a href="https://github.com/tzinfo/tzinfo-data/issues/1">https://github.com/tzinfo/tzinfo-data/issues/1</a> 。</p>
<hr />
<p>後來稍微考據了一下台灣的時區過渡過程，發現了一些有趣的事，<a href="http://blog.yorkxin.org/2013/08/26/time-zone-in-taiwan/">下一篇文章詳述</a>。</p>

            ]]></description>
        </item>
        <item>
            <title>利用 CSS 分別設定中文字、英數、注音、假名的字體：使用 CSS3 @font-face</title>
            <link>https://blog.yorkxin.org/posts/assign-fonts-for-specific-characters/</link>
            <pubDate>Sun, 17 Jun 2012 09:23:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2012/06/17/assign-fonts-for-specific-characters.html</guid>
            
            <description><![CDATA[
                <p>先曬一下結果的畫面，也可以用 Chrome 或 Safari ，<a href="http://playground.yorkxin.org/mixed-font-face/">打開這裡看 demo</a>（目前 Firefox 不支援）：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2012/2012-06-17-assign-fonts-for-specific-characters/Screen%20Shot%202012-06-17%20at%2017.51.48.png" alt="" /></p>
<p>以上<strong>全都是用 CSS 做出來的</strong>，不是一個一個反白再選字體（要是這樣我也不用寫一篇了）。</p>
<p>要做到這一點，必須使用 CSS3 <code>@font-face</code> 提供的 <code>unicode-range</code> descriptor 。這招也不是沒有人玩過，在 2011 年底，就有人拿來<a href="http://24ways.org/2011/unicode-range">設定西文裡面 "&amp;" (ampersand) 的字體</a>了，而在中文圈裡，<a href="https://twitter.com/ethantw">@ethantw</a> 在 2011 年中期開始製作<a href="http://ethantw.net/lab/han/biaodian_fuhao_yangshi.html">漢字標準格式・標點樣式</a>，來修正不同漢字地區的標點符號，當前的版本也應用了 <code>unicode-range</code>。</p>
<p>以下就來解釋如何完成的。</p>
<span id="continue-reading"></span>
<h2 id="css3-font-face-de-unicode-range">CSS3 @font-face 的 unicode-range</h2>
<p>根據 <a href="http://www.w3.org/TR/css3-fonts/#descdef-unicode-range">W3C 的 spec</a> ，<code>unicode-range</code> 是這樣用的：</p>
<pre data-lang="css" class="language-css "><code class="language-css" data-lang="css">@font-face {
  font-family: MyCustomFont;
  unicode-range: U+00-7F; &#x2F;* ASCII *&#x2F;
  src: local(Helvetica), local(Arial); &#x2F;* 先找 Helvetica ，沒有的話用 Arial *&#x2F;
}
</code></pre>
<p>這樣子的話，在 CSS 裡面就可以使用 <code>font-family: MyCustomFont</code> ，但是它只會套用在 U+00 到 U+7F 這些字元（<a href="http://en.wikipedia.org/wiki/Basic_Latin_%28Unicode_block%29">Basic Latin</a>，即 ASCII），即使 Helvetica 和 Arial 有提供其他字元，例如帶有重音符號的拉丁字母 <code>é</code> （U+00E9，在 <a href="http://en.wikipedia.org/wiki/Latin-1_Supplement_%28Unicode_block%29">Latin-1 Supplement block</a>），它還是不會套用這個字體。</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2012/2012-06-17-assign-fonts-for-specific-characters/Screen%20Shot%202012-06-17%20at%2021.36.01.png" alt="" /></p>
<h3 id="unicode-range-de-xie-fa">unicode-range 的寫法</h3>
<p><code>unicode-range</code> 的 syntax 有以下 3 種（範例由 W3C spec 而來）：</p>
<ul>
<li><code>U+416</code> - 單一個 code point</li>
<li><code>U+400-4FF</code> - 某個 code point 區間（含頭尾）</li>
<li><code>U+4??</code> - 尾碼任意的 code point，以這個例子來說，等義於 <code>U+400-4FF</code>。</li>
</ul>
<p>如果要指定多個 ranges ，就是用逗號 <code>,</code> 分開：</p>
<pre data-lang="css" class="language-css "><code class="language-css" data-lang="css">@font-face {
  &#x2F;* ... *&#x2F;
  unicode-range: U+123, U+456-7FF, U+9??;
}
</code></pre>
<p><code>unicode-range</code> 預設值是 <code>U+0-10FFFF</code> ，也就是涵蓋了所有 <a href="http://en.wikipedia.org/wiki/Unicode_block">Unicode 的 code point space</a>，白話的意思就是「不寫的話，這個 font face 要套用到所有字元」。</p>
<h2 id="dui-tong-yi-ge-font-family-de-te-ding-unicode-range-she-zi-ti">對同一個 font-family 的特定 unicode-range 設字體</h2>
<p>這招在 W3C 的 spec 有教，以下的例子是「拉丁字、注音符號、日文假名用特別的字體，其他用預設的 Heiti TC（如果沒有 Heiti TC，就使用微軟正黑體）」。我們都知道，Heiti TC 的拉丁字、注音符號、日文假名都很醜，至於微軟正黑體是為了在 Windows 7 上面 Demo 用的：</p>
<pre data-lang="css" class="language-css "><code class="language-css" data-lang="css">@font-face {
  font-family: MyCustomFont;
  src: local(Heiti TC), local(&quot;微軟正黑體&quot;);
  &#x2F;* no unicode-range; default to all characters *&#x2F;
}

&#x2F;* Latin characters 專用 *&#x2F;
@font-face {
  font-family: MyCustomFont; &#x2F;* 同樣的 font-family *&#x2F;
  unicode-range: U+00-024F;  &#x2F;* Latin, Latin1 Sup., Ext-A, Ext-B *&#x2F;
  src: local(Helvetica),     &#x2F;* OS X preferred *&#x2F;
       local(Arial);         &#x2F;* Other OS *&#x2F;
}

&#x2F;* 注音符號專用 *&#x2F;
@font-face {
  font-family: MyCustomFont;      &#x2F;* 同樣的 font-family *&#x2F;
  unicode-range: U+3100-312F;     &#x2F;* Bopomofo *&#x2F;
  src: local(LiHei Pro),          &#x2F;* OS X *&#x2F;
       local(&quot;微軟正黑體&quot;); &#x2F;* Windows Vista+ *&#x2F;
}

&#x2F;* 日文假名專用 *&#x2F;
@font-face {
  font-family: MyCustomFont;            &#x2F;* 同樣的 font-family *&#x2F;
  unicode-range: U+3040-30FF;           &#x2F;* Hiragana, Katakana *&#x2F;
  src: local(Hiragino Kaku Gothic Pro), &#x2F;* OS X *&#x2F;
       local(Meiryo);                   &#x2F;* Windows Vista+ *&#x2F;
}

body {
  font-family: MyCustomFont, sans-serif;
}
</code></pre>
<p>最後的結果，就是拉丁字、注音符號、日文假名用了另一種字體，而其他的文字則是用 Heiti TC；在 Windows 上面，則是使用微軟正黑體（假設使用者沒有自行安裝 Mac OS X 的字體）。</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2012/2012-06-17-assign-fonts-for-specific-characters/Screen%20Shot%202012-06-17%20at%2021.42.43.png" alt="" /></p>
<h2 id="fu-xie-generic-family">覆寫 Generic Family</h2>
<p><a href="http://www.w3.org/TR/css3-fonts/#generic-font-families">Generic Family</a> 就是 sans-serif 、 serif 這些基本的 style 。我在嘗試 <code>@font-face</code> 的時候，很好奇能不能把 <code>font-family</code> 指定成 Generic Family ，用來覆寫瀏覽器預設（或在偏好設定裡指定）的這些字體。實際試了一下，還真的可以，只要把上面的 CSS 裡面的 <code>MyCustomFont</code> 改成 <code>sans-serif</code> ，然後把 <code>body</code> 的 <code>font-family</code> 設成 <code>sans-serif</code> ，效果就跟之前一樣。</p>
<h2 id="zui-zhong-xiao-guo">最終效果</h2>
<p>文章一開頭的效果跟上面所示的例子其實不太一樣：</p>
<ol>
<li>沒有預設字體，交給瀏覽器決定。</li>
</ol>
<ul>
<li>漢字用 Heiti TC</li>
<li>其他一樣</li>
</ul>
<p>並且把 <code>serif</code> 也加進來。<a href="http://playground.yorkxin.org/mixed-font-face/">實際 demo 在此</a>。</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2012/2012-06-17-assign-fonts-for-specific-characters/Screen%20Shot%202012-06-17%20at%2017.51.48.png" alt="" /></p>
<h2 id="hou-ji-web-font">後記：Web Font</h2>
<p>會做這個實驗，是前幾天看到 <a href="https://twitter.com/ethantw">@ethantw</a> 做的<a href="http://ethantw.net/lab/han/biaodian_fuhao_yangshi.html">漢字標準格式・標點樣式</a>，可以讓標點符號符合台、港式（置中）或中、日式（左下角）的標點。我很是好奇他怎麼能夠只針對標點符號去設定字體，於是打開了他的 CSS ，發現他在 <code>@font-face</code> 裡面使用了 <code>unicode-range</code>。</p>
<p>我以前只知道 <code>@font-face</code> 拿來玩 Web Font，也就是說即使你的電腦裡沒有這個字體，也可以讓瀏覽器去下載並套用。現在才知道 <code>@font-face</code> 裡面可以寫 <code>unicode-range</code> 並且重覆寫 <code>font-family: MyCustomFont</code> 來製作出混合了數種字體的新字體。</p>
<p>事實上，上一次 <a href="http://blog.yorkxin.org/2012/05/03/chrome-18-chinese-font-fail-and-solution/">Chrome 18 字體亂象</a>發生時，我曾經嘗試在 User Agent Stylesheet （<code>Custom.css</code>）裡面覆寫 <code>sans-serif</code> 的字體為 <code>Helvetica</code> ，想說這樣應該可以讓 Chrome 的字體 fallback 機制回到 OS X 處理。但那時候這樣做卻沒有成功，所以轉而去搞 Extension 。日前 Safari 6 也出現了同樣的功能，於是試了一下 <a href="http://blog.yorkxin.org/2012/06/17/safari-6-per-script-font-fallback/">custom stylesheet</a> ，還真的可以 hack 掉 fallback 機制，那時候再回去 Chrome 試這招 override <code>sans-serif</code> 的 hack，竟然就可以了。</p>
<p>不過說到 Web Font 這技術，因為設計給漢字文化圈的字體都比較大（以 MB 為單位，因為字元多），所以這項技術只流行在歐美的網站（用拉丁字母，數十 KB 而已）。當然台灣也有<a href="http://www.justfont.com/">救世字</a>在嘗試提供中文 Web Font 服務，最近的 <a href="http://hitcon.org/2012/">HITCON 2012</a> 也展示了這項服務的潛力。</p>
<h2 id="can-kao-zi-liao">參考資料</h2>
<ul>
<li><a href="http://www.w3.org/TR/css3-fonts/#descdef-unicode-range">Character range: the unicode-range descriptor - CSS Fonts Module Level 3 - W3C</a></li>
<li><a href="http://en.wikipedia.org/wiki/Unicode_block">Unicode block - Wikipedia</a></li>
<li><a href="http://24ways.org/2011/unicode-range">24 ways: Creating Custom Font Stacks with Unicode-Range</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>Copy as Markdown -- 輕鬆複製 Markdown 救手指</title>
            <link>https://blog.yorkxin.org/posts/copy-as-markdown/</link>
            <pubDate>Thu, 01 Mar 2012 18:52:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2012/03/02/copy-as-markdown.html</guid>
            
            <description><![CDATA[
                <p>如果你跟我一樣是 Markdown 的愛用者（就像 Blog 也用 Markdown 寫），你或許常常在引用資料的時候一直要按 <code>Ctrl+C</code>、<code>Ctrl+V</code> 的快速鍵。遇到圖片還得用滑鼠按「複製圖片網址」。有時我會覺得我手指快斷了。</p>
<p><a href="https://chrome.google.com/webstore/detail/fkeaekngjflipcockcnpobkpbbfbhmdn"><strong>Copy as Markdown</strong></a> 是我做的 Chrome Extension ，專門做這些事：</p>
<ol>
<li>把目前的分頁標題及網址複製成 Markdown</li>
<li>按右鍵把超連結複製成 Markdown
<ul>
<li>如果你是在圖片上按右鍵，則複製出來的會是連結包圖片 <code>[![](img url)](link url)</code></li>
</ul>
</li>
<li>按右鍵把圖片複製成 Markdown</li>
<li>把目前視窗的所有分頁及網址複製成 Markdown 的列表（！）</li>
</ol>
<p>一按就完成，手指可以空出來做更多事，我是說寫稿。</p>
<h2 id="zai-dian">載點</h2>
<p><a href="https://chrome.google.com/webstore/detail/fkeaekngjflipcockcnpobkpbbfbhmdn">Chrome 網路商城由此去</a> 、<a href="https://github.com/yorkxin/copy-as-markdown">原始碼由此去</a> (MIT)。</p>
<h2 id="jie-tu">截圖</h2>
<p>大概像這樣（其實下圖的 markdown code 我也是直接用這個 extension 複製的… 超懶人 XD）：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2012/2012-03-02-copy-as-markdown/Screen%20Shot%202012-03-02%20at%2001.40.37.png" alt="" /></p>
<h2 id="known-issues">Known Issues</h2>
<p>已知問題有幾個：</p>
<ol>
<li><strong>在 Windows 的 Chorme，複製連結的時候，不會複製連結文字</strong>。這個原因是 Mac OS X 會自動在按右鍵的時候就選取文字，但 Windows 版本不會。這是我上架之後才發現的 bug ，沒用 Windows 測試真的不行 orz</li>
<li><strong>複製圖片的時候，抓不到 <code>alt</code> 屬性</strong>；這個我還在找答案，很顯然直接從 Chrome 的 Context Menu <a href="http://code.google.com/chrome/extensions/contextMenus.html#type-OnClickData">OnClickData 物件</a> 是抓不到的。這個問題要是解決了， 1 或許也就解決了。</li>
</ol>
<span id="continue-reading"></span>
<h2 id="yi-xia-xin-de-wen">以下心得文</h2>
<p>這是我最近這兩天開始做的，但其實已經想很久了。壓倒我的最後一根稻草，是最近在學 Code Academy 的 <a href="http://www.codecademy.com/languages/javascript">JavaScript 課程</a>，總覺得終於有條理地學了 JavaScript ，那就把以前因為沒技術所以寫不出來的東西寫一寫吧。</p>
<p>第一次做 Chrome Extension ，當然第一個是參考<a href="http://code.google.com/chrome/extensions/docs.html">官方文件</a>。不過官方文件似乎只列出了基本的 API ，使用範例也很少，就老是叫你翻 Sample 。</p>
<p>別的不說，光是複製到剪貼簿，就是上 StackOverflow 找到<a href="http://stackoverflow.com/questions/3436102/copy-to-clipboard-in-chrome-extension">答案</a>的（非常怪的招數），這個功能我以為會是 Chrome API 要提供的… Orz</p>
<p>功能大致寫完了以後，要上架時又碰到阻礙。</p>
<p>首先是 Chrome Web Store 要求上架時要有 1280x800 <strong>或</strong> 640x400 解析度的螢幕截圖。</p>
<ol>
<li>我想說 Chrome Web Store 的圖片這麼小，我隨便切個小圖就行了。</li>
<li>第一次放，<strong>不滿 640x400</strong> ，叫我再傳一次。</li>
<li>第二次放，我切 643x404 ，上傳後跟我說「尺寸不符」，所以是一定要<strong>完全一致</strong>才行…</li>
<li>第三次放，我切得剛好 640x400 ，上傳後卻跟我說「<strong>請上傳 1280x800 的解析度</strong>」</li>
<li>第四次放，就只好先用別的工具把 Chrome 縮到 1280x800 ，抓圖，然後用 Preview.app 小心切…</li>
<li>而且上架之後才發現他<strong>還是縮到 640x400</strong> 了，那你叫人家放大圖放身體健康的？</li>
</ol>
<p>此外還要放一張宣傳圖，是 440x280 ，為了這個只好再切一次。不過這好像是要放在 Web Store 的 index ，哪天這個 app 被擠到上面就需要了<strike>（會有這一天嗎？）</strike>。</p>
<p>最後要付 US$5.00 的一次性註冊費，這麼便宜當然不介意了。不過我竟然看到這個畫面：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2012/2012-03-02-copy-as-markdown/Screen%20Shot%202012-03-02%20at%2001.48.09.png" alt="" /></p>
<p>五樓你評評理啊！當然我在姓名之間加入一個空格就通過了 Orz</p>
<p>不過這次還真的學到了怎麼做 Chrome Extension ，以前還想說要學怎麼做 Firefox Extension ，但最近兩年已經沒在用 Firefox 了...。</p>

            ]]></description>
        </item>
        <item>
            <title>再見，TaiwanMoney</title>
            <link>https://blog.yorkxin.org/posts/taiwanmoney-bye/</link>
            <pubDate>Tue, 16 Aug 2011 16:00:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2011/08/17/taiwanmoney-bye.html</guid>
            
            <description><![CDATA[
                <p>TaiwanMoney 終於還是走入歷史了。這是南部七縣市 RFID 交通票證的一個里程碑（？）。</p>
<p>2006 年我基於嘗鮮的心態去買了一張 <a href="http://www.taiwanmoney.com.tw" target="_blank">TaiwanMoney</a> ，那時候覺得台北有悠遊卡很酷。所以我的 blog 有一些 <a href="/tag/taiwanmoney/">TaiwanMoney 的文章</a>。</p>
<p>以我以前居住的台南市區而言，公車族使用 TaiwanMoney 的還是少之又少，學生通勤族大多數都是買紙印的月票，我就用過興南客運和高雄客運的。至於大學生和上班族，大部份也都騎機車，在台南會像我這樣搭公車四處跑的人真的是異類。高中職學生大部份都會在 18 歲的時候去考駕照<del datetime="2011-08-17T07:35:13+00:00">當成年禮</del>，我自己也在 20 歲考到駕照後，幾乎沒上過公車，假期回台南都是騎機車。即使是在大眾運輸最發達的（舊）高雄市，在高雄捷運通車後，TaiwanMoney 因為通訊協定跟一卡通不同，出入都得走公務門，也讓 TaiwanMoney 的發展更加受阻。</p>
<p>現在這一刻終於還是來了，TaiwanMoney <a href="http://www.taiwanmoney.com.tw/news/20110504.htm" target="_blank">在 6/9 終止業務</a>，交易到 8/31 為止，現金贖回也是。我就去贖回了，然而這其中有很波折的故事。</p>
<p><a title="Flickr 上 chitsaou 的 最後的 TaiwanMoney 交易" href="http://www.flickr.com/photos/chitsaou/6051847575/"><img src="https://farm7.static.flickr.com/6198/6051847575_5f98528664.jpg" alt="最後的 TaiwanMoney 交易" width="500" height="281" /></a></p>
<span id="continue-reading"></span>
<p>我收到這則訊息是 6 月中，是在 PTT 看到的。原本計畫七月中旬回台南做兵役專科體檢的時候一併去玉山銀行辦贖回，不過櫃員跟我說要等幾個工作天才能拿到現金（具體數字忘了），因為要跟台北總行做什麼會計手續；或是轉帳到我在其他銀行的帳戶，扣手續費。我那時候不知道腦子在想什麼，就問說既然要跟台北總行確認，那我能不能去台北辦贖回？他說可以，我也就走了。事實上我現在想想，更不懂為什麼我那時候不跑去興南客運在台南火車站附近的營運處處理就好了。</p>
<p>總之我還是把這張卡帶到了台北，去住所附近的玉山銀行處理。一進門，服務人員以為我是要剪信用卡（看起來很像），但我說不是，總之他帶我到某個櫃台前面。我想台北的櫃員應該也不知道，一坐下來就把來龍去脈說了一遍，櫃員一開始也不知道是什麼卡，問了襄理，還打了電話求救，過程中還問了身份證字號（不過這卡是不記名的）。</p>
<p>大概過了 10 分鐘，就說因為該行沒有讀卡機，所以要我留卡，寄到有讀卡機的其他分行處理，並且問了我要匯到哪個帳戶。我說這卡是買斷的，我要回來領卡，櫃員打電話問了遙遠的同事，確定可以。我要求寫一張字據什麼的證明我有留卡，之後回來拿卡也方便，也就印了卡片的正反面。這張字據一式二份，我留一份。</p>
<p>不到三個工作天，就收到了玉山銀行的電話，說已經匯款了，刷簿子發現沒有扣手續費（科科）。隔天又打電話來說可以去原本那間分行拿卡，今天就去拿了。</p>
<p>這張卡片我留作紀念，它見證了非接觸式交通票證在南部地區的實驗，以後要在南部推這種服務的單位，應該要參考他們為什麼失敗的經驗。我也不想在這裡說太多事後諸葛的話，留待其他對大眾運輸比較在行的人來評論吧。</p>

            ]]></description>
        </item>
        <item>
            <title>Mac OS X 的 Launch Daemon &#x2F; Agent</title>
            <link>https://blog.yorkxin.org/posts/osx-launch-daemon-agent/</link>
            <pubDate>Wed, 03 Aug 2011 16:00:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2011/08/04/osx-launch-daemon-agent.html</guid>
            
            <description><![CDATA[
                <p>因為想做某個應用，今天研讀了 Apple Developer 網站上的 <a title="Daemons and Services Programming Guide - Apple Developer" href="http://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPSystemStartup/" target="_blank">Daemons and Services Programming Guide</a> ，終於懂了 Mac OS X 的 Launch Daemon / Agent 是做什麼用的，筆記一下。為了避免專有名詞翻譯不同造成誤解，我試著統統不翻譯。不過我對 Mac OS X 的 system programming 涉世（？）未深，要是有解釋不對的地方，路過的大俠請不吝指教。</p>
<p>以下的操作全是在 Mac OS X 10.6.8 完成的。</p>
<hr />
<h1>What is launchd ?</h1>
Mac OS X 從 10.4 開始，採用 <code>launchd</code> 來管理整個作業系統的 services 及 processes 。傳統的 UNIX 會使用 <code>/etc/rc.*</code> 或其他的機制來管理開機時要啟動的 startup services ，而現在的 Mac OS X 使用 <code>launchd</code> 來管理，它的 startup service 叫做 <strong>Launch Daemon</strong> 和 <strong>Launch Agents</strong> 。而視為 service 的程式，就該是 background process ，不應該提供 GUI ，也不應該跳到 （console 的）foreground 。當然有些例外，例如聽快速鍵之後跳出視窗的程式。
<span id="continue-reading"></span>
<p><code>launchd</code> 管理的 background process 有四種：</p>
<ol>
	<li><strong>Launch Daemon</strong>: 在開機時載入 (load) 。</li>
	<li><strong>Launch Agent</strong>: 在使用者登入時載入。</li>
	<li><strong>XPC Service</strong>: 好像是 10.7 才有的，我還沒灌 10.7 ，先跳過。</li>
	<li><strong>Login Items</strong>: 在 User 登入時執行。有兩種方法可以用程式新增項目到 Login Item：</li>
<ol>
	<li>Shared File List：會出現在 Account 偏好設定的 Login Item 清單。</li>
	<li>Service Management Framework：這個就不會出現在 Login Item 清單。</li>
</ol>
</ol>
（以下把重點放在 Launch Daemon / Agent 。至於 XPC 和 Login Item 就留待其他比較在行的大大來解釋。）
<h1>Launch Daemon &amp; Launch Agent</h1>
Launch Daemon 和 Launch Agent 是同一種東西在不同 scopes 的異名。Launch Daemon 是 system-wide 的 service ，稱為 daemon，Launch Agent 是 per-user 的 service ，稱為 agent，前者在開機時會載入 （load） ，後者在使用者登入時（才）會載入。
<p>如果你打開 Activity Monitor ，並切換到 Hierarchy view ，你會發現有個 <code>launchd</code> 會在最上層，跟它同層的只有 <code>kernel_task</code> ，它下面有很多 child processes 的 user 都是 <code>root</code> ，其中還有一個 <code>launchd</code> ，啟動的 user 是你自己，它底下的 child processes 的 user 也幾乎都是你自己。當這些 processes 是由 <code>launchd</code> 載入 <strong><code>launchd</code> property list file</strong> 來執行的時候，前者由 <code>root</code> 執行的稱為 Launch Daemons ，後者由使用者執行的稱為 Launch Agents 。</p>
<p><strong><code>launchd</code> property list file</strong> 就是你會在 LaunchDaemon 或 LaunchAgents 目錄中看到的 <code>*.plist</code> 檔案（以下統稱 plist 檔，反正本文講到的 plist 檔也只有這種用途）。它是 XML 格式，不過咱們別這麼糾結手刻 XML ，你直接按兩下打開就是 Property List Editor ，滑鼠點一點就好，不糾結。</p>
<h2>launchd Service Process Lifecycle</h2>
由 launchd 所管理的 services （Launch Daemon 、 Launch Agent）是要先由 launchd 載入（load）以後才會執行（run），但載入之後並不一定馬上執行。在蘋果的官方文件說明了 kernel 載入完成後會發生的事，用來說明 Launch Daemons 、Launch Agents 及其 processes 的生命週期。
<p>開機時，會先<a href="http://developer.apple.com/library/mac/documentation/Darwin/Conceptual/KernelProgramming/booting/booting.html" target="_blank">載入 OS Kernel</a> ，載入完成後就執行 <code>launchd</code> ，用來載入 system-wide services （daemons）。這個 system-wide <code>launchd</code> 在開機時會做這些事：</p>
<ol>
	<li><strong>載入 (load)</strong> 存放在這些目錄下的 <code>plist</code> ：</li>
<ul>
	<li><code>/System/Library/LaunchDaemons</code></li>
	<li><code>/Library/LaunchDaemons</code></li>
</ul>
	<li><strong>註冊</strong>那些 <code>plist</code> 裡面設定的 sockets (port) 和 file descriptors</li>
	<li><strong>執行 (run)</strong> <code>KeepAlive = true</code> 的 daemons ，當然 <code>RunAtLoad = true</code> 的也會啟動。</li>
</ol>
該 run 的 run 好後， <code>loginwindow</code> 就出現了，提示使用者登入。有設定自動登入的話，就會跳過這關。
<p>在使用者登入以後，會執行屬於該使用者的 <code>launchd</code> ，負責處理 Launch Agent ，做的事跟上面載入 Launch Daemon 很像，差別在於它從以下的目錄載入 <code>plist</code>：</p>
<ul>
	<li><code>/System/Library/LaunchAgents</code></li>
	<li><code>/Library/LaunchAgents</code></li>
	<li><code>~/Library/LaunchAgents</code></li>
</ul>
由使用者執行的任何程式也都是 <code>launchd</code> 來執行的，所以 <code>launchd</code> 也是該使用者的所有 processes 之母。
<p>在使用者登出、關機或重新開機時，會觸發 <strong>Termination event</strong>。接受登出、關機、重新開機使用者指令的 process 是 <code>loginwindow</code> 。它會先向使用者確認，一但確認，就會對每個由該使用者的 <code>launchd</code> 所啟動的 processes 送出 termination signal，如果是 Cocoa 則送出 Cocoa API 的 event，其他的就送出 <code><a href="http://en.wikipedia.org/wiki/SIGTERM" target="_blank">SIGTERM</a></code> 要他們自我了斷，45 秒之後，除了 Cocoa 的應用程式可以丟出某個 error 來取消這整個 termination process，其他還沒結束的都會被 kill 掉。</p>
<p>這就是為什麼 <code>loginwindow</code> 這個 process 會一直存在，它要負責把該使用者執行起來的 processes 統統清掉。而 per-user services 都關掉以後，就回到 <code>loginwindow</code> ，或是執行關機、重新開機的流程，後兩者就是照著差不多的流程去關掉所有 system-wide services 。</p>
<hr />
<h1><code>launchd</code>-compatible Daemon Programming Guide</h1>
以下是該文件中提及關於配合 <code>launchd</code> 開發 daemon 時應注意的事，提到關於 <code>plist</code> 的 key 就請參考 <a href="http://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man5/launchd.plist.5.html" target="_blank"><code>man 5 launchd.plist</code></a> 。以下的 daemon 指的是 Launch Daemon 所要運行的 process ，所以 Launch Agent 也一併適用。
<h2>Listen to <code>SIGTERM</code></h2>
如上文所提及的，由於 <code>loginwindow</code> 這個 process 在要關掉你的 daemon 時會先送 <code><a href="http://en.wikipedia.org/wiki/SIGTERM" target="_blank">SIGTERM</a></code> ，要你自我了斷，等太久沒關掉才會 <code>SIGKILL</code> 。如果你的程式需要在結束之前做什麼事，一定要聽 <code>SIGTERM</code> 這個 signal 。
<h2>On-Demand Daemon</h2>
Launch Daemon / Agent 預設不會讓某個 process 一直執行，當它的設定沒有 <code>KeepAlive = true</code> 時，它會根據被執行的 process 的 CPU usage 和 requests （如 TCP/IP service）來決定要不要送出 <code>SIGTERM</code> 叫他自盡。
<p>當該 service 需要被使用時，而相對應的 program 沒有跑成 process 時，會自動把該 service 給跑起來。例如某個 TCP/IP service 聽某個 port，當這個 port 有封包進來時， <code>launchd</code> 會把相對應的 service 給啟動，這種行為叫做 <strong>on-demand</strong> 。</p>
<p>當然，也有 non-on-demand daemon （好繞舌），其實也就是 <strong>keep-alive daemon</strong> ，這也是傳統意義上的 daemon ，我是說那種一直躲在牆角默默執行，直到有人找他，他才跳出來回一下話，回完了以後又繼續躲在牆角的那種。只要把 <code>KeepAlive</code> 這個 key 設成 <code>true</code> ，它就會在 <code>plist</code> 被 <code>launchd</code> 載入 (load) 時執行 (run) 起來。要是那個 process 死掉，<code>launchd</code> 會知道，馬上再把它開起來。所以如果你試著去 Activity Monitor 砍掉這種 daemon ，它就馬上會復活。</p>
<h2>No <code>fork</code> or <code>exec</code></h2>
傳統的 system programming 會教你用 <code><a href="http://en.wikipedia.org/wiki/Exec_(operating_system)" target="_blank">exec</a></code> 、<code><a href="http://en.wikipedia.org/wiki/Fork_(operating_system)" target="_blank">fork</a></code> 等等的 POSIX API 來做一隻 daemon ，但配合 <code>launchd</code> 時，由於 daemon 的生命週期是由 <code>launchd</code> 來控制的，除非強制要求 Kepp-Alive，否則要生要死是 <code>launchd</code> 決定，更何況 Keep-Alive 還要考慮 daemon process 在結束以後自動重新執行，所以在配合 <code>launchd</code> 寫 daemon 時，蘋果建議你不要用傳統的 <code>fork</code> 和 <code>exec*</code> 。當然， <code>plist</code> 檔案中的 <code>ProgramArguments</code> 就是 <code>exec*</code> 系列 subroutine 的參數。
<p>當一個 process 跑起來 10 秒內就死掉， launchd 會判定為 crash ，然後試著重新執行。要是你用傳統的 <a href="http://en.wikipedia.org/wiki/Fork-exec" target="_blank">fork-exec</a> style ，就可能會造成無限迴圈。</p>
<h2>No <code>setuid</code> / <code>setgid</code> / <code>chroot</code> / <code>chdir</code> etc.</h2>
為了安全性的考量，蘋果強烈建議你不要自己呼叫 <code>setupd</code>, <code>setgid</code>, <code>chroot</code>, <code>chdir</code> 等等 system subroutines ，而是透過 <code>plist</code> 檔的設定值來讓 <code>launchd</code> 幫你完成，參考 <code>UserName</code> 、<code>GroupName</code> 、<code>RootDirectory</code> 、 <code>WorkingDirectory</code> 的 keys 。
<h2>No pipe redirection hell for <code>fd</code> <code>0</code>, <code>1</code> or <code>2</code></h2>
在寫 log 或輸出訊息時也不用煩惱開檔等等問題，你可以設定 <code>StandardOutPath</code> 、 <code>StandardErrorPath</code> ，只管輸出到 <code>stdout</code> 或 <code>stderr</code> 就好了。而 <code>StandardInPath</code> 也可以讓你的 process 一執行就從 <code>stdin</code> 吃指定 path 的內容。也就是說， <code>launchd</code> 幫你把 <code>fd</code> = <code>0</code>, <code>1</code>, <code>2</code> 的東西都傳便便。
<hr />
<h1>其他應用</h1>
<h2>工作排程</h2>
Launch Daemon / Agent 的設定檔可以指定該 service 的執行週期及執行時間，也就是說，它可以替代傳統的 <code>at</code>, <code>periodic</code> 和 <code>cron</code> 。這些設定值的 key 請參考 <code>StartInterval</code> 和 <code>StartCalendarInterval</code>。
<p>搭配 <code>LaunchOnlyOnce</code> 的話可以模擬 <code>at</code> ，但對我來說，如果要用 <code>launchd</code> 只臨時做一件事，還不如直接 <code>at</code> 方便。</p>
<h2>監視檔案或目錄異動</h2>
Launch Daemon / Agent 可以監視某個 path 的異動，設定在 <code>WatchPaths</code> 這個 key。這裡所說的 path 可以是 directory 或是某個特定的檔案，只要該 path 有異動，就會執行你的 job 。
<p>也可以用來清 queue ，只要 directory 裡面有東西，就會執行 job 直到空為止，可以用來做 mail server 或 notification 。設定在 <code>QueueDirectories</code> 這個 key 。</p>
<hr />
<p>See also:</p>
<ul>
	<li><a href="http://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man5/launchd.plist.5.html" target="_blank">man 5 launchd.plist</a></li>
	<li><a href="http://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/launchctl.1.html" target="_blank">man 1 launchctl</a> ，也就是管理 Launch Daemons / Agents 的工具</li>
	<li><a href="http://developer.apple.com/library/mac/technotes/tn2083/" target="_blank">Technical Note TN2083: Daemons and Agents</a></li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>Git-rebase 小筆記</title>
            <link>https://blog.yorkxin.org/posts/git-rebase/</link>
            <pubDate>Thu, 28 Jul 2011 16:00:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2011/07/29/git-rebase.html</guid>
            
            <description><![CDATA[
                <p>最近剛好有個機會整理很亂的 Git commit tree，終於搞懂了 rebase 的用法，筆記一下。</p>
<p>大家都知道 Git 有個特色就是 branch 開很大開不用錢，但很多 branches 各自開發，總要在適當時機 merge 進去 master 。看過很多 git 操作指南都告訴我們，可以妥善利用 rebase 來整理看似很亂或是中途可能不小心手滑 commit 錯的 commits ，甚至可以讓 merge 產生的線看起來比較簡單，不會有跨好幾十個 commits 的線。</p>
<h2 id="rebase-de-yi-yi-zhong-xin-ding-yi-can-kao-ji-zhun">rebase 的意義：重新定義參考基準</h2>
<p>首先要提一下 rebase 的意思，我擅自的直譯是「重新 <em>(re-)</em> 定義某個 branch 的參考基準 <em>(base)</em>」。把這個意思先記起來，比較容易理解 rebase 的運作原理。就好比移花接木那樣（稼接），把某個樹枝接到別的樹枝。</p>
<p>在 git 中，每一個 commit 都可以長出 branch ，而 branch 的 base 就是它生長出來的 commit ，rebase 也就是把該 branch 所長出來的 commit 給改去另一個 commit 。不過，因為 rebase 會調整 commit 的先後關係，弄不好的話可能會把你正在操作的 branch 給搞爛，所以在做 rebase 之前，最好開一個 backup branch ，什麼時候出差錯的話，reset 回 backup 就行了。</p>
<p>以下用實際的例子來操作比較容易解釋。看 log 的程式是 <a href="http://gitx.laullon.com">GitX (L)</a> 。</p>
<p><strong>Update</strong> 2012/06/28：也可以看 <a href="http://ihower.tw/blog/archives/6704/">ihower 的錄影示範</a> ，實際操作會比讀文字來得容易懂。</p>
<span id="continue-reading"></span>
<p>例如我要寫個網頁，列出課堂上的學生。我把樣式的設計 (<code>style</code>) 跟主幹 (<code>master</code>) 分開，檔案有 <code>index.html</code> 和 <code>style.css</code> 。</p>
<p>到目前為止有以下的 commit history：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2011/2011-07-29-git-rebase/screen-shot-2011-07-25-at-00-58-36.png" alt="" /></p>
<p><code>style</code> 完成了一小部份，而接下來要修飾的頁面是 <code>master</code> 裡面有改過的，如何讓 <code>style</code> 可以繼承 <code>master</code> 呢？就是用 rebase 把 <code>style</code> branch 給接到 <code>master</code> 後面了，因為 rebase 是「重新定義基準點」。就像是在稼接時，把新枝的根給「接」在末梢上。</p>
<p>rebase 的基本指令是 <code>git rebase &lt;new base-commit&gt;</code> ，意思是說，把目前 checkout 出來的 branch 分支處改到新的 commit。而 commit 可以使用 branch 去指（被指中的 commit 就是該 branch 的 <strong>HEAD</strong>），所以現在要把 <code>style</code> 這個 branch 接到 <code>master</code> 的 <strong>HEAD</strong> （<code>dc39a81e</code>），就是在 <code>style</code> 這個 branch 執行</p>
<pre><code>git rebase master
</code></pre>
<p>完成之後，圖變這樣：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2011/2011-07-29-git-rebase/screen-shot-2011-07-25-at-01-00-38.png" alt="" /></p>
<p>果然順利接起來了。</p>
<p>而在執行的過程中會看到：</p>
<pre><code>First, rewinding head to replay your work on top of it...
Applying: set body&#x27;s font to helvetica
Applying: adjust page width and alignment
</code></pre>
<p>這是它的操作方式，照字面上的意思，就是它會嘗試把當前 branch 的 <strong>HEAD</strong> 給指到你指定的 commit （在這裡是原本 <code>master</code> 的 <strong>HEAD</strong>，也就是 <code>dc39a81e</code>），然後把每個原本在 <code>style</code> 上面的 commits （<code>d242d00c..0b373e34</code>） 給<strong>重新 commit</strong> 進去 <code>style</code> 這個 branch (re-apply commits)。也由於是「<strong>重新 commit</strong>」，所以 rebase 以後的 commit ID (SHA) 都不一樣。</p>
<p>那如果過程中有 conflict 呢？後文會提到。</p>
<h2 id="fast-forwarding-ke-yi-de-hua-zhi-jie-gai-zhi-biao-bu-zhong-xin-commit">fast-forwarding: 可以的話，直接改指標，不重新 commit</h2>
<p>接著再開個新的 branch 叫 <code>list</code> ，專門改學生清單，同時<del>另一個人</del>也在改 <code>style</code> 這個 branch ，修飾網頁的整體裝飾。改啊改，變成這樣分叉的兩條線：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2011/2011-07-29-git-rebase/screen-shot-2011-07-25-at-01-14-55.png" alt="image" />]</p>
<p><code>list</code> 改到一個段落，沒有問題了，就想 merge 進 <code>master</code> 。在 <code>master</code> branch 做</p>
<pre><code>git merge list
</code></pre>
<p>這時 git 發現，剛好 <code>master</code> 直接指到 <code>list</code> 的 <strong>HEAD</strong> commit 也行 ，所以 git 直接就改了 <code>master</code> 的 commit ID ，也就是所謂的 <strong>fast-forward</strong>，熟悉 C 語言的同學應該對這種指標移動不陌生。完成之後就是這樣：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2011/2011-07-29-git-rebase/screen-shot-2011-07-25-at-01-16-30.png" alt="image" />]</p>
<h2 id="rebase-onto-zhi-ding-yao-cong-na-li-kai-shi-jie-zhi"><code>rebase --onto</code>: 指定要從哪裡開始接枝</h2>
<p><code>list</code> 繼續改，<code>style</code> 還是繼續改，變這樣：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2011/2011-07-29-git-rebase/screen-shot-2011-07-25-at-01-26-05.png" alt="image" />]</p>
<p>現在 <code>style</code> 要開始裝飾學生清單了，而學生清單是 <code>list</code> 這個 branch 在改的。於是 <code>style</code> 應該要 rebase 到 <code>list</code> ，可是這時管 <code>list</code> 的說，我後面幾個 commits 還沒敲定，你先拿 <code>64a00b7e (add their ages)</code> 這個 commit 當基準，這我改好了。所以這時候，應該要把 <code>style</code> 這個 branch 接到 <code>64a00b7e</code> 的後面。</p>
<p>該怎麼辦呢？這時就要用 <code>git rebase --onto</code> 了。指令是</p>
<pre><code>git rebase --onto &lt;new base-commit&gt; &lt;current base-commit&gt;
</code></pre>
<p>意思是說，把當前 checkout 出來的 branch 從 <code>&lt;current base-commit&gt;</code> 移到 <code>&lt;new base-commit&gt;</code> 上面 ，就像是在稼接時，把新枝的根給「種」在某個點上，而不是接在末梢。（這似乎也是稼接最常用的方式？有請懂園藝同學的指教一下）</p>
<p>再看一下 commit history：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2011/2011-07-29-git-rebase/screen-shot-2011-07-25-at-01-26-05.png" alt="image" />]</p>
<p>現在 <code>style</code> 是 based on <code>dc39a81e (add some students)</code>，要改成 based on <code>64a00b7e (add their ages)</code>，也就是</p>
<ul>
<li><code>&lt;current base-commit&gt;</code> = <code>dc39a81e</code></li>
<li><code>&lt;new base-commit&gt;</code> = <code>64a00b7e</code></li>
</ul>
<p>那就來試試看</p>
<pre><code>git rebase --onto 64a00b7e dc39a81e
</code></pre>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2011/2011-07-29-git-rebase/screen-shot-2011-07-25-at-01-36-05.png" alt="image" />]</p>
<p>果然達到了目的，<code>style</code> 現在是 based on <code>64a00b7e</code> 了（當然 commit IDs 也都不同了）。</p>
<h2 id="conflict-de-chu-li">conflict 的處理</h2>
<p>接著改 <code>style</code> 的人修改了學生清單的樣式，可是他很機車，他要改 <code>index.html</code> 裡面的東西（實際情況是，<code>list</code> 裡寫了一個 <code>table</code>，但寫 css 總要有些 <code>class</code> 或 <code>id</code> 的 attributes 才能設定）。剛好改 <code>list</code> 的人也在他自己的 branch 裡面改，這時候，在 rebase 試著 re-apply commits 的過程中，必定會產生 conflict。</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2011/2011-07-29-git-rebase/screen-shot-2011-07-25-at-02-12-081.png" alt="image" />]</p>
<p>現在 <code>list</code> 要利用到 <code>style</code> 裡面修飾好的樣式，在這個情況下，就是把 <code>list</code> 給 rebase 到 <code>style</code> 上面，也就是在 <code>list</code> branch 做 <code>git rebase style</code> 。不過你會看到這個：</p>
<pre><code>First, rewinding head to replay your work on top of it...
Applying: add gender column
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Failed to merge in the changes.
Patch failed at 0001 add gender column

When you have resolved this problem run &quot;git rebase --continue&quot;.
If you would prefer to skip this patch, instead run &quot;git rebase --skip&quot;.
To restore the original branch and stop rebasing run &quot;git rebase --abort&quot;.
</code></pre>
<p>跟預期的一樣出現了 conflict。當然，它會先試著自動 merge ，但如果改到的行有衝突，那就得要手動 merge 了，打開他說有衝突的檔案，改成正確的內容，接著使用 <code>git add &lt;file&gt;</code> （要把該檔案加進去 staging area，處理 rebase 的程式才能 commit），再 <code>git rebase --continue</code> 。</p>
<p>完成以後就會像這樣：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2011/2011-07-29-git-rebase/screen-shot-2011-07-25-at-02-22-47.png" alt="image" />]</p>
<h2 id="interactive-mode-tou-tian-huan-ri-zi-ding-zhong-xin-commit-de-xiang-xi-bu-zou">Interactive Mode: <del>偷天換日</del>，自定重新 commit 的詳細步驟</h2>
<p>接著 <code>style</code> 和 <code>list</code> 又陸續改了一些東西，主要是 <code>list</code> 裡面加了表單元件，而 <code>style</code> 則繼續修飾網頁整體設計。到了一個段落，該輪到 <code>style</code> 修飾 <code>list</code> 的表單了。目前的 commit history 長這樣：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2011/2011-07-29-git-rebase/screen-shot-2011-07-29-at-17-53-44.png" alt="image" />]</p>
<p>不過在 <code>style</code> 要 rebase 到 <code>list</code> 上面之前，管 <code>list</code> 的人想把 <code>list</code> 上面的一些 commits 給整理過，因為他發現有這些問題：</p>
<ul>
<li><code>"wrap the form with div"</code> 太後面了，想移到前面</li>
<li><code>"fix typo of age field name"</code> 跟 <code>"add student id and age..."</code> 可以合併</li>
<li><code>"add student id and age ..."</code> 裡面東西太多，該拆成兩個</li>
<li><code>"form to add more *studetns*"</code> 這 message 有錯字 "studetns"</li>
<li><code>"add gender select box"</code> 裡面的程式碼有打錯字（囧</li>
</ul>
<p>上面提到了 rebase 運作的方式是重新 commit 過一遍，那這個「重新 commit」的過程，能不能讓程式設計師來干預，達到<del>偷天換日</del>修改 commit 的目的呢？當然可以，只要利用 rebase 的 <strong>Interactive Mode</strong>。Git 的靈活就在這裡，連 commit 的內容都可以改。</p>
<p>如何啟動 interactive mode 呢？只要加入 <code>-i</code> 的參數就行了。以這個例子來說，<code>list</code> branch 是 based on <code>0580eab8 (fill in gender column)</code> ，要從這個 commit 後面重新 apply 一次 commits ，也就是：</p>
<pre><code>git rebase -i 0580eab8
</code></pre>
<p>接著會以你的預設編輯器打開一個檔案叫做 <code>.git/rebase-merge/git-rebase-todo</code> ，裡面已經有一些 git 幫你預設好的內容了，其實就是原本 commits 的清單，你可以修改它，告訴 git 你想怎麼改：</p>
<pre><code># git rebase -i
pick 2c97b26 form to add more studetns
pick fd19f8e add student id and age field into the form
pick 02849bf fix typo of age field name
pick bd73d4d wrap the form with div
pick 74d8a3d add gender select box

# Rebase 0580eab..74d8a3d onto 0580eab
# ...[chunked]
</code></pre>
<p>第一個欄位就是操作指令，指令的解釋在該檔案下方有：</p>
<ul>
<li><code>pick</code> = 要這條 commit ，什麼都不改</li>
<li><code>reword</code> = 要這條 commit ，但要改 commit message</li>
<li><code>edit</code> = 要這條 commit，但要改 commit 的內容</li>
<li><code>squash</code> = 要這條 commit，但要跟前面那條合併，並保留這條的 messages</li>
<li><code>fixup</code> = squash + 只使用前面那條 commit 的 message ，捨棄這條 message</li>
<li><code>exec</code> = 執行一條指令（但我沒用過）</li>
</ul>
<p>此外還可以調整 commits 的順序，直接剪剪貼貼，改行的順序就行了。</p>
<h3 id="diao-zheng-commit-shun-xu-xiu-gai-commit-message">調整 commit 順序、修改 commit message</h3>
<p>首先我想要把 <code>"wrap the form with div"</code> 移到 <code>"form to add more studetns"</code> 後面，然後 <code>"form to add more studetns"</code> 要改 commit message （有 typo），那就改成這樣：</p>
<pre><code># git rebase -i
reword 2c97b26 form to add more studetns
pick bd73d4d wrap the form with div
pick fd19f8e add student id and age field into the form
pick 02849bf fix typo of age field name
pick 74d8a3d add gender select box
</code></pre>
<p>接著儲存檔案後把檔案關掉（如 vim 的 <code>:wq</code>），就開始執行 rebase 啦，遇到 <code>reword</code>  時會再跳出編輯器，讓你重新輸入 commit message 。這時我把 <code>studetns</code> 改正為 <code>students</code> ，然後就跟平常 commit 一樣，存檔並關掉檔案。</p>
<pre><code># git commit
form to add more students

# Please enter the commit message for your changes. Lines starting
# ...[chunked]
</code></pre>
<p>完成後會看到：</p>
<pre><code>Successfully rebased and updated refs&#x2F;heads&#x2F;list.
</code></pre>
<p>再看 commit history ，的確達到了目的，而且 <code>list</code> 這個 branch 一樣還是 based on <code>0580eab8</code> ，後面那些剛剛 rebase 過的 commits 統統換了 commit ID ：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2011/2011-07-29-git-rebase/screen-shot-2011-07-29-at-18-21-28.png" alt="image" />]</p>
<h3 id="he-bing-commits">合併 commits</h3>
<p>剩下這些要做：</p>
<ul>
<li><code>"fix typo of age field name"</code> 跟 <code>"add student id and age..."</code> 可以合併</li>
<li><code>"add student id and age ..."</code> 裡面東西太多，該拆成兩個</li>
<li><code>"add gender select box"</code> 裡面的程式碼有打錯字</li>
</ul>
<p>現在來試試看合併，一樣是 <code>git rebase -i 0580eab8</code> ，並使用 <code>fixup</code> 來把 commit 給合併到上一個（如果用 <code>squash</code> 的話，會讓你修改 commit message ，修改時會把多個要連續合併的 commit messages 放在同一個編輯器裡）：</p>
<pre><code># git rebase -i
pick c3cff8a form to add more students
pick 7e128b4 wrap the form with div
pick 0d450ea add student id and age field into the form
fixup 8f5899e fix typo of age field name
pick e323dbc add gender select bo
</code></pre>
<p>完成後再看 commit history ，的確合併了：</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2011/2011-07-29-git-rebase/screen-shot-2011-07-29-at-18-24-50.png" alt="image" />]</p>
<h3 id="xiu-gai-chai-san-commit-nei">修改、拆散 commit 內</h3>
<p>剩下了拆 commit 和訂正 commit 內容。現在先來做訂正 commit ，這個學會了就知道怎麼拆 commit 了。</p>
<p>在這裡下 <code>edit</code> 指令來編輯 commit 內容：</p>
<pre><code># git rebase -i
pick c3cff8a form to add more students
pick 7e128b4 wrap the form with div
pick 53616de add student id and age field into the form
edit c5b9ad8 add gender select box
</code></pre>
<p>存檔並關閉之後，現在的狀態是停在剛 commit 完 <code>"add gender select box"</code> 的時候，所以現在可以偷改你要改的東西，存檔以後把改的檔案用 git add 加進 staging area ，再打</p>
<pre><code>git rebase --continue
</code></pre>
<p>來繼續，這時候因為 staging area 裡面有東西，git 會將它們與 <code>"add gender select box"</code> 透過 <code>commit --amend</code> 一起重新 commit 。</p>
<p>最後是拆 commit 。怎麼拆呢？剛剛做了 <code>edit</code> ，不是停在該 commit 之後嗎？這時候就可以偷偷 reset 到 <code>HEAD^</code> （即目前 HEAD 的<strong>前一個</strong>），等於是退回到 HEAD 指到的 commit 的前一個，於是該 commit 的 changes 就被倒出來了，變成 <em>changed but not staged for commit</em> ，再根據你的需求，把 changes 給一個一個 commit 就行了。</p>
<p>實際的操作如下。首先是用 <code>edit</code> 指令來編輯 commit 內容：</p>
<pre><code># git rebase -i
pick c3cff8a form to add more students
pick 7e128b4 wrap the form with div
edit 53616de add student id and age field into the form
pick 4dbcf49 add gender select box
</code></pre>
<p>接著使用</p>
<pre><code>git reset HEAD^
</code></pre>
<p>來把目前的 HEAD 指標給指到 HEAD 的前一個，指完之後，原本 HEAD commit 的內容就被倒出來，並且也不存在 stage area 裡面， git 會提示有哪些檔案現在處於 changed but not staged for commit：</p>
<pre><code>Unstaged changes after reset:
M	index.html
</code></pre>
<p>現在我可以一個一個 commit 了，原本是 <code>add student id and age field</code> ，我想拆成一次加 student id field ，一次加 age field 。commit 完成以後，再打</p>
<pre><code>git rebase --continue
</code></pre>
<p>這次因為 staging area 裡面沒東西，所以就繼續 re-apply 剩下的 commits 。</p>
<p>現在打開 log 看，拆成兩個啦！</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2011/2011-07-29-git-rebase/screen-shot-2011-07-29-at-20-18-30.png" alt="image" />]</p>
<p>掌管 <code>list</code> branch 的人折騰完了，便告訴管 <code>style</code> 的說，可以 rebase 了，<del>git 再度拯救了苦難程序員的一天</del>。</p>
<p><img src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2011/2011-07-29-git-rebase/screen-shot-2011-07-29-at-21-04-34.png" alt="image" />]</p>
<hr />
<p>更多 rebase ：</p>
<ul>
<li><a href="http://ihower.tw/blog/archives/2622">Git 版本控制系統(3) 還沒 push 前可以做的事</a> by ihower</li>
<li><a href="http://www.slideshare.net/littlebtc/git-5528339">寫給大家的 Git 教學</a> by littlebtc</li>
</ul>

            ]]></description>
        </item>
        <item>
            <title>MIDI 與我──時代的眼淚</title>
            <link>https://blog.yorkxin.org/posts/midi-and-me-toki-no-namida/</link>
            <pubDate>Thu, 10 Mar 2011 16:00:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2011/03/11/midi-and-me-toki-no-namida.html</guid>
            
            <description><![CDATA[
                <p>實在是突然想懷舊一下，真是<a href="http://wiki.komica.org/wiki/?%E6%88%90%E5%8F%A5%2F%E6%99%82%E4%BB%A3%E7%9A%84%E7%9C%BC%E6%B7%9A" target="_blank">時代的眼淚</a>啊。</p>
<p>猶記得我國小三年級時（1997），第一次有電腦課，因為我們好歹是台南市的國小，所以也有個看起來有點像樣的電腦教室：大顆 CRT 螢幕、 Windows 95、Office （版本忘了）。那時候電腦課教的，不外乎是奇摩搜尋、畫賀卡、小畫家、錄音機等，我人生第一個電子郵件也是在這時候辦的（但早就沒在用了），還有一些林林總總的。不過有個東西我特別記得：MIDI。</p>
<p>Windows 95 、 98 都有內建一些 MIDI 檔，既然是國小的電腦課嘛，就會教大家怎麼播這些 MIDI 、去網路上找 MIDI 檔，最有名的就是「MIDI 音樂廳」，而且剛剛才發現，經過十來年的一直換網址，它竟然還活著，只是最新歌曲似乎都是十幾年前的事了。</p>
<span id="continue-reading"></span>
<p>當時我對於電腦可以播音樂，非常驚訝，每次電腦課我都花很多時間去搜集 MIDI，多半是當時流行的卡通、日劇主題曲，然後存到軟碟片帶回家。有些網站不給載，當時不懂網頁技術的我，只知道去 IE 暫存資料夾撈 .mid 檔出來<del>，可以說是我成為<del>宅</del>宅的濫觴</del>。（說到這裡，現在小朋友還知道甚麼是軟碟片嗎？）</p>
<p>後來到了國小六年級，第一次擁有 MP3 隨身聽（32MB），才第一次知道 MIDI 是不能放進 MP3 隨身聽的。結果呢？身為宅宅的我竟然不知道去哪裡找到「把 MIDI 變成 MP3」的工具，於是乎，一首首 MIDI 就被音源器重新演譯、進了我的 MP3 隨身聽。</p>
<p>只是有一天，我在重灌電腦的時候，忘了在事前把 My Documents 給解除鎖定，重灌後的 Windows 解不開舊的資料夾（註），於是就只好忍痛與上百首 MIDI 檔案說再見了，其中有不少我很愛的音樂，每天都要聽的那種。因此還哭了好幾天。</p>
<p>上了國中，就開始流行 MP3 了，這時候的我也不再拘泥於只有電子音源的 MIDI，但也就跟 MIDI 漸行漸遠了。直到現在。</p>
<p>為甚麼現在突然想起來呢？是因為昨天我在 Mac App Store 下載了 <a href="http://itunes.apple.com/tw/app/miditrail/id421739418?mt=12" target="_blank">MIDITrail</a>（<a href="http://sourceforge.jp/projects/miditrail/" target="_blank">Open Source</a>，也有 Windows 版），它可以把 MIDI 各軌的譜讀出來，並視覺化彩現在 3D 空間中，Camera 可以任意轉，播放到該音符時還會出現圈圈，如下圖：</p>
<p><a href="http://itunes.apple.com/tw/app/miditrail/id421739418?mt=12" target="_blank"><img class="alignnone size-full wp-image-1219" title="MIDITrail" src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2011/2011-03-11-midi-and-me-toki-no-namida/miditrail.png" alt="" width="640" height="494" /></a></p>
<p>然後就開始拿出我殘存（多數是那次慘案後憑印象重新下載的）的 MIDI 檔出來邊聽邊看，頗是新鮮，我控 MIDI 這麼久，第一次知道可以把 MIDI 視覺化（雖然似乎早就有人做出這這種應用了），也許是因為太久沒聽了吧，這兩天睡覺前都在把玩它、又去網路上找一些 MIDI 回來聽，所以前面才有突然發現 MIDI 音樂廳還活著的事實。</p>
<p>二十一世紀已經過了十分之一，即使因為科技進步，電子影音都逐漸走向高解析度（高畫質、高音質），不過有時候，純電子的音樂還是可以讓我有一些<del>莫名其妙的</del>感動呢。</p>
<hr />
<p>不過根據我的習慣，應該也是兩三天就玩膩，把它從 Dock 移除了吧，趁現在心情還在，把它寫下來。</p>
<hr />
<p>註：當時我以為解不開，不過昨天有一位 Windows 大師告訴我，其實只要拿回擁有者就行了（囧）</p>

            ]]></description>
        </item>
        <item>
            <title>電影的髒話</title>
            <link>https://blog.yorkxin.org/posts/profanity/</link>
            <pubDate>Sat, 14 Aug 2010 16:00:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2010/08/15/profanity.html</guid>
            
            <description><![CDATA[
                <p>不少好萊塢電影都可以聽到有 F●●k 或 S●●t 等等的髒話，不過翻譯的時候卻常常不翻本義，而是翻作「混蛋」等比較沒有<del datetime="2010-08-14T18:46:02+00:00">威力</del>殺傷力的語詞。而某些台灣電影也常常會出現有髒話的對話，不論這句話是沒有攻擊意味的純粹語氣詞（例如：「幹！作業終於寫完了！耶！！ (^ε^)/」）或是具有攻擊意味的（例如...你知道的）。</p>
<p>幾乎每個文化都有髒話存在，通常嚴重具攻擊性的髒話都與性交有關。可是就我從小到現在的觀察發現，大部份人對於西洋電影的髒話接受度甚高，不覺得那有什麼「髒」的，有的人甚至以美式英文的髒話代替國語或台語的髒話，即使它們的本義相同（例如，使用「F●●k」或「法克」而不使用「幹」）。</p>
<p>雖然從文化的觀點來看，因為我們不是美國人，所以對於那些充滿嚴重攻擊意義的字眼，沒有什麼感覺，也是很正常的。</p>
<p>但我不能認同的是，某些人視國台語髒話為禁忌，卻完全不忌諱英語的髒話：</p>
<ul>
	<li>台灣的電影裡出現大量髒話→下流電影</li>
	<li>美國的電影裡出現大量髒話→ ^_^ so what?</li>
</ul>
請注意我並不是在說，台灣的髒話很好所以我們要支持電影裡有髒話；而是，這根本不是排斥的理由。
<p>說得明白一點，不管是在社會什麼階層的人，生活中多少一定會接觸到髒話，或髒話的字眼（而不具有攻擊的意義）。像電影這種會忠實呈現文化的藝術作品，為什麼要視髒話為禁忌？好像電影裡有髒話就一定是下流電影之類的，更何況髒話也不是全部都代表攻擊，君不見美國電影中，年輕一代在覺得高興的時候也會隨口說出一兩個髒話，對的，就像一些台灣年輕人用「幹」表示開心的語助詞，一樣的道理；那美國人會覺得這種電影下流或兒童不宜嗎？台灣人呢？</p>
<p>──雖然我也覺得電影『艋舺』裡面出現的髒話是太多了，不過我認為，那是因為我沒有過那樣的經歷，所以才覺得他們滿口髒話不甚習慣；甚至，這部電影如果少了髒話的元素，會讓人覺得很假。</p>
<p>如果你覺得，在罵人的時候，英語的髒話跟國台語的髒話一樣髒（即：你認為都是非常嚴重攻擊），那我不是在說你。</p>
<p>講這麼多就是，我覺得電影裡如果出現 F●●k 應該要翻作「幹」才合理啊！不管它是不是用在攻擊或侮辱，都很合適啊不是嗎 XDD。當然也可能是因為代理商認為這個字在原文裡就具有很強的攻擊意義，找個比較和緩的詞來代替，會比較好一點，看看英美出品的一些影視節目，還會把髒話直接做消音，有的還把嘴形打馬賽克，就知道他們對於那些字的反感有多強烈。但反效果就像我上面講的，有些人會以為那些美國髒話不具有那麼強的攻擊性。</p>
<p>最後呢，你可以不同意我認為髒話應該要合理出現的論調。在日常生活中，我也不太喜歡用髒話的字詞（是啦我是有比較常講啦，不過講一次就後悔一次，而且只跟熟的、可以接受的人講），同時我也有不少朋友對於完全不具攻擊意義的髒話字眼感到很不舒服的人，這種話當然生活中還是少說點好。</p>
<p>我在批評的是那些<strong>認為台灣髒話不好，而美國髒話好</strong>的人啊！講美國髒話並沒有比較高級啊！</p>
<hr />
<p>隨文附上維基百科關於<a href="http://zh.wikipedia.org/zh-tw/髒話">髒話</a>的條目網址，不喜勿入…。</p>
<p>不過，維基百科裡面記載的髒話，好像定義比較寬鬆，連某些攻擊性極低（普遍接受）的也列入了。</p>
<p>p.s. 本文把某些美國髒話標以馬賽克，是為了不要讓笨笨的美國製搜尋引擎以為這篇文章充滿髒話。至於中文的部份，我想那些電腦到現在還分不清楚幹是樹幹還是勞動。</p>

            ]]></description>
        </item>
        <item>
            <title>R3versi - 大一寫的黑白棋遊戲</title>
            <link>https://blog.yorkxin.org/posts/r3versi/</link>
            <pubDate>Mon, 16 Nov 2009 16:00:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2009/11/17/r3versi.html</guid>
            
            <description><![CDATA[
                <p>由於聽說同學去上本系蔣某老師的進階程式設計，最近要寫<a href="http://zh.wikipedia.org/wiki/黑白棋">黑白棋</a>的遊戲<span style="text-decoration:line-through;">（其實是想修卻萬年衝堂修不到的怨念）</span>，所以我把我大一寫的黑白棋遊戲給放上來了...。這不是作業，只是寫爽的...。</p>
<p>程式碼都在 <a href="http://github.com/yorkxin/R3versi">GitHub 的 Repository</a>，不會用 git 的話可以按 Download 下載 zip 或 tgz 包裝。</p>
<img class="aligncenter size-full wp-image-684" title="R3versi screenshot" src="https://pub-e523589e504d4ca49336e74978c38e52.r2.dev/images/2009/2009-11-17-r3versi/e89ea2e5b995e5bfabe785a7-2009-11-17-4-00-18.png" alt="R3versi screenshot" width="700" height="504" />
<p>兩年了，再回頭看自己大一寫的程式碼，除了有很濃厚的 <a href="http://cpu.tfcis.org/~itoc">MapleBBS-itoc</a> 的影子之外，就是又雜又難看啊而且還很少註解的 code 了。檔案的結構也是亂七八糟的，濫用 external function reference -_-，連 Makefile 都是抄 MapleBBS-itoc 的 XDD</p>
<p>「視窗」是用 <a href="http://en.wikipedia.org/wiki/Curses_(programming_library)">curses</a> 函式庫寫出來的，在 Unix-like 的 OS 都有內建了。而 Windows 只要用 Dev-C++ 和 PDCurses (有 Dev-C++ 的 Package) 也可以編譯並執行喔（而且是 Static Linking，執行檔不用函式庫就能玩了；為什麼是 Static Linking...最近才在上系統程式，還沒學到怎麼改那個 Linking Scheme，囧rz）</p>
<p>至於編譯的方法和所需要的函式庫都描述在 GitHub Repo 的 Readme 了，自個兒去看吧。</p>
<p>License... 我不知道要用哪個 = = 事隔多年也不知道有沒有抄到別人的 code 了，不敢亂寫授權。GNU/GPL、BSD 還是別的？五樓你說呢？</p>
<hr />
<p>p.s. 其實我從來沒玩贏過一次電腦的黑白棋，不管是×電族裡面的，還是 Windows XP 的...。</p>
<p>p.s. 2 不要怪我沒說，這支程式裡<span style="color:#ff0000;"><strong>有很多 bug</strong></span>，而且 <span style="color:#ff0000;"><strong>Makefile 當年是亂抄亂寫的</strong></span>，一點規範都沒有，請<strong><span style="color:#ff0000;">不要拿來當範例</span></strong>程式！（可以拿來當「寫得很爛的程式」的 case study 啦）</p>

            ]]></description>
        </item>
        <item>
            <title>打狗與高雄、打貓與民雄</title>
            <link>https://blog.yorkxin.org/posts/takao-kaohsiung-tamiao-minhsiung/</link>
            <pubDate>Tue, 01 Sep 2009 16:00:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2009/09/02/takao-kaohsiung-tamiao-minhsiung.html</guid>
            
            <description><![CDATA[
                <p>在 PTT Railway 板看到一篇關於日治時期 1920 年 (大正 9 年)「驛名改正」(<code>#1AcVOYPb (Railway)</code>)，簡單來說就是 1920 年的行政區重劃 (所謂的改正) ，連帶鐵路沿線的車站名稱都一併改正。</p>
<p>「<a href="http://zh.wikipedia.org/wiki/高雄歷史">高雄</a>」原名「<a href="http://zh.wikipedia.org/wiki/打狗港">打狗</a>」這個我是已經知道了。不過「打貓」竟然是「<a href="http://zh.wikipedia.org/wiki/民雄鄉">民雄</a>」的古名，這我就很訝異了。印象中只知道打貓約在嘉義地區，但沒有很確定在哪裡（事實上是根本沒上網查過）。</p>
<p>所以當然是丟進 Google 找找看。</p>
<span id="continue-reading"></span>
<hr />
<p>「<strong>打狗</strong>」是平埔族語，原意是「竹林」。日本人依其音似日文「高雄」二字的訓讀(*1)（たかお）而改名。</p>
<p>「<strong>打貓</strong>」是平埔族一個社（部落）的社名。日本人依其音似日文「民雄」二字的訓讀（たみお）而改名。</p>
<p>有趣的地方是，原本漢人寫出來的是「<strong>打</strong>」字輩，到了日文竟很巧地變成了「<strong>雄</strong>」字輩。</p>
<p>這些原本都屬於平埔族原住民的語言，先是被空耳(*2)聽成閩南語的音，轉寫成漢字，之後又被空耳聽成日文音，自 1920 年以來我們便接受了這樣的寫法，即使 1949 年國民政府來台，即使 21 世紀初正名運動如此火熱，我們還是接受了它。──當然別忘了，在這之前還有荷蘭人。</p>
<p>雖然，在台灣歷史上，有<a href="http://zh.wikipedia.org/wiki/台灣舊地名列表">許許多多的地名</a>都有著這樣的命運。我們知道<a href="http://zh.wikipedia.org/wiki/大員">大員</a>也是一個平埔社名，漢人不斷改變它的漢字寫法，它後來甚至還延伸為整個台灣島的稱呼。我想，這是當時的大員社歷代頭目們想不到的吧。</p>
<hr />
<p>參考資料：</p>
<ol>
	<li>PTT 實業坊 Railway 板 〈[情報] 1920年驛名改正〉 一文，作者 weichia。文章代碼 <code>#1AcVOYPb</code></li>
	<li><a href="http://zh.wikipedia.org/wiki/%E9%AB%98%E9%9B%84%E6%AD%B7%E5%8F%B2">高雄歷史 - 維基百科，自由的百科全書</a></li>
	<li><a href="http://zh.wikipedia.org/wiki/%E6%89%93%E7%8B%97%E6%B8%AF">打狗港 - 維基百科，自由的百科全書</a></li>
	<li><a href="http://zh.wikipedia.org/wiki/%E6%B0%91%E9%9B%84%E9%84%89">民雄鄉 - 維基百科，自由的百科全書</a></li>
	<li><a href="http://zh.wikipedia.org/wiki/%E5%8F%B0%E7%81%A3%E8%88%8A%E5%9C%B0%E5%90%8D%E5%88%97%E8%A1%A8">台灣舊地名列表 - 維基百科，自由的百科全書</a></li>
	<li><a href="http://zh.wikipedia.org/wiki/%E5%A4%A7%E5%93%A1">大員 - 維基百科，自由的百科全書</a></li>
</ol>
---
<p>*1 <strong><a href="http://zh.wikipedia.org/wiki/訓讀">訓讀</a></strong>是指以自己的母語來朗讀漢字，例如我們看到「小泉純一郎」會讀「ㄒㄧㄠˇ ㄑㄩㄢˊ ㄔㄨㄣˊ ㄧ ㄌㄤˊ」而不是「こいずみじゅんいちろう」。相對的概念是<strong><a href="http://zh.wikipedia.org/wiki/音讀">音讀</a></strong>，也就是以接近原文的音來讀漢字，就像日本人習慣把中文姓名讀作古漢音，即為音讀。</p>
<p>*2 <a href="http://zh.wikipedia.org/wiki/空耳"><strong>空耳</strong></a>是日文詞，意思是故意把別的語言的音聽成自己語言的音。</p>

            ]]></description>
        </item>
        <item>
            <title>國語在台灣的官方地位如何產生？</title>
            <link>https://blog.yorkxin.org/posts/mandarin-as-official-lang-in-tw/</link>
            <pubDate>Mon, 25 May 2009 16:00:00 +0000</pubDate>
            <guid isPermalink="true">https://blog.yorkxin.org/2009/05/26/mandarin-as-official-lang-in-tw.html</guid>
            
            <description><![CDATA[
                <p>今天 (5/25 一) 上了國語與方言，這是國文系的選修課。上課的主要教材是台大語言所<a href="http://homepage.ntu.edu.tw/~sfhuang/">黃宣範教授</a>的《語言、社會與族群意識》一書。</p>
<p>今天剛好講到一個很有趣的段落： (p. 115)</p>
<blockquote>
<p>國語（漢語<a href="http://zh.wikipedia.org/wiki/%E5%AE%98%E8%AF%9D_(%E4%B8%AD%E5%9B%BD%E5%8E%86%E5%8F%B2)">官話</a>的北京話方言）身為台灣地區現行的<a href="http://en.wikipedia.org/wiki/Standard_language">標準語</a> (通用全國，從政府官員到國民教育的語言)，是如何產生的？</p>
</blockquote>
<p>文中提到標準語大抵上有四種形成的方式：</p>
<blockquote>
<p>一是以境內<strong>多數語言</strong>為標準語。諸如中國、法國。</p>
<p>二是以<strong>外來少數語族</strong>的語言(外來語)為標準語。這通常發生在殖民地，強勢民族統治弱勢民族而產生的。但殖民地脫離殖民統治後，可能會（意識形態地）採用原先的語言，或沿用原殖民國的語言。諸如亞非多數原屬西洋人殖民地的國家。</p>
<p>三是把境內<strong>所有主要語言</strong>都提升到官方語言的地位，如比利時、端士、香港。</p>
<p>四是以<strong>少數語言</strong>為標準語，因為多數語言太多了大家吵翻天乾脆一不做二不休改用少數語。</p>
</blockquote>
<hr />
<p>讀完了這一頁，老師便提問了：</p>
<blockquote>
<p>「那麼台灣屬於哪一種呢？」</p>
</blockquote>
<p>我們知道現在的國語身為標準語，其地位確立是基於（一）多數語。（這應該沒人反對；我想國語教育在台灣還算是成功的吧？）</p>
<p>那麼，國民政府「播遷來台」初期推行國語，應該歸在哪一類呢？</p>
<hr />
<span id="continue-reading"></span>
<p>有人很直覺的就說了「外來語。」這讓我很驚訝其實。</p>
<p>我心裏突然有了一個想法，於是說了如下：</p>
<blockquote>
<p>當初國民政府來台的時候，是把台灣當作反攻大陸的復興基地，君不見那地圖上還是美妙的秋海棠。其政府制度直接承襲中華民國那一套（或說整套搬過來用），當然也包括民國 20 幾年(?)的時候確立的「以<a href="http://zh.wikipedia.org/wiki/%E7%8F%BE%E4%BB%A3%E6%A8%99%E6%BA%96%E6%BC%A2%E8%AA%9E">國語</a>為中華民國標準語」的制度。</p>
</blockquote>
<p>因此，當時的政府認為台灣是中華民國的一部份，自然而然就要使用這樣的制度。</p>
<p>結論是，它是<strong>多數語</strong>──僅管它「多數」的分母在國民政府的眼中，和在當時台灣民眾的眼中，可能有所不同。</p>
<p>（一些年代的細節不是當時說的，是現在寫文章補的）</p>
<p>而持「外來語」觀點的同學則提到：</p>
<blockquote>
<p>國民政府視台灣為一個反攻大陸的基地，同時也僅止於此。其行徑近乎殖民統治，情況類似第二點的外來語。因此國語的官方地位的確立，可以算是<strong>外來語</strong>。
其實他這麼說好像也不無道理。</p>
</blockquote>
<hr />
<p>後來的討論頗熱烈的，有支持多數語論者，有支持外來語論者，還有人把日治時期的漢文和口頭漢語（閩南話、客家話）搞混了。</p>
<p>基本上「國府初期的台灣人使用閩南語居多，相對而言國語是少數語」這點沒有人否認，但它是否會影響這個問題的答案呢？</p>
<p>最後有某個同學提出一個觀點，這是不是一種「意識形態」？</p>
<blockquote>
<p>「從不同的觀點來看，就會有不同的結果。」</p>
</blockquote>
<hr />
<p>老師最後沒有給答案，她只說了：</p>
<blockquote>
<p>這個問題沒有標準答案。沒錯，你從不同的角度看，就會有不同的答案。如果現在換到以原住民為本的角度，你就會發現，原住民一直都是處於被殖民狀態，一直要被迫學習其他完全不同語系的語言，每換一個統治民族就要換一次。對他們而言，國語也只是另一種<strong>外來語</strong>罷了。</p>
</blockquote>
<p>而從國民政府的角度來看，台灣只不過是中華民國的一小部份，中華民國規定國語，台灣當然也用國語。在國民政府的眼裡，國語是屬於<strong>多數語</strong>。</p>
<p>如果再從當時本來就住在這塊土地上的漢人來說，可能閩南語才是多數語，而使用國語的人成為少數族群，國語變成一種<strong>外來語</strong>。
「意識形態沒那麼可怕的，」她說，「只是觀點不同罷了。」</p>
<hr />
<p>如此討論後，我也認為這個問題沒有一個標準的答案。</p>
<p>解釋方式不同，得到的自然就不同。</p>
<p>黃宣範教授的書中還有許多有趣的事，諸如當初國民政府如何實現「中華民國國語推行模範省」的目標的具體方式，以及日本人對待同一件事（語言造成的族群意識）的操作手段。</p>
<hr />
<p>歷史也是可以如此有趣的。</p>
<p>──那為什麼我以前讀得要死要活  = =</p>

            ]]></description>
        </item>
    </channel>
</rss>
