技術とか戦略とか

SIerで証券レガシーシステムを8年いじってからSESに転職した普通の文系SEによる技術ブログ。

SpringFramework:「application.properties」の定義を漏らすことで発生するバグ

SpringFrameworkで、JavaやHTMLのコードを読んでも原因がわからないバグは、「application.properties」の定義漏れで引き起こされることがあります。
 
例えば、「spring.messages.basename」の定義が漏れていると、「messages.properties」のようなファイルに定義するようなメッセージを取得できません。
この場合、画面上にアスタリスク2つに挟まれているような表示になります(具体的には「?? userId ??」のような表示)。
 
また、「mybatis.type-aliases-package」の定義が漏れていると、MyBatisによるDIができなくなります。
この場合、select文の結果を取得する時に、取得結果をどのオブジェクトに詰めるのは判定できなくなり、そこで落ちます。

ハッシュ化(暗号化)におけるソルトとは

この記事では、ハッシュ化で使われる「ソルト」について、説明していきます。
どちらかと言うと初心者向けです。
 
----
 
【ハッシュ化とは】
ハッシュ化とは、与えられた文字列を特定の方式(アルゴリズム)に従って変換することです。
変換後の文字列から変換前の文字列に戻すことは困難であるため、変換前の文字列を知られたくない場合に用いられます。
具体的には、パスワードを使用した認証処理(ログイン処理)で使われることが多いです。
 
例えば、「SHA-512」と呼ばれる方式では、「password」という文字列は以下のように変換されます。
password -> B109F3BBBC244EB82441917ED06D618B9008DD09B3BEFD1B5E07394C706A8BB980B1D7785E5976EC049B46DF5F1326AF5A2EA6D103FD07C95385FFAB0CACBC86
 
【認証処理におけるハッシュ化の使い方】
認証処理においては、パスワードを管理するファイルやデータベースに、ハッシュ化された形でパスワードが保持されます。
認証を行う際には、ユーザーから入力されたパスワードをハッシュ化し、ファイルやデータベースに保持されているハッシュ化された文字列と一致することを確認することで妥当性を確認します。

f:id:akira2kun:20210808123018j:plain
 
なお、システムの管理者もパスワードの平文を知ることができないので、ユーザーがパスワードを忘れてしまった場合はパスワードを再作成する運用を行います。
 
【レインボーテーブルによる攻撃】
不正ログインを試みる攻撃者は、何らかの方法でハッシュ化されたパスワードの一覧を入手した後、レインボーテーブルによる攻撃を試みることがあります。
レインボーテーブルとは、平文とハッシュ化後文字列の対応表のことを指し、この対応表を用いることでハッシュ化後文字列から平文を推測することができます。
例えば、以下のような対応表がレインボーテーブルです。全ての平文について対応表を作ることは困難ですが、良く使われる平文はこれで突破されてしまいます。
 
password -> B109F3BBBC244EB82441917ED06D618B9008DD09B3BEFD1B5E07394C706A8BB980B1D7785E5976EC049B46DF5F1326AF5A2EA6D103FD07C95385FFAB0CACBC86
1234 -> D404559F602EAB6FD602AC7680DACBFAADD13630335E951F097AF3900E9DE176B6DB28512F2E000B9D04FBA5133E8B1C6E8DF59DB3A8AB9D60BE4B97CC9E81DB
123456 -> BA3253876AED6BC22D4A6FF53D8406C6AD864195ED144AB5C87621B6C233B548BAEAE6956DF346EC8C17F5EA10F35EE3CBC514797ED7DDD3145464E2A0BAB413
:
:
:
 
【ソルトによる対策】
ここでソルトの登場です。
ソルトとは、平文からハッシュ化を行う前に、平文に付加される文字列のことを指します。
ソルトは、平文の前に付加されても後に付加されても構いません。
ソルトを付加することにより、レインボーテーブルによる攻撃で平文のパスワードを推測されることを防ぎやすくなります。
 
