この記事は、 Calendar for 東京電機大学 | Advent Calendar 2022 - Qiita の5日目です。
東京電機大学では、学生用のポータルサイトで日本システム技術株式会社のUNIVERCAL PASSPROT RX(以下、UNIPA)を導入しています*1。
https://portal.sa.dendai.ac.jp/uprx/
このページはあまり言いたくはありませんがUXが悪く、ブラウザバックが無効化されていたりリロードするとセッション維持が終わってしまいます。
この記事では、そんなUNIPAのセッション維持方法の仕組みを見ていこうかと思います。
Webサービスのセッション維持方法
ログインするWebサービスでは、様々な方法でセッションを維持することができます。以下で方法を説明します。
Cookieを使用する
一番スタンダードな方法です。CookieはHTTPリクエストヘッダに格納してサーバに送信することができます。これを利用してサーバで発行したトークンやJWTなどをCookieに入れることでクライアント側では、ブラウザのCookieに格納され、認証はサーバでCookieを比較するだけでできます。
Session StorageやLocal Storageを使用する
ブラウザにはSession StorageやLocal Storageが存在します。ここにデータを格納することでブラウザ上で永続的*2に保存されます。これを使用してJavaScriptでデータをfetchする際にそこに格納したトークンなどをカスタムHTTPヘッダに入れて送信することで認証します。
JavaScriptの変数内に入れる
JavaScriptの変数内に入れます。主にグローバル変数などに格納するかと思います。
DOM内に保存してJavaScriptで取得して送信する
古いWebページにあるやつです。DOM内にdisplay: none
にしているInputElementなどを入れておりそこにカスタム要素で色々保存しています。これは、<form>
内に保存することでJavaScriptを使用せずともトークンを付与したリクエストが送れるためです。
UNIPAのセッション維持方法
UNIPAをDev Toolsで見ると、リンク先に移動したときに取得するHTMLがすべてPOSTメソッドで取得されており、payloadにはrx-token
とrx-loginKey
が存在します。このトークン名をDOMで検索書けると10箇所ほど見つかります。このことからUNIPAはDOM内に保存してJavaScriptで取得して送信するを使用していることがわかります。
また、form
要素で定義されているのを見ると、method="post" action="[link]" enctype="application/x-www-form-urlencoded"
とあります。そのためこのフォーム内のボタンをクリックするとPOSTメソッドでpayloadはapplication/x-www-form-urlencoded
形式で送信されるということがわかります。ちなみに、すべて同じフォームであるためリンクは1つです。出し分けにはpayloadのmenuForm:mainMenu_menuid
を使用しているようでした。
クロールする方法
さて、これらを踏まえてクロールする方法を考えます。
一番簡単な方法としては、Seleniumなどでヘッドレスブラウザを開いてDOM操作をするのが一番簡単ですが、処理が重いというデメリットがあります。
考えなくてはいけないこと
2つあります。rx-tokenがリクエストの度に変わるのでこれはDOMから読み取らなくては行けません。また、フォームで送信するリンクも毎回変わっています。どちらも、CSRF対策でしょう。
それらを踏まえたベストプラクティス
- まず、
portal.sa.dendai.ac.jp/
をプログラム上でGETします。 - DOMを解析し、CSSセレクタ
form[id="infoForm"]
の要素のaction
属性を取得します。(このURLにPOSTすることでログインできます。) 2で取得したURLに以下のpayloadを付与してPOSTします。
loginForm=loginForm&loginForm%3AuserId=[学籍番号]&loginForm%3Apassword=[password]&loginForm%3AloginButton=&javax.faces.ViewState=stateless
3で取得したDOMから、CSSセレクタ
input[name="rx-token"]
の要素1つ取得して*3value
属性を取得します。これがrx-token
です。- 3で取得したDOMから、CSSセレクタ
input[name="rx-loginKey"]
の要素を1つ取得して*4value
属性を取得します。これがrx-loginKey
です。 - 3で取得したDOMから、CSSセレクタ
input[name="javax.faces.ViewState"]
の要素を1つ取得して*5value
属性を取得します。これはjavax.faces.ViewState
で、よくわかりません。 - 3で取得したDOMから、CSSセレクタ
form[method="post"]
の要素を1つ取得して*6action
属性を取得しします。これが別ページに行くためのURLです。URLは/uprx/up/bs/bsd007/Bsd00701.xhtml
のようになっているので前にhttps://portal.sa.dendai.ac.jp
を追加します。 - 行きたい場所に取得した値を入れてPOSTします。以下は「学生時間割表」のリンク例です。
menuForm:mainMenu_menuid
などは実際にアクセスしてみて取得するのがいいと思います。
menuForm=menuForm&rx-token=[rx-token]&rx-loginKey=[rx-loginKey]&rx-deviceKbn=1&javax.faces.ViewState=-[javax.faces.ViewState]&rx.sync.source=menuForm%3AmainMenu&menuForm%3AmainMenu=menuForm%3AmainMenu&menuForm%3AmainMenu_menuid=3_0_0_1
ここで注意する必要があるのは、rx-token
は同一セッションで同じなのに対してrx-loginKey
とjavax.faces.ViewState
はアクセスごとに変わってきます。そのため、ページ遷移したらrx-loginKey
とjavax.faces.ViewState
は毎回DOMから取得する必要があります。
最後に
ぶっちゃけ、UNIPAのUXが最高で大学側もAPIを提供してくれたらこんな記事書かなかったな。