UserAgentをアプリから切り分けられるからといって安易に採用するとトラブルが起きるという話。
1) WebブラウザのUserAgennt
サービス向けのPC/モバイル向けサイトとモバイルアプリ向けサイトを共有するとWebから見るとモバイルの開発コストが浮く。コストだけではなくWeb開発者がモバイルアプリの煩雑さを見ていないことにする、という捉え方もある。
PC/モバイル/モバイルアプリを切り分けずにサービスを提供しようとしてもどうしてもモバイルアプリを判別したいばあいにWebブラウザのUserAgentをカスタマイズすることでサーバー側で判別するという手法が2010年代後半まで採用されていた。
iOSアプリ開発向けにはUserAgentを解決する方法がある。iOSアプリ開発の歴史が長いためいくつかの手段が提供されており手段毎に優先順位がある。
WKWebViewのカスタムUserAgentを設定する方法 - すっさんぽ
2) reCAPTCHA v2
reCAPTCHAはBotによるアカウント追加や、不正ログインを避けるための仕組みで不正アクセスにたいする門番として機能した。門番としての悪名が高いのはv2と言われるバージョンでバスや信号機、横断歩道などをユーザーに選択させるのだが、選択画像がわざと荒かったり、選択画面が小さいなど騎乗が多い門番だった。
最新はv3でv2のような写真選択機能ではなく、画面に常駐しユーザーの挙動を見てBotか利用者かの判断を行うようになった。
3) UserAgenet詐称を許さないreCAPTCHA v2
UserAgenetでPC/モバイルとモバイルアプリを切り分けしつつ、reCAPTCHA v2を採用した場合、利用者に写真選択を必要以上に求めている可能性がある。通常なら1回で済む画像判定が6回程度繰り返される。
GoogleとしてはカスタムUserAgentは許さない方向で取り組みを進めている。
ChromeでUserAgentが凍結される日(User Agent Client Hintsの使い方) - Qiita
reCAPTCHA v2もGoogleの1サービスであるので不明なUserAgentはBotとみなして厳しい基準で写真選択を求めるようだ。
4) reCAPTCHAの抜本的な対策あり
iOS向けのreCAPTCHA SDKが用意されている(AndroidはSDK無しで導入可能)が2020年12月現在、iOSはベータ版扱いで、reCAPTCHA v2,v3 からreCAPTCHA Enterpriseへの切り替えが必要となる。
iOS アプリへの実装 | reCAPTCHA Enterprise | Google Cloud
当面は採用できそうにない。Webサービス側でAPI(ログイン、投稿)を整備しないと採用しても利用は厳しいように見受けられる。
5) アプリだけで解決を試みる
PC/モバイル向けWebページとモバイルアプリで共有の画面を使わざるをえない場合は、reCAPTCHA v2は表示されているページでしか動作しないので、リクエストされたURLを判断してカスタムなUserAgenetと、オリジナルUserAgeentを切り替えで回避する。
5-1) 新規実装でのカスタムUserAgent対策
新規実装であればUserAgenetにカスタム要素を持たせる方法としては以下のサイトが参考になる。
WKWebViewのUserAgentに追記をする | 杏z学習帳 https://blog.anzfactory.xyz/articles/20190902/swift-wkwebview-custom-useragent/
既存のUserAgentにカスタム情報を付与する。画面追加タイミングでカスタム情報を渡すので設定方法としては簡潔でわかりやすい。
5-1) 既存実装でのカスタムUserAgent対策
既存に稼働しているサービスで、
- UserAgenetをオリジナルからカスタムな内容に書き換えている
- サーバー側の改修が難しい とった条件下では採用できない対策となる。
回避策としてはa.オリジナルのUserAgentを保存しておき、b.reCAPTCHA v2を必要とする画面だけUserAgentに置き換える方法で解決を試みる。
図にすると以下のような流れとなる。
この解決方法は制約事項があって、reCAPTCHA v2を必要とする画面とカスタムUserAgentを必要とする画面が別のパスで、ログイン後にメイン画面のようなreCAPTCHA v2を必要とする画面を経由しないとUserAgentを必要とする画面に辿りつかない場合に成立する。
対策用コードはWKWebViewのDelegateイベントであるWKNavigationDelegate でそれぞれ対応対応コードを追加する。
5-3)WKWebViewでのUserAgentの取得
UserAgentを取得のためdidStartProvisionalNavigation イベントを記述する。didStartProvisionalNavigatioはWKWebViewで暫定的な読み込みを開始する際に呼ばれるがWeb画面のインスタンスを生成タイミングにも呼ばれる。WKWebView.evaluateJavaScript() 経由でUserAgentを取得する。UserAgentは非同期でおこなわれる。すぐにUserAgentを取得できていない点に注意する。
var originalUserAgent: String? = nil // オリジナルUserAgentを格納する変数
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
webView.evaluateJavaScript("navigator.userAgent", completionHandler: { (result, error) in
if self.originalUserAgent == nil {
self.originalUserAgent = result as? String
}
})
}
}
5-3)WKWebViewでのUserAgentの入れ替え
UserAgentを入れ替える処理は、didCommit navigationに記述する。didCommit navigationは読み込むコンテンツが決定した際に呼び出される。読み込むコンテンツはWebサーバー側のリダイレクト処理や読み込むJavaScriptのアクセス有無が解決されたあとに呼び出される。
読み込むコンテンツはwebView.url に格納されているのでドメインとURL末尾のコンポーネントでカスタムUserAgentを設定するか判断する。
let servicePath = "your domain and service path: ex. http:://foo.com/path"
let lastPathComponent = "target page last path componenct: ex. login.php"
let cutomUserAgent = "your custom useragent: ex. CustomApp"
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
guard let url = webView.url, let originalUseragent = self.originalUserAgent else {
return
}
if url.absoluteString.hasPrefix(self.servicePath) && url.lastPathComponent == self.lastPathComponent {
webView.customUserAgent = self.cutomUserAgent
} else {
webView.customUserAgent = originalUseragent
}
}
上記コードによってUserAgentが切り替わって欲しい画面だけでカスタムUserAgentが認識されるようになる。ログアウトに関しても、UserAgentが切り替わって欲しい画面のURLと異なるのであれば説明した対策でカバーできる。
まとめ
PC/モバイル向けWebページとモバイルアプリで共有の画面と、reCAPTCHA v2の組み合わせで問題が発生したが、いずれもモバイルアプリに対する初期実装で雑な対応が行われ、メンテナンスフェイズでは利用者のフィードバックが大多しく行われていない場合はWebサービスとモバイルアプリ間でだけおきる問題が発生しそのまま放置されるケースは増えてくるだろう。