vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

Pythonで CSV -> Json を作る(そしてそれをUnityで読み込み)

はじめに

前回に引き続き必要になったのでメモっておきます。

Pythonを使ってCSVとして保存されたファイルを読み込み、Jsonファイルとして出力。
それをUnityで読み込みます。

Pythonを使うことになった理由は単に興味があったからですw

※2017.9.20修正
Pythonのファイル名、データを格納するための Parts クラスでのアトリビュートの宣言方法を修正しました。

CSVを読み込む

まずはCSVを読み込んで、 Parts というクラス(酷い名前なのは気にしない方向で。。。)に格納します。

parts.csv

PartsNo,PartsName,Description,
No.1,Parts1,一つ目のパーツです,
No.2,Parts2,二つ目のパーツです,

parts.py

 # -*- coding: utf-8 -*-
# coding: utf-8
class Parts:
    def __init__(self, parts_no: str, parts_name: str, description: str):
        self.parts_no = parts_no
        self.parts_name = parts_name
        self.description = description

main.py

 # -*- coding: utf-8 -*-
import csv
from parts import Parts


def main():
  # CSVファイルを読み込む.
  with open(f'parts.csv', encoding='utf-8') as csvFile:
    for index, row in enumerate(csv.reader(csvFile)):
        # タイトル行を無視する.
        if index <= 0:
            continue

        # 読み込んだ内容に[]が含まれているためそれを取り除く.
        replace_row = remove_symbols(str(row))
        split_rows = str.split(replace_row, ',')

        # コンマ区切りでSplitした内容をクラスに格納.
        new_parts = Parts(split_rows[0], split_rows[1], split_rows[2])


def remove_symbols(original: str):
    # 読み込んだ内容に[]、半角スペースが含まれているためそれを取り除く.
    remove_left_bracket = str.replace(original, '[', '')
    remove_right_bracket = str.replace(remove_left_bracket, ']', '')
    remove_space = str.replace(remove_right_bracket, ' ', '')
    return remove_space

main()

  • 「with」を使用することで、C#の「using」と同様ファイルの使用が終わったら自動で開放してくれるようになります。
  • 読み込んだ内容は「[‘PartsNo’, ‘PartsName’, ‘Description’, ‘’]」のように [ と ] が含まれるため、それを取り除く必要があります
    (もう少し上手いやり方がありそうな気はしています)。
  • 今回は読み込んだ内容をわざわざクラスに入れる必要はあまりないのですが、形式や文字数に制限がある場合などはクラス格納 -> チェックとした方が良さそうです。

Jsonファイルとして出力

クラスのデータをDictionaryデータとして受け取る

このままクラスをJsonに変換できると良いのですが(C#Javaのように)、
Pythonでは難しいようです。

そのため、クラスとして格納したデータを受け取って、Dictionaryとして返すようにします。

parts_dictionary_factory.py

 # -*- coding: utf-8 -*-
from parts import Parts


class PartsDictionaryFactory:

    def create(self, target_parts: Parts):
        if type(target_parts) != Parts:
            return

        return {"PartsNo": target_parts.parts_no, "PartsName": target_parts.parts_name,
                "Description": target_parts.description}

main.py

 # -*- coding: utf-8 -*-
import csv
from parts import Parts
from parts_dictionary_factory import PartsDictionaryFactory


def main():
    parts_dictionary = []
    dictionary_creator = PartsDictionaryFactory()

    with open(f'parts.csv', encoding='utf-8') as csvFile:
        for index, row in enumerate(csv.reader(csvFile)):
            if index <= 0:
                continue
            replace_row = remove_brackets(str(row))
            split_rows = str.split(replace_row, ',')
            new_parts = Parts(split_rows[0], split_rows[1], split_rows[2])

            # Dictionaryのデータに変換したものをDictionaryに追加.
            parts_dictionary.append(dictionary_creator.create(new_parts))

~省略~

Jsonファイルの出力

あとはこれまで取得・変換した文字列をJsonに置き換えて、 ファイルとして出力するだけです。

main.py

# -*- coding: utf-8 -*-
import json
import csv
from parts import Parts
from parts_dictionary_factory import PartsDictionaryFactory


def main():
    parts_dictionary = []
    dictionary_creator = PartsDictionaryFactory()

    with open(f'parts.csv', encoding='utf-8') as csvFile:

        for index, row in enumerate(csv.reader(csvFile)):
            if index <= 0:
                continue
            replace_row = remove_symbols(str(row))
            split_rows = str.split(replace_row, ',')
            new_parts = Parts(split_rows[0], split_rows[1], split_rows[2])
            parts_dictionary.append(dictionary_creator.create(new_parts))

    meta_parts = {"ModelName": "Model1", "PartsList": parts_dictionary}

    # 文字列をJson形式に変換.
    json_text = json.dumps(meta_parts)

    # ファイル出力.
    with open(f'parts.json', 'w') as file_object:
        file_object.write(json_text)
        file_object.flush()


def remove_symbols(original: str):
    remove_left_bracket = str.replace(original, '[', '')
    remove_right_bracket = str.replace(remove_left_bracket, ']', '')
    remove_space = str.replace(remove_right_bracket, ' ', '')
    return remove_space


main()

結果として下記のようなファイルが作成されます。

parts.json

{
  "ModelName": "Model1",
  "PartsList": [
    {
      "PartsNo": "'No.1'",
      "PartsName": "'Parts1'",
      "Description": "'\u4e00\u3064\u76ee\u306e\u30d1\u30fc\u30c4\u3067\u3059'"
    },
    {
      "PartsNo": "'No.2'",
      "PartsName": "'Parts2'",
      "Description": "'\u4e8c\u3064\u76ee\u306e\u30d1\u30fc\u30c4\u3067\u3059'"
    }
  ]
}
  • 実際は改行されずに出力されますが、見やすさのために編集しています。

UnityでJsonファイルを読み込む

UnityでJsonのデータをクラスに格納するのは以前もやりましたが、
今回もJsonUtilityを使用します。

まずはJsonのデータを格納するクラスの準備から。

今回はJsonの中に配列(PythonでDictionaryとして出力したもの)が含まれており、
これを格納する Parts というクラスと、
Parts のリスト、及びメタ情報(ModelName)を格納する MetaParts というクラスを作成します。
(名前の酷さはry)

Parts.cs

using System;

[Serializable]
public class Parts
{
    public string PartsNo;
    public string PartsName;
    public string Description;
}

MetaParts.cs

using System;
using System.Collections.Generic;

[Serializable]
public class MetaParts
{
    public string ModelName;
    public List PartsList;
}
  • Jsonのデータを格納するクラスには [Serializable] をつけておく必要があり、
    これがないとうまくデータを格納できませんでした。

ファイルの読み込みとJsonデータからの変換

あとはJsonファイルを読み込み、クラスに格納します。
JsonファイルはUnityEditorで動かす場合は Assets フォルダ直下、Windowsで出力した場合は プロジェクト名_Data フォルダの直下に置きます。

FileLoader.cs

using System.IO;
using System.Text;
using UnityEngine;

public class FileLoader
{
    private readonly string filePath;
    public FileLoader()
    {
        filePath = Application.dataPath + @"/parts.json";
    }
    public MetaParts LoadFromJson()
    {
        if (File.Exists(filePath))
        {
      // ファイルの読み込み.
            using (var reader = new StreamReader(filePath, Encoding.UTF8))
            {
        // ファイルの内容は一括で読み込む.
                var readText = reader.ReadToEnd();
                if (readText.Length > 0)
                {
          // Jsonからの変換に関するエラーチェックは省略.
                    return JsonUtility.FromJson(readText);
                }
            }
        }
        return new MetaParts();
    }
}

あとはこのクラスを読んであげればOKです。

終わりに

Pythonを使うことで、さらっと処理が書けるのは良いと感じます。

ただ、C#でやるように常に型を意識して書きたいのならちょっと厳しいかもしれませんね。

その辺り、Pythonならではの便利なやり方というものがあると思うので、
それを身につけられたらな、と思います。

参照

Python

Unity

【Windows】Unityでコマンドライン引数の受け渡し

はじめに

ちょっと必要になったので。

UnityでWindows用に出力したexeファイルを開くときに、 合わせて引数となる文字列を受け取る(コマンドライン引数)ことで起動後の処理を変える方法。

そしてコマンドライン引数を指定してアプリを開く方法についてまとめます。

コマンドライン引数を受け取る

コマンドライン引数を受け取るには、ズバリ「System.Environment.GetCommandLineArgs()」を使います。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class MainController : MonoBehaviour
{
    public Text ResultText;
    private void Start () {
        var args = System.Environment.GetCommandLineArgs();
        var resultString = "";

        for (var i = 0; i < args.Length; i++)
        {
            resultString += i.ToString();
            resultString += ": ";
            resultString += args[i];
            resultString += ", ";
        }
        ResultText.text = resultString;
    }
}

配列の0番目にはexeファイルのパスが入り、1番目以降にコマンドライン引数が入ります。

またコマンドライン引数は半角スペースで区切られます。

コマンドライン引数を送る

「System.Diagnostics.Process」を使って別のアプリを開きます。

その際、コマンドライン引数を指定することができます。

using System.Diagnostics;
using UnityEngine;
using UnityEngine.UI;

public class MainController : MonoBehaviour
{
    public Button OpenProcessButton;
    private void Start () {
        OpenProcessButton.onClick.AddListener(() =>
        {
            var process = new Process
            {
                StartInfo =
                {
                    FileName = @"C:\Users\{{Exeファイルのパス}}\GetCommandlineArgs.exe",
                    Arguments = "arg arg2"
                }
            };
            process.Start();

        });
    }
}

上記のように引数を半角スペースで区切ると、受け取る側では2つの引数として認識されます。

参照

初心者(含む非開発者)向けGitによるバージョン管理資料メモ(Git Bash編)

はじめに

前回 Git GUIであれこれやってみたのですが、
なぜか会社のPCで Git GUI が正しく動かない(ファイルの変更が Rescan 後も Unstaged changes に表示されない)ため、
Git Bash を使って説明することにしました。

ということで、前回の内容をCUIで実行してみる、という内容となります。

準備

バージョン管理対象のプロジェクト作成

  1. バージョン管理対象のファイルを任意の場所に置いたフォルダに入れます。
  2. Git Bashを起動します。
  3. git init を実行します。
  4. 1.のフォルダ内に .git というフォルダが作成されます(表示から隠しファイルを表示する必要あり)。
  5. 4.のフォルダ内にこれから保存していく履歴情報が蓄積されることになります。

ユーザー情報などの登録

以下を実行します。

git config --global user.name "ユーザー名"
git config --global user.email メールアドレス

Commitする

まずはファイルを一つ作成して、バージョン管理してみることにします。

  1. ファイルをプロジェクトフォルダに置きます。
  2. Git Bash で git add . を実行します。
  3. git commmit -m “変更内容” を実行してCommmitします。

  4. 2.で、 . の代わりに特定のファイル名を指定することもできます。

  5. 3.で、 -m “変更内容” をつけない場合、実行後に Commit Message を入力するエディタ(デフォルトではVim? が開きます)。

履歴を見る

保存した内容を見てみます。

git log を実行することで、 Commit した日時や Commit Message を見ることができます。

なお、 git log で表示される履歴が4つ以上ある場合、
下記のような状態になり、そのままでは履歴表示モード?が終了できません。

その場合は q を押すと終了できます。

f:id:mslGt:20170901055427j:plain

また、それぞれの Commit 同士の差分を見るには、別途diffツールを用意する必要がありそうです。

バージョン管理しないフォルダ・ファイル

gitignoreの扱いは前回と同じです。

取り消し

直前のCommitを取り消したい場合

git revert HEAD

指定したCommitを取り消したい場合

例えば下記の一番上の Commit を取り消す場合

git revert 082822cfff3c4be617d01758e91a6722b4b6642d
  • ただし、例えば上から2番目のCommitに対して実行するとエラーとなりました。このような場合はオプションなどをつける必要があるかもしれません。

f:id:mslGt:20170901055451j:plain

2017.9.1 更新
Resetについて追記

Reset の場合は git reset を使います。

git reset --hard 082822cfff3c4be617d01758e91a6722b4b6642d

Branchを作る

Branchを作る

Branch を作成するには、以下を実行します。

git branch Branch名

Branchを切り替える

git checkout Branch名

master にマージする

checkoutして master ブランチに戻したあと、

git merge マージするBranch名

終わりに

やっぱり git を使うのはCUIの方が素直で良い気がしますね。。

参照

初心者(含む非開発者)向けGitによるバージョン管理資料メモ

はじめに

初心者(含む非開発者)の方に向けてGitを使ったバージョン管理のお話をすることになったので、
その資料を作るためのメモです。

目的としてはGitの使い方を覚える、というよりはZIPファイルや別名保存以外にも、
バージョン管理のためのツールを使う方法もあるんだよ~、ということを緩く紹介する感じで。

OSはWindowsが多いため Git For Windows に付属?している Git GUI を使うことにします(ある種これが僕にとっての試練w)。

準備

まずはインストールから。特に必要なければそのまま指示に従って進めればOKかと。

バージョン管理対象のプロジェクト作成

  1. バージョン管理対象のファイルを任意の場所に置いたフォルダに入れます。
  2. Git GUI を起動します。
  3. Create New Repository を選択して、1.のフォルダを指定します。
  4. 1.のフォルダ内に .git というフォルダが作成されます(表示から隠しファイルを表示する必要あり)。
  5. 4.のフォルダ内にこれから保存していく履歴情報が蓄積されることになります。

ユーザー情報などの登録

  1. Git GUI でバージョン管理対象のプロジェクトフォルダを開いた状態で、 Edit > Options を開きます。
  2. 左が今開いているプロジェクトの設定、右が共通(グローバル)の設定です。
  3. それぞれの User Name 、 Email Address を入力します。
  4. Default File Contents Encoding を(そうなっていなければ) utf-8 にします。
  5. ファイルエクスプローラーでバージョン管理対象のプロジェクトフォルダを開いて、右クリック > Git Bash Here でGit Bashを開き、以下を実行します(ファイル名やdiffの文字化け防止)。
git config --global gui.encoding utf-8

f:id:mslGt:20170830022318j:plain

Commitする

まずはファイルを一つ作成して、バージョン管理してみることにします。

  1. ファイルをプロジェクトフォルダに置きます。
  2. Git GUI で Rescan を押して、1.のファイルが Unstaged Changes に表示されたることを確認します。
  3. Stage Changed を押して、2.のファイルが Staged Changes に移動したことを確認します。
  4. Commit Message に変更内容を入力して、 Commit ボタンを押します。

履歴を見る

では保存した内容を見てみます。

今はmasterブランチなので、 Git GUI のメニューの Repository > Visualize master’s History を開きます。

変更内容や入力したメッセージ、変更した人(自分だけですが)の情報などを見ることができます。

変更のたびにCommitしていくことで、情報が溜まっていくことになります。

バージョン管理しないフォルダ・ファイル

プロジェクトのフォルダにバージョン管理しないフォルダ・ファイルが含まれる場合があります。

例えば以下のフォルダ、ファイルをプロジェクトのフォルダに置いて、
これをバージョン管理対象から外してみます。

  • IgnoreDirectory (フォルダ。中に IgnoreText2.txt というファイルを含む)
  • IgnoreText.txt

バージョン管理対象から外すには、下記のような内容で .gitignore というファイルをプロジェクトフォルダ直下に置きます。

.gitignore

IgnoreDirectory/*
IgnoreText.txt

この状態でCommitすると、 .gitignore のみが追加され、
IgnoreDirectory フォルダ、中の IgnoreText2.txt 、及び IgnoreText.txt は無視されます。

Unityプロジェクトの Temp フォルダ( Unity Editor 実行時に一時的に作成されるフォルダ)などは、
保存したりしようとするとエラーになるため、 .gitignore に含めると良いと思います。

取り消し

例えばCommit Messageの入力ミスなどでCommitを取り消したい場合。

先程と同じく Repository > Visualize master’s History から履歴を表示します。

で、取り消したいCommitを選択した状態で右クリック > Revert this commit で取り消すことができます。

過去のCommitを消すことも可能ですが、例えばテキストファイルを追加してCommit > 内容を変更してCommitした状態で、
前者のCommitを取り消そうとすると競合が発生し、エラーとなります。

最新のCommitから順番に戻すなどの作業が必要となります。

このRevertも履歴には残るのですが、完全にCommitを抹消したい場合は、
戻したいCommitを選択して右クリック > Reset master branch to here を選択します。

この時 Soft 、 Mixed 、 Hard が表示されます。

  • Soft: ファイルの変更内容はそのままで、Commitのみが取り消される
  • Mixed: ファイルの変更内容はそのままだが、Commitが取り消されて Stage Changed の状態も Unstage Changedに戻される
  • Hard: 変更内容ファイルも取り消され、Commit前の状態まで戻される

Resetのあとファイルを任意で変更するなどしてCommitを行うと、
それより先のCommitは削除されます。

Branchを作る

例えば比較的規模の大きい変更を試しに実装してみたい場合。

もちろんプロジェクトをまるごとコピー、という方法もあります。

しかし、 Branch を利用することで、履歴を複数に分割することができるため、
もとのファイルを( master に)残しながら変更後の内容をCommitし、
それをマージする、といったことが可能になります。

また、複数人で開発を行う場合、
それぞれの開発環境にブランチを作成して開発 > master にマージする、といったことが可能になります。

Branchを作る

Git GUI のメニュー > Branch > Create から作成が可能です。

Branchを切り替える

Git GUI のメニュー > Branch > Checkout から切り替えが可能です。

なおデフォルトのBranchは master です。

作成した Branch でファイルを変更してCommit > master にブランチを切り替えると、
master には変更が反映されていないことが確認できます。

f:id:mslGt:20170830022350j:plain

masterにマージする

作成したBranch (ここでは Branch1 とします)を master にマージするには、

  1. Branch1 で変更をCommitする
  2. master に切り替えて、 Git GUI のメニュー > Merge > Local Merge から、 Branch1 を選択する

ただし master と Branch1 の両方で同じファイルを変更していると、
競合が発生してエラーになります。

終わりに

とりあえず触りの部分だけ書いてみましたが、
資料を作成しつつ必要に応じて修正・追加する予定です。。。

参照

上海、蘇州に行ってきました

はじめに

8/12〜8/19にかけて上海、蘇州に行ってきました。

大まかなところはTwitterでつぶやいたりInstagramで画像を上げたりしまくったのですが、
それ以外に思ったことだったり、使ったVPNなどについてまとめておくことにします。

シェアサイクルについて

最近の中国絡みのニュースでよく話題になっているものの一つにシェアサイクルがあります。

実際、特に上海では至るところにシェアサイクル用の自転車が置かれていました。

www.instagram.com

使用方法としては、使用前に本体(だいたい鍵のある辺り)につけられたQRコードをアプリで読み取って、
鍵に数字が書かれたプッシュ式のボタンがついているので、それを押して解錠。

使用後はもう一度アプリでQRコードを読み取って終了。

金額は使用時間に対してかけられて、無料のものもある、といった感じのようです。

上海のシェアサイクルはどこでも乗り捨て可能、ということで、道を塞ぐくらい大量に自転車が置かれているところがあったり(´・ω・`)

www.instagram.com

どうしても乗り捨てられる場所には偏りが生じるため、夜間にニーズに合わせて?一部自転車を移動しているようです。

一方蘇州はというと、まだそこまで普及はしていないようで、自転車を見かけることが少なかったように思います。

また、見かけたものはどこでも乗り捨て可能なものではなく、専用の機械が用意されたものでした。

www.instagram.com

蘇州も地下鉄が通ったりどんどん便利になっているので、
今後シェアサイクルも広がっていくのかなぁ、といった感じがしました。

キャッシュレス決済について

もう一つ中国絡みのニュースで話題になっているものといえばャッシュレス決済です(私の中で)。

微信や支付宝を使った支払いにはQRコードを使用するのですが、
これが原因なのか街中至るところでQRコードを見かけました。

支払いができるところも格段に増えていて、
確かに現金無しで生活、というのも可能かもなぁ、と思わせられました。

www.instagram.com

ただキャッシュレス生活がそこまで便利かと言われると、正直まだ賛成しかねるところはあります。

支払いの時に支払い方法(微信なのか支付宝なのかなど)を伝えないといけなかったり、
レストランでの食事のとき、現金だとテーブルでそのまま支払い可能なものが(中国のレストランではレジではなくテーブルで支払うことが多いです)レジまで行かないといけなかったり。

この辺りは次回私が中国へ行く頃には解消されているのかもな〜、といった感じです。

あと現状で言うと、現金決済からキャッシュレス決済に移行している、というよりは現金決済に新しい決済方法が加わっている、といった印象を受けました。 (決済方法が増える方が便利になるので、消費者側としては良いことだと思います)

もう一つ、少し気になっているのが、ネットの記事などを見ていると、ホームレスがキャッシュレス決済での支払いに対応しているみたいな話がありますが、
セキュリティ的な危険はないのかなぁ、というところです。

せいぜい言った金額より多く取られるとかそれくらいで、あとはキャッシュレス決済のサービス提供側が安全にやり取りしてくれる感じなのでしょうか。

微信での支払いは、次回中国へ行くときには自分でも使ってみたいところです。

VPNについて

いつも中国へ行くと、TwitterGoogleのサービスが使えず大変厳しい気持ちになるのですが、
今回はVPNを試してみることにしました。

やっぱりGFWには皆困っているようで、VPNも結構種類がありましたが、
2017年のVPNのランキングで上位3位内に入っていて、Paypalが使えて、Linux版にも対応しているというExpressVPNというものにしました。

www.expressvpn.com

あと一ヶ月単位でライセンスを購入できるのも良いですね。

まぁ結局Linux版はやり方がまずかったのかうまく動作させられませんでしたが。。。
(インストール自体は問題なさそうでしたが、接続に失敗していました)

Androidについて言えば、時々つながりにくくなることはあったものの、特に問題なく接続できました。

多分つながりにくくなったのはGFWパワーのせいかと(; ・`ω・´)

VPNとGFWはいたちごっこ的なところはあるわけですが、次回中国に行くときも使えてそうなら、
またExpressVPNを使おうかな〜、という感じです。

おわりに

上海へ行くたびに思うのは、思うよりも変わっているけど思ったよりも変わってない、ということです。

ネットの記事などを見ていると、中国は日本より遥かに先を行っていてもう追いつけないみたいな気持ちにもなりますが、
そういう側面もあるけれども全てがそうでもない、とも思います。

それぞれ状況が違うわけで、そんな簡単には比較できないのではないでしょうか。

ということで、中国を見下すとか崇拝するとかではなく、良いところをどんどん取り入れて進歩していく、というのが良いのではないかな〜、というのが私の感想でした。

主語がでかくてすみませんm( )m

Angularのセキュリティ対策について調べてみる その4

はじめに

続きです。

特定のURLでCSRF(XSRF)対策を無効にする方法、ドキュメントのCross-site script inclusion (XSSI)について追いかけてみました。

特定のURLでCSRF対策を無効にする

前回は全ページでログインしていないとPostリクエストが送れない状態でしたが、
これでは少し不便ですね。

基本的にCSRFの対策はアカウントのパスワード変更など、ログインした状態で行う処理に対して実行するもの、
ということを考えると、例えばログインしていなくても送信したい情報がある場合、
そのページではCSRFを無効にして未ログイン状態でもPostリクエストを送信できるようにする必要があります。

http.csrf().disable()を使う(失敗)

下記のように「http.csrf().disable()」を使ってCSRF対策を無効にしようとすると、 全ページで無効になったり、「http.csrf().disable()」が無視されてしまいます。

WebSecurityConfig.java

〜省略〜
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    〜省略〜
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .httpBasic().and()
                .authorizeRequests()
                .antMatchers("/menulist").authenticated()
                .and().formLogin()
                .and().csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .and().exceptionHandling()
                .accessDeniedPage("/accessdenied.html");

        http
                .authorizeRequests()
                .antMatchers("/").permitAll()
                .and().csrf()
                .disable();
    }
    
}

http.csrf().ignoringAntMatchers()

じゃあどうするか、というと、「http.csrf().ignoringAntMatchers()」を使用します。

WebSecurityConfig.java

〜省略〜
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    〜省略〜
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .httpBasic().and()
                .authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/menulist").authenticated()
                .and().formLogin()
                .and().csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .and().exceptionHandling()
                .accessDeniedPage("/accessdenied.html");

        http
                .csrf()
                .ignoringAntMatchers("/postsample");
    }
    
}

ここで注意が必要なのは、「ignoringAntMatchers()」で指定するURLはページのURLではなくPostリクエストの送信先のURLであるということです。

そのため、CSRFを有効にするページとそうでないページとでPostリクエストの送信先が一緒になっていると、
両方でログインを求められたりCSRF対策が無効になってしまったりします。

まぁ適当にコピペしているのでもなければ、通常そんなことは無いだろうとは思いますがorz

X-XSRF-TOKENについて

CSRF対策用のトークンなどは、Spring boot側でCSRF対策が有効・無効であるかにかかわらず、常に含まれるようです。

f:id:mslGt:20170804005758j:plain

f:id:mslGt:20170804005810j:plain

ただし、トークンの値はログインのタイミングで更新されるため、

  1. ログイン前にCSRF対策を無効にしたPostリクエストを送信する
  2. ログインして、CSRF対策を有効にしたPostリクエストを送信する
  3. ログイン状態のままCSRF対策を無効にしたPostリクエストを送信する

とすると、2と3で同じトークンが渡され、1とは異なる値になっています。

IDの固定化攻撃の対策も自動で行ってくれる、ということですね。すごい!

Cross-site script inclusion (XSSI)

JSONから重要なデータを読み取られる脆弱性、ということですが、
JSONとしてサーバー側から受け取った値がそのままJavascriptとして実行できる場合にのみ発生するようで、
XSSなどと比較すると常に気をつけなければいけない、という訳でもないのかしら。。。?
(すみません。イマイチ内容が理解できていません)

とりあえず、このXSSIを防ぐ方法としてサーバー側で冒頭に「)]}」や「\n(改行コード)」をJSONデータのプレフィックスとして付与するというものがあり、
AngularのHttpClientではそれらのプレフィックスをJSONの解析前に削除する、というのが本項目の内容です。

おわりに

一通りAngularのSecurityのページを眺めてみました。

軽く触っただけのところなど、気になる部分もありますが、
次からはSpring Security側の設定なども含めて、実際に何かWebアプリでも作りつつ試していくことにしたいと思います。

ブログはせっかくここに書き溜めているわけですし(なお質は問わないものとする)、
PCやスマホからメモを残したりスケジュールを追加したりできるようなものを作ってみようかな?
(できるとは言っていない)

参照

CSRF

XSSI

Angularのセキュリティ対策について調べてみる その3

はじめに

続きです。

今回はサニタイズせずに値を渡す方法、CSRF (XSRF) などを追いかけてみます。

Trusting safe values

以前Templateを使ってHTMLタグを挿入する場合、自動でサニタイズされることを学びました。

では信頼したHTMLなどの値を、サニタイズせずにページに反映するにはどうすべきか。

DomSanitizerのbypassSecurityTrustHtmlやbypassSecurityTrustUrlを使用します。

  • bypassSecurityTrustHtml
  • bypassSecurityTrustScript
  • bypassSecurityTrustStyle
  • bypassSecurityTrustUrl
  • bypassSecurityTrustResourceUrl

bypass-security.component.ts

import { Component, OnInit } from '@angular/core';
import { DomSanitizer, SafeHtml, SafeUrl } from '@angular/platform-browser';

@Component({
  selector: 'app-bypass-security',
  templateUrl: './bypass-security.component.html',
  styleUrls: ['./bypass-security.component.css']
})
export class BypassSecurityComponent implements OnInit {
  // 元の文字列. そのままTemplateに渡すとサニタイズされる.
  private dangerousHtml: string;
  private dangerousUrl: string; 

  // 信頼された値としてサニタイズされずに表示される.
  private trustedHtml: SafeHtml;
  private trustedUrl: SafeUrl;

  constructor(private sanitizer: DomSanitizer) { }

  ngOnInit() {
    this.dangerousHtml = '< button onclick="alert(0)">Click me!< /button>';
    this.dangerousUrl = 'javascript:alert("Hi there")';
    
    this.trustedHtml = this.sanitizer.bypassSecurityTrustHtml(this.dangerousHtml);
    this.trustedUrl = this.sanitizer.bypassSecurityTrustUrl(this.dangerousUrl);
  }
}

bypass-security.component.html

< h4>An untrusted HTML:< /h4>
< iframe [srcdoc]='dangerousHtml'>< /iframe>
< h4>An trusted HTML:< /h4>
< iframe [srcdoc]='trustedHtml'>< /iframe>
< h4>An untrusted URL:< /h4>
< p>< a [href]="dangerousUrl">Click me< /a>< /p>
< h4>A trusted URL:< /h4>
< p>< a [href]="trustedUrl">Click me< /a>< /p>

これを実行すると以下のようになります。

f:id:mslGt:20170727071523j:plain

trustedHtml、trustedUrlの値がサニタイズされずに表示されているのが確認できます。

{{trustedHtml}}

さて、以前確認していたように、 < p>{{trustedHtml}}< /p> とするとどうなるでしょうか。

bypass-security.component.html

〜省略〜
< p>{{trustedHtml}}< /p>

結果は下記のようなメッセージが表示されます。

SafeValue must use [property]=binding: < button onclick="alert(0)">Click me!< /button> (see http://g.co/ng/security#xss)

つまり、{{trustedHtml}}として表示した場合は値を信頼している・していないに関係なくHTMLが反映されず、
文字列でHTMLのソースを渡して表示したい場合は、iframeなどを使って信頼した値として渡してね、ということのようです。

HTTP-level vulnerabilities

さて、ここからはサーバー側のお話となります。

当然サーバー側の対応はAngularではなく(今回は)Spring bootでの対応となるわけですが、
Cross-site request forgery (CSRF or XSRF) と Cross-site script inclusion (XSSI) についてはサーバー側の処理を補助する仕組みがあるとのことです。

エラーページ

完全に話がずれてしまいますが、引っかかったので書き残しておきます。

前回のCSPの話の中で、「WebSecurityConfig」というクラスを作ってURLごとに閲覧権限を付与していましたが、 例えば必要な権限を持たない状態でページにアクセスすると、Spring Securityがデフォルトで提供する(と思う)エラーページが表示されます。

これを自分で用意する、という場合についてです。

WebSecurityConfig.java

〜省略〜
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .withUser("user1")
                .password("1234")
                .roles("USER");
        auth
                .inMemoryAuthentication()
                .withUser("user2")
                .password("1234")
                .roles("ADMIN");
    }
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/menulist").hasAuthority("ADMIN") 
                .and().formLogin()
                .and().exceptionHandling()
                .accessDeniedPage("/accessdenied.html");
    }
}
  • 上記ではLogin画面はデフォルトのものが表示されますが、「formLogin()」のあとに「.loginPage(“/login”)」のようにログインページのパスを指定することで、
    (ルーティングはControllerクラスで実行)自分で用意したログインフォームを表示できます。
  • 上記では未ログイン時はまずログインフォームが表示され、ログイン後にAdmin権限を持っていない場合(つまりuser1でログインした場合)は「accessDeniedPage()」で指定したページが表示されます。

失敗1: ログインフォームを表示していない

WebSecurityConfig.java

〜省略〜
@Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/menulist").hasAuthority("ADMIN") 

                .and().exceptionHandling()
                .accessDeniedPage("/accessdenied.html");
    }
  }

上記のように、ログインフォームを表示する「.and().formLogin()」を入れずにaccessDeniedPageを指定したところ、
デフォルトのエラーページしか表示されませんでしたorz (未ログイン状態でアクセスして確認)

実際の使用ではまずログインさせるであろう、ということで、
特に問題は無いのですが、ちゃんとドキュメント等を読まずに適当にやると失敗する、という例でした。

失敗2: AccessDeniedPageの表示にAngularのルーティングを使おうとした

WebSecurityConfig.java

〜省略〜
@Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/menulist").hasAuthority("ADMIN") 
                .and().formLogin()
                .and().exceptionHandling()
                .accessDeniedPage("/accessdenied");
    }
  }

以前トライしたように、アクセス拒否のエラーでもAngularのルーティング機能を使ってページを表示しようとしたところ、
Admin権限でないと見られないはずの「/menulist」のページまで一般ユーザー権限のユーザーが閲覧できるようになってしまいましたorz

Spring boot側で「/accessdenied」に切り替える前に、Angularで「/menulist」を表示してしまうため、
ということなのでしょうか。

まぁそもそもエラーページは通常起こり得ない異常が発生したときに表示するものだと考えれば、
通常通り処理しようとするな、ということでもありますorz

ログインやログインフォームなどについては別途詳しく追いかけてみたいと思います。

Cross-site request forgery (CSRF or XSRF)

さて、元の話題に戻ります。

CSRFの説明は安全なウェブサイトの作り方の1.6などを参照していただけたらと思いますが、
簡単にまとめると

ユーザーが ①のページ を開いてログインを行い、ログインした状態のまま別のタブなどで ②罠の仕掛けられたページ を開いた場合に、
②のページから①のページに対し、ログイン済みの情報を利用して不正なリクエストを送ることができてしまう、という問題です。

この対策として、以下のような方法があります。(防ぎたいリクエストの種類にもよりますが)

  1. サーバー側とクライアント側でセッションID、乱数などのトークンを共有・確認する
  2. ユーザーに再ログインを求める
  3. CAPTCHA機能を利用する
  4. Referer情報から正しいURLから遷移したかを確認する

2、3はパスワードなどの変更や入出金などを行う時に見かけるように思います。

で、今回は1について追いかけてみることにします。

Angular

下記によると、AngularのHttpClient(後述しますが、正確にはHttpClientXsrfModule)にはCSRF(XSRF)の対策としてトークンを発行する機能があります。

クッキーの値は同じドメインのコードからしか読めないことから、そのリクエストが正しくユーザーからのものであり、
攻撃者からではないことが確認できます。

とのことです。

確認してみましょう。

Postリクエストを送信してみる(失敗)

ということで、Postリクエストを送信してみてどのようなHTTPヘッダにデータが渡されているかを見てみることにします。

まずはAngular側から。

post-page.component.ts

import { Component, OnInit } from '@angular/core';
import { Http } from '@angular/http';

@Component({
  selector: 'app-post-page',
  templateUrl: './post-page.component.html',
  styleUrls: ['./post-page.component.css']
})
export class PostPageComponent implements OnInit {

  constructor(private http_: Http) { }

  ngOnInit() {
    this.http_.post("/postsample", {id: 0, createdDate: "2017年6月24日", title: "ニュースだよ", article: "中身です"})
      .subscribe();
  }
}
  • 実際にはせっかく作成したinterface(top-news.ts)を利用したり、ボタン操作によって送信を行いたいところですが、
    今回は確認作業の簡略化のためngOnInitで実行しています。
  • Http.postの戻り値は「Observable」であり、実行には「subscribe」が必要です。

あとは任意のページからpost-page.componentが表示・実行されるようにしておけばOKです。

次にSpring bootです。
RestControllerクラスにPostメソッド用の処理を追加します。

import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;

@RestController
public class CrudSampleController {
    
    〜省略〜

    @PostMapping("/postsample")
    public TopNews postsample(@RequestBody TopNews news){
        System.out.println(news.article);
        return news;
    }
}
  • こちら側も今回は確認のため、特に何も処理を行わずに返しています。

さてこれで準備はOKです。
確認してみましょう!!

404エラーになりましたorz

Postメソッドの送信先が見つからない原因

ズバリSpring SecurityのCSRF対策のための機能です/(^o^)\

そのため、下記のようにCSRF対策を無効にすると正しくアクセスできます。

WebSecurityConfig.java

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

〜省略〜

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/menulist").hasAuthority("ADMIN")
                .and().formLogin().loginPage("/login")
                .and().exceptionHandling()
                .accessDeniedPage("/accessdenied.html");


       // CSRF対策を無効にする.
       http
                .csrf()
                .disable();

    }
}

これで不正アクセスしようとする攻撃者の気持ちが体験できたわけですね/(^o^)\

…というところでここからが本題です。

Angularでトークンを発行する

HttpClientXsrfModule を使って、CSRF対策のためのトークンを発行できます。

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { HttpClientXsrfModule } from '@angular/common/http'

import { AppComponent } from './app.component';
import { PostPageComponent } from './post-page/post-page.component';

@NgModule({
  declarations: [
    AppComponent,
    PostPageComponent
  ],
  imports: [
    BrowserModule,
    HttpModule,
    
    HttpClientXsrfModule.withOptions({
      cookieName: 'My-Xsrf-Cookie',
      headerName: 'My-Xsrf-Header',
    }),
    
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

この後後述のSpring boot側でAngularのトークンを使う設定を行うと、
Postリクエスト送信時のRequestHeaderで「XSRF-TOKEN」(Cookie)、「X-XSRF-TOKEN」というトークンが付与されます。

ここで一点注意があります。

HttpClient - Angular などでは「HttpClientXsrfModule.withConfig」としてcookieNameなどを設定していますが、
「withOptions」でないとエラーになります。

仕様変更があったのでしょうか。
HttpClientXsrfModule - Angularでは「withOptions」になっていますね。

ドキュメントの修正依頼ってプルリクになるのでしょうか。。。?

Spring bootでAngularのトークンを使うようにする

Spring bootでAngularのトークンを使用し、Postリクエストを有効にします。

WebSecurityConfig

〜省略〜
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .withUser("user1")
                .password("1234")
                .roles("USER");
        auth
                .inMemoryAuthentication()
                .withUser("user2")
                .password("1234")
                .roles("ADMIN");
    }
    @Override
    public void configure(HttpSecurity http) throws Exception {
        
        http
                .httpBasic().and()
                .authorizeRequests()
                .antMatchers("/").permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .and().csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
                
    }
}
  • トークンの指定は「csrf()」以降です。
  • Angularのトークンを使用するためには、CookieのHttpOnlyはFalseにしておく必要があるようです。
    「.csrfTokenRepository(new CookieCsrfTokenRepository)」のようにすると403エラーが発生しました。
  • 上記ではPostリクエストを送信する前にログインが必要で、「.anyRequest().authenticated()」を、
    「.anyRequest().permitAll()」にすればいけるのでは?と思いましたが、403エラーが発生しましたorz

おわりに

CSRF対策についてはとりあえずPostリクエストを送信できるようになりましたが、
現状だと全てのページでログインを行うか、CSRF対策を無効にする必要が出てきてしまいます。

次回は特定のページでのみCSRF対策を有効にし、ログインしていなくてもPost送信ができるようにする方法を調べてみます。

また、Angularのドキュメントの最後の項目、Cross-site script inclusion (XSSI)についても追いかけてみます。

参照

BypassSecurity

Spring Security

CSRF (XSRF)