中秋連假的時候想到老爸的客戶大多是稍微有年紀的長輩,過去都是慢慢教會怎麼使用 Email 來註冊系統的,但在台灣 LINE 的普及率其實非常的高,如果能用 LIFF 並且免去註冊流程的話似乎是個不錯的選擇。
目前 LINE 的 Mini App 還沒開放,因此不知道是否能獲得比 LIFF 更好的開發體驗。
LIFF
LIFF 全名叫做 LINE Front-end Framework 簡單來說就是允許我們在 LINE 裡面製作一些前端應用的工具。
因為是以「前端」為前提的,因此使用 Vue.js / React 之類的開發一些小工具實際上也是蠻輕鬆的。
不過如果要整合後端,像是 Ruby on Rails 就會遇到一些不方便的。
IDToken
目前我們有三種方式可以跟 LINE 的帳號連結,分別是 Chatbot 的 Account Linking 模式、OAuth2 的 LINE Login 以及 LIFF 的 IDToken。
扣掉 Account Linking 實際上 OAuth2 跟 IDToken 有點是取得流程的不同。
在 LIFF 中要獲取最佳的使用者體驗,使用 IDToken 會比 OAuth2 來得更好。這是因為自從 OAuth2 有了 # CVE-2015-9284 這個 CSRF 漏洞後,所有 OAuth2 登入都需要透過 POST 方式啟動並且加上 CSRF Token 才能動作,這個操作變相的會需要使用者點選「登入」按鈕。
但是在 LIFF 中取得 IDToken 雖然有點繞路,但是我們可以透過控制 LIFF 的 Redirect 流程來完成無縫登入。
LIFF Redirect
LIFF 根據使用的情況會有一次到兩次的轉跳,並且不管怎樣都無法避免掉第一次轉跳。
Primary Redirect
這個步驟是從 LINE 開啟 LIFF 應用時會從 https://liff.line.me
跳到實際的網站,同時會攜帶一些資訊到指定的頁面中。
如果是在 LINE 裡面觸發,會是先把 IDToken 放進去方便後面動作,如果是一般瀏覽器開啟就需要額外呼叫
liff.login()
跑過一次類似 OAuth2 的流程。
Secondary Redirect
如果是簡單的 Single Page Application 的話我們會直接呼叫 https://liff.line.me/xxx
那麼就只會有 Primary Redirect 的觸發,但是如果我們的應用是稍微複雜的,就會需要像是 https://liff.line.me/xxx/profile?mode=liff
的設計來開啟不同畫面,這就會觸發 Secondary Redirect 的動作。
在 LIFF 的流程基本上是這樣的
階段 | 動作 |
---|---|
liff.line.me | 生成 Token 並轉跳 |
Primary | 紀錄資訊並等待 liff.init() 完成後轉跳 |
Secondary | 等待 liff.init() 完成後允許後續操作 |
因為上述的步驟是不可迴避的,如果到了 Secondary Redirect 結束後才呼叫 liff.getIDToken()
並且處理登入,時機上有點太晚而且對後端來說非常不好處理。
登入處理
在尋找解決方案的過程中,發現 Primary Redirect 階段拿到的 window.location.search
和 window.location.hash
裡面是帶有 IDToken 可以使用的,因此我們可以稍微調整一下流程。
原本是當頁面載入時馬上做 liff.init()
來初始化 LIFF 應用,但我們加入幾個判斷,變成類似這樣的物件。
1export default class LIFFApp {
2 constructor() {
3 this.search = new URLSearchParams(window.location.search)
4 this.hash = new URLSearchParams(window.location.hash)
5 }
6
7 get isSecondary() {
8 const state = this.search.get('liff.state')
9 return state && state.length !== 0
10 }
11
12 get idToken() {
13 return this.hash.get('id_token')
14 }
15
16 get liffId() {
17 return document.body.dataset.liffId
18 }
19
20 async run() {
21 await this.doLogin()
22 await liff.init({ liffId: this.liffId })
23 }
24
25 doLogin() {
26 if (this.isSecondary) {
27 const form = new FormData()
28 form.append('id_token', this.idToken)
29
30 return fetch('/liff/sessions', {
31 method: 'POST',
32 body: form
33 })
34 }
35
36 return Promise.resolve()
37 }
38}
我們在 Rails 中就可以像這樣使用
1document.addEventListener('turbolinks:load', () => {
2 const app = new LIFFApp()
3 app.run()
4})
透過這樣的機制我們就可以在 Primary Redirect 的時候判斷是否要先呼叫一次登入的 API 還是直接執行就好。
目前的設計會讓 LIFF 入口不實現任何功能,單純處理登入機制。
後端處理
從 LIFF 拿到的 IDToken 官方不建議直接使用,因此需要再呼叫一次 API 到 LINE 的後端驗證,如果每次操作都要呼叫 API 可能會有效能問題,也不確定是否會有 Rate Limit 的限制。
我自己在這邊直接實作了 Warden::LINE 這個套件來輔助我驗證。
在這次的實驗發現回傳值跟 Devise 不相容,未來會更新到能被 Devise 吃到的狀態。
因為這次要搭配 Devise 使用,作為為來支援網頁版或者 App 的情境,因此除了使用 Warden 之外還需要跟 Devise 整合在一起,登入的流程也無法直接使用 Devise 的行為。
1module LIFF
2 class SessionsController < LIFF::BaseController
3 skip_forgery_protection only: :create
4 skip_before_action :ensure_liff_user!, only: :create
5
6 def create
7 # TODO: Rewrite Warden::LIFF to support Devise
8 if warden.authenticate(:line, scope: :line)
9 @user = LIFF::UserFinder.new(warden.user(:line)).perform
10 sign_in @user
11 head :no_content
12 else
13 head :unauthorized
14 end
15 end
16 end
17end
因為 Devise 已經佔用走了 user
scope 所以我們呼叫 Warden 的 warden.authenticate
時需要指定使用自訂的 Warden::LINE
規則以及套用在 line
scope 上面。
另一方面 Devise 把 FailureApp 改掉,所以我們也無法用 authenticate!
來自動處理登入失敗,這邊改為用 Wardne 的登入結果判斷。
好像也可以直接呼叫 API 驗證,不過
Warden::LINE
是之前做其他專案實現的,所以就直接拿來使用。
至於 LIFF::UserFinder
會在找不到使用者時自動產生新的使用者,這邊就不多做說明。
有了這樣的機制,我們就可以在 LIFF::BaseController
加上 ensure_liff_user!
方法,確保所有使用 LIFF 的使用者都會先經過 /liff
的 Primary Redirect 呼叫 /liff/sessions
登入,再經過 Secondary Redirect 的流程進入 LIFF 指定的頁面。
1module LIFF
2 class BaseController < ApplicationController
3 layout 'liff'
4
5 before_action :ensure_liff_user!
6
7 private
8
9 def ensure_liff_user!
10 return if user_signed_in?
11
12 render :not_found, locals: { reason: t('liff.shared.user_not_found') }
13 end
14 end
15end
這邊我們可以簡單利用 Devise 提供的 user_signed_in?
來協助我們處理,同時因為沒有成功登入再完成 Secondary Redirect 時就會以錯誤畫面顯示,取代原本應該出現的功能。
實作起來也相對的簡單跟乾淨,剩下的就是繼續用 Rails 最近新推出的 Hotwire 等新工具以純後端的方式開發,在幾乎不依靠前端的狀況下以後端實現大多數功能。
總結
這個方法算不上非常乾淨,但是在操作上可以利用「載入中」來消除使用者覺得卡頓的感覺,另一方面無縫的完成會員登入的操作也讓使用體驗變得簡單很多。
至於無法在 Query String 拿到 IDToken 雖然有點可惜,但推測可能是在安全性上有所考量,原本想參考 LINE Taxi 但似乎已經不是 LIFF 的狀態。
只能期待之後可以使用 Mini App 的時候在這方便會有更好的開發體驗,以及能製作更流暢的應用。