UNIPAのセッション維持の方法を見る

この記事は、 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 StorageLocal Storageが存在します。ここにデータを格納することでブラウザ上で永続的*2に保存されます。これを使用してJavaScriptでデータをfetchする際にそこに格納したトークンなどをカスタムHTTPヘッダに入れて送信することで認証します。

JavaScriptの変数内に入れる

JavaScriptの変数内に入れます。主にグローバル変数などに格納するかと思います。

DOM内に保存してJavaScriptで取得して送信する

古いWebページにあるやつです。DOM内にdisplay: noneにしているInputElementなどを入れておりそこにカスタム要素で色々保存しています。これは、<form>内に保存することでJavaScriptを使用せずともトークンを付与したリクエストが送れるためです。

UNIPAのセッション維持方法

UNIPAをDev Toolsで見ると、リンク先に移動したときに取得するHTMLがすべてPOSTメソッドで取得されており、payloadにはrx-tokenrx-loginKeyが存在します。このトークン名をDOMで検索書けると10箇所ほど見つかります。このことからUNIPAはDOM内に保存してJavaScriptで取得して送信するを使用していることがわかります。

メニューのform。実際にform内に保存されている

また、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対策でしょう。

それらを踏まえたベストプラクティス

  1. まず、portal.sa.dendai.ac.jp/をプログラム上でGETします。
  2. DOMを解析し、CSSセレクタ form[id="infoForm"] の要素のaction 属性を取得します。(このURLにPOSTすることでログインできます。)
  3. 2で取得したURLに以下のpayloadを付与してPOSTします。

    loginForm=loginForm&loginForm%3AuserId=[学籍番号]&loginForm%3Apassword=[password]&loginForm%3AloginButton=&javax.faces.ViewState=stateless
    
  4. 3で取得したDOMから、CSSセレクタinput[name="rx-token"]の要素1つ取得して*3value属性を取得します。これがrx-tokenです。

  5. 3で取得したDOMから、CSSセレクタinput[name="rx-loginKey"]の要素を1つ取得して*4value属性を取得します。これがrx-loginKeyです。
  6. 3で取得したDOMから、CSSセレクタinput[name="javax.faces.ViewState"]の要素を1つ取得して*5value属性を取得します。これはjavax.faces.ViewStateで、よくわかりません。
  7. 3で取得したDOMから、CSSセレクタform[method="post"]の要素を1つ取得して*6action属性を取得しします。これが別ページに行くためのURLです。URLは/uprx/up/bs/bsd007/Bsd00701.xhtmlのようになっているので前にhttps://portal.sa.dendai.ac.jpを追加します。
  8. 行きたい場所に取得した値を入れて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-loginKeyjavax.faces.ViewStateはアクセスごとに変わってきます。そのため、ページ遷移したらrx-loginKeyjavax.faces.ViewStateは毎回DOMから取得する必要があります。

最後に

ぶっちゃけ、UNIPAのUXが最高で大学側もAPIを提供してくれたらこんな記事書かなかったな。

*1:スマホ版は噂によると評価が低すぎるため未契約らしいです。

*2:ブラウザの履歴を削除したりすると消えます。

*3:複数ありますがすべて同じです。

*4:複数ありますがすべて同じです。

*5:複数ありますがすべて同じです。

*6:複数ありますがすべて同じです。