以下は、「password」という平文の後ろに「LdCTFPMk」という平文を追加する例です。
ハッシュ化後文字列が全く違うものになり、レインボーテーブルで逆引きすることが困難になります。
 
passwordLdCTFPMk -> 461708CDFAA6629C9C79F935F153A6F5DEC7C32D300E192AFE1076192B68C92F888390522D812CD52B564838F506BED957193131FF1DC975B7EE04EC6CD54A50
 
なお、ソルトに用いる文字列は、複雑で長い文字列(例えばランダムで生成された文字列)、かつユーザー毎で使い分ける(ログイン時に入力されたユーザーから対応するソルトを取得する処理を別途作成する)ことが望ましいです。

JavaScript:未宣言・var・letの変数の挙動の違い

JavaScriptにおける変数の宣言方法は、constを除くと以下の4つがあります。
 
・未宣言
 var1 = "var1";
 のように、値が代入された時点で変数が宣言されたものとみなされる。
 
・var
 var var1;
 のように、var句により変数を宣言する。
 
・var(巻き上げ)
 var1 = "var1";
 var var1;
 のように値が代入された後にvarにより変数が宣言された場合、
 変数が宣言された後に値が代入されたものとみなされる。
 この例では、以下と同価とみなされる。
 var var1;
 var1 = "var1";
 
・let
 let var1;
 のように、var句により変数を宣言する。
 ES6のバージョン以降でサポートされている。
 
これらの宣言方法の違いにより、非Strictモードの場合に以下の挙動の違いが生じます。
(varの巻き上げについては、巻き上げしなかった場合と同じです)

f:id:akira2kun:20210807165734j:plain
 
なお、Strictモード(https://cyzennt.co.jp/blog/2021/04/01/javascript%EF%BC%9Astrict%E3%83%A2%E3%83%BC%E3%83%89%E3%81%A8%E3%81%AF/)の場合は、varやletやconstを用いて宣言をせずに変数に値を代入した場合にReferenceErrorで異常終了するようになります。
 
----
 
以下、挙動を確認するためのサンプルコードです。
(Node.jsで確認します)
 
それぞれの変数で以下を確認します。
 
・var1
 ブロック外で宣言した変数をブロック内でも宣言。
 ブロック内の変更がブロック外に影響するなら、
 ブロックを出た時点で変数が書き変わる。
 
・var2
 ブロック内でのみ変数を宣言。
 ブロック内の変更がブロック外に影響するなら、
 ブロックを出た時点で参照不可になる。
 
・var3
 関数内でのみ変数を宣言。
 関数内の変更が関数外に影響するなら、
 関数を出た時点で参照不可になる。
 
【テストコード(未宣言)】
・test.js
var1 = "var1";
console.log("before change :" + var1);

if (true) {
  var1 = "var1mod"
  var2 = "var2";
  console.log("in block    :" + var2);
}
console.log("after change  :" + var1);
console.log("out of block  :" + var2);

function func() {
  var3 = "var3";
  console.log("in function  :" + var3);
  return var3;
}
func();
console.log("out of function:" + var3);
 
【実行結果(未宣言)】
c:\tmp>node test.js
before change :var1
in block    :var2
after change  :var1mod
out of block  :var2
in function  :var3
out of function:var3

c:\tmp>
 
【テストコード(var)】
・test.js
var var1 = "var1";
console.log("before change :" + var1);

if (true) {
  var var1 = "var1mod"
  var var2 = "var2";
  console.log("in block    :" + var2);
}
console.log("after change  :" + var1);
console.log("out of block  :" + var2);

function func() {
  var var3 = "var3";
  console.log("in function  :" + var3);
  return var3;
}
func();
// console.log("out of function:" + var3); // ReferenceError
 
【実行結果(var)】
c:\tmp>node test.js
before change :var1
in block    :var2
after change  :var1mod
out of block  :var2
in function  :var3

c:\tmp>
 
【テストコード(var巻き上げ)】
・test.js
var1 = "var1";
var var1;
console.log("before change :" + var1);

if (true) {
  var1 = "var1mod"
  var var1;
  var2 = "var2";
  var var2;
  console.log("in block    :" + var2);
}
console.log("after change  :" + var1);
console.log("out of block  :" + var2);

function func() {
  var3 = "var3";
  var var3;
  console.log("in function  :" + var3);
  return var3;
}
func();
// console.log("out of function:" + var3); // ReferenceError
 
【実行結果(var巻き上げ)】
c:\tmp>node test.js
before change :var1
in block    :var2
after change  :var1mod
out of block  :var2
in function  :var3

c:\tmp>
 
【テストコード(let)】
・test.js
let var1 = "var1";
console.log("before change :" + var1);

if (true) {
  let var1 = "var1mod"
  let var2 = "var2";
  console.log("in block    :" + var2);
}
console.log("after change  :" + var1);
// console.log("out of block  :" + var2); // ReferenceError

function func() {
  let var3 = "var3";
  console.log("in function  :" + var3);
  return var3;
}
func();
// console.log("out of function:" + var3); // ReferenceError
 
【実行結果(let)】
c:\tmp>node test.js
before change :var1
in block    :var2
after change  :var1
in function  :var3

c:\tmp>

java:読み込んだ文字列を後ろから解釈する

ファイルから読み込んだ文字は前から解釈することが多いですが、ある程度のまとまり(例えば1行)を読みこんでから後ろから解釈した方が楽な場合があります。
例えば、項目数が可変の場合に後ろの項目を取得するような場合に楽に処理できます。
 
今回は、以下のCSVファイルから、ユーザー名(3項目目)を読みこむ例を考えてみます。
 
【ファイルフォーマット】
1項目目…連番(7文字固定の数値)
2項目目…コメント(改行文字は含まない、カンマ(区切り文字)はエスケープされない)
3項目目…ユーザー名(改行文字やカンマは含まれない)
 
サンプルコードは以下のようになります。
1行読み込んで後ろから文字列を組み立てている所がポイントです。
 
【サンプルコード】
・FileReadWriteMain3.java
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

public class FileReadWriteMain3 {

  public static void main(String[] args) {

    // 1バイトずつ読込(テキストファイルとして処理)
    BufferedReader br = null;
    BufferedWriter bw = null;

    try {
      // 入出力ファイルパス
      br = new BufferedReader
        (new InputStreamReader
        (new FileInputStream("C:\\tmp\\test.csv"),"MS932"));
      bw = new BufferedWriter
        (new OutputStreamWriter
        (new FileOutputStream("C:\\tmp\\test_out.csv"), "MS932"));


      // 読み込んだ箇所を保持
      StringBuilder sb = new StringBuilder();

      // データ読み込みループ
      while (true){

        // 1行読み込み
        String str = br.readLine();
        if (str == null) {
          break;
        }

        // 読み込んだ行を後ろから解釈
        sb.delete(0, sb.length());
        for(int i = str.length() - 1; i >= 0; i--) {
          if (String.valueOf(str.charAt(i)).equals(",")) {
            // 3項目目の終わりとして判定しループを抜ける
            break;
          } else {
            // 3項目目(ユーザー名)を組み立てる
            sb.insert(0,str.charAt(i));
          }
        }

        // 組み立てたユーザー名を出力
        bw.write(sb.toString());
        bw.newLine();

      }

    // 例外処理
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      try {
        br.close();
        bw.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }

  }

}
 
【処理結果】
・インプットファイル
0000001,コメントの中にカンマ,がある,hoge
0000002,コメント,fuga
 
・アウトプットファイル
hoge
fuga

情報処理技術者試験対策「非機能要件」

経験が浅い技術者は、システムを構築する際、ユーザーの目から見える機能(機能要件)を満たすことのみを考えがちです。
しかし、システムが価値を生み出すためには、システムが安定的に運用される必要があります。
システムの安定運用を実現するための要件を、「非機能要件」と呼びます。
 
非機能要件は、国際規格ISO/IEC 9126(JIS X 0129)においては、「機能性」「信頼性」「使用性」「効率性」「保守性」「移植性」の6つの特性に分類できます。
これらの分類を詳しく書くと以下の通りです。
 

f:id:akira2kun:20211001002747j:plain

 
----------------------
 
なお、試験対策としては、6つの特性とその概要さえ覚えておけば十分です。
実務に従事する上では、6つの特性とその概要を理解した上で、副特性に書かれている観点を網羅的に見て非機能要件を洗い出すことが重要です。
(現場に要件定義書のテンプレートがあるのであれば、その現場のテンプレートに従って非機能要件を記述すれば良いです)
 
----------------------
 
情報処理技術者試験に関する記事の目次
https://1drv.ms/b/s!AivF3bzWXOzuhG1Xk5hscKYqkLkM

java:ファイルを1バイトずつ読み込んで解釈する例

 ファイルを1バイト/1文字ずつ読み書きすることでファイルの中身を解釈しながら処理することができる、というのは以前の記事で書いた通りです。
今回の記事では、複雑なフォーマットのファイルを例に出して説明したいと思います。
 
----
 
今回の例では、以下のCSVファイルがインプットであると仮定します。
要件は、2項目目のみを抜き出して表示する、とします。
 
【ファイルフォーマット】
1項目目:連番
2項目目:コメント(半角カンマは全角カンマにエスケープ済み、改行文字を含む)
3項目目:"END"+改行文字(CRLF)固定
 
項目中に改行文字が含まれるため、単純に1行読んでカンマで分割するという方法では処理できないのですが、先頭から1バイト(1文字)ずつ読み込むことで処理が可能になります。
 
----
 
【サンプルコード】
・FileReadWriteMain2.java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileReadWriteMain2 {

  public static void main(String[] args) {

    // 1バイトずつ読込(バイナリファイルとして処理)
    FileInputStream fis = null;
    FileOutputStream fos = null;

    try {
      // 入出力ファイルパス
      fis = new FileInputStream("C:\\tmp\\test.csv");
      fos = new FileOutputStream("C:\\tmp\\test_out.csv");

      // 状態保持
      int state = 0; // 0…1項目目読み込み中(読み飛ばし)
              // 1…2項目目読み込み中(出力)
              // 2…3項目目読み込み中(読み飛ばし)

      // 3項目目文字列
      StringBuilder sb = new StringBuilder();

      // 定数群
      // UTF-8SHIFT_JISEUC-JPではマルチバイト文字で0x2cは使わない
      // そのため、判定時にマルチバイト文字の考慮は不要
      final String commaStrHex = "2c"; // カンマ
      final String endStrHex = "454e440d0a"; // END + CRLF

      // データ読み込みループ
      int data;
      while ( (data = fis.read() ) != -1){

        // 条件判定のため、読み込んだ1バイトのデータを変換
        // 10進数 → 16進数
        String hex = Integer.toHexString(data);
        if (hex.length() == 1) {
          hex = "0" + hex;
        }

        // 状態毎に処理内容を変化
        if (state == 0) { // 1項目目
          // カンマが来たことを判定
          if (hex.equals(commaStrHex)) {
            state = 1; // 以降は2項目目
          }
        } else if (state == 1) { // 2項目目
          // カンマが来たことを判定
          if (hex.equals(commaStrHex)) {
            // ファイルにCRLFを書き込み(レコード終わり)
            fos.write(13);
            fos.write(10);
            state = 2; // 以降は3項目目
          } else {
            // ファイルに書き込み
            fos.write(data);
          }
        } else if (state == 2) { // 3項目目
          // 判定のため直近5バイト(16進数で10桁)を保持
          // 既に5バイト持っている場合は最初の1バイトは消す
          if (sb.length() == 10) {
            sb.delete(0, sb.length() - 8);
          }
          sb.append(hex);
          // END + CRLFが来たことを判定
          if (sb.toString().equals(endStrHex)) {
            state = 0; // 1項目目に戻る
          }
        }

      }

    // 例外処理
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      try {
        fis.close();
        fos.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }

  }

}
 
【処理結果】
・インプットファイル
0000001,コメント
です。,END
0000002,comment,コメント,END
 
・アウトプットファイル
コメント
です。
comment,コメント

システムの概要を示す設計書が本当に必要な設計書である

表題の通りですが、システム開発で本当に必要な設計書は、システムの概要を示す設計書です。
そのような設計書があれば、実装を知らない立場の人(例えば要件定義担当や上位の設計者)との意思疎通がスムーズになりますし、開発者を新たに向かい入れる時にも実装の内容をスムーズに理解してもらえるようになります。
 
----
 
例として、在庫確認モジュールを開発しているとします。
具体的な実装としては以下のイメージとします。
実際には、「:」の箇所に数行~数十行のコードが書かれているイメージです。
 
【実装イメージ】
// インプットのオブジェクトの生成(全商品リスト)

// アウトプットのオブジェクトの生成(要発注リスト、確認済リスト)

// 現時点の全商品リストの取得

// 商品の発注点の計算(全商品リストを更新)

 // 種類Aの商品の場合
 :
 :(複雑なビジネスロジック
 :
 // 種類Bの商品の場合
 :
 :(複雑なビジネスロジック
 :
 // 以下同じような処理
 :
 :
 :
 :
 :
 :
// 発注が必要かどうかの判定

 // もし必要なら要発注リストへ当該商品を出力
 :
// 全商品を確認済リストへ出力

// オブジェクトクローズ

 
----
 
良くない設計書の例としては、以下のようなものがあります。
(残念ながら、実務でも見かけることがあります)

ソースコードと1対1対応の設計書
 ソースコードを1行1行日本語に訳したような設計書は良くありません。
 そのような設計書は記述量が多くなり、
 作成するのも大変ですし、それを読んで内容を理解するのも大変です。
 また、日本語(自然言語)は意味が曖昧になりがちなので内容も不正確になります。
 更に、ソースコードに些末な変更を行う度に、
 設計書の更新も必要になるという問題もあります。
 (設計書の更新を忘れると、設計書を信用できなくなる)
 
・見た目だけで内容に乏しい設計書
 経験が浅く見様見真似で設計業務に当たっている場合、
 以下の絵のような見た目だけの内容に乏しい設計書を作成しがちです。
 (この例の絵は少し誇張しすぎですが)

f:id:akira2kun:20210718190255j:plain
 しかし、設計書はかっこ良い所を見せる資料ではなく、
 実装に直接的・間接的に関わる人と意思疎通を取るための資料なので、
 実装内容が早く正確に伝わればそれで良いです。
 どのような設計書を作れば良いかイメージが付きにくい場合は、
 現現場の既存の設計書や他現場の設計書を参考にしたり、
 先輩社員に聞いたりすると良いでしょう。
 
----
 
 システムの概要が早く正確に伝わる設計書が良い設計書です。
 例えば、以下のような処理概要を掴めるフローチャートは良い設計書です。

f:id:akira2kun:20210718190357j:plain

 このような設計書があれば、
 要件定義担当や上位の設計者が全体像をつかみやすくなり、
 レビュー(要件や全体の設計の観点から見て問題がないことの確認)が捗ります。
 また、新たに開発者を向かい入れる場合にも、
 モジュールの全体像を掴んでもらってから、
 必要に応じて細かい箇所を確認してもらうことができるようになります。