セッションベース認証まとめ

セッションベース認証について調べたことをまとめます。

セッションベース認証 概要

セッションベース認証は、主にセッションIDを用いて行う認証方式です。 基本的なフローは以下です。 まずユーザが認証画面で認証情報を入力しwebサーバに情報を送ります。webサーバは認証情報をチェックし、正しければセッションIDというランダムな文字列を生成しユーザ情報と紐付けます。それをクライアント(ブラウザ)に返し、以降のやり取りにセッションIDを含めることでログイン状態を維持します。セッションIDのやり取りは主にCookieを使います。

Cookieとは

Cookieとは、Webサーバがクライアント(ブラウザ等)に預けておく小さなファイルのことを指します。 ブラウザ・サーバ間でCookieをやり取りする際はHTTPのリクエストヘッダ(Cookieフィールド)・レスポンスヘッダ(Set-Cookie)に格納することでCookieのやりとりを行います。

具体的な動きを見てみます。

f:id:surfi8000:20220119111506p:plain

① ユーザはログイン画面で認証情報を入力しサーバに送ります。 ② 送られてきた認証情報が正しい場合、サーバはそのデータをレスポンスに含めます。 具体的には以下のようにレスポンスヘッダのSet-Cookieフィールドにデータを入れます。Set-Cookieは、ブラウザにデータをCookieとして保存してくれ、という命令です。

HTTP/2.0 200 OK
Content-Type: text/html
Set-Cookie: id=123
Set-Cookie: passwd=hogehoge

③ レスポンスを受け取ったブラウザはSet-Cookieフィールドからデータを取り出し、ブラウザのCookieに保存します。次回リクエストの時には以下のようにCookieをリクエストヘッダのCookieフィールドに含め送信します。

GET /index.html HTTP/2.0
Host: www.example.org
Cookie: id=123; passwd=hogehoge

具体的にブラウザのCookieにデータを保存するにはJavaScriptDocument.cookieを使い以下のようにします。

document.cookie = "id=123";
document.cookie = "passwd=hogehoge";

参考:

Document.cookie - Web API | MDN

④ リクエストヘッダからCookieに含まれる認証情報をチェックし、正しい情報であればレスポンスする、などします。

Cookieの基本的な動きは以上です。

Cookieの性質

RFC2109によると、Cookieには以下の性質があります。

項目 説明
保存形式 テキスト
cookieの全数量 少なくとも300個
ドメイン毎のcookieの数量 少なくとも20個
1つのcookieの容量 4096byte
有効期限 設定可能(未設定の場合はブラウザ終了時に削除)
異なるブラウザ間の共有 不可
有効範囲 domainオプションを付けない限り、Cookieが有効なオリジンに限る

有効範囲についてですが、設定しない限りCookieは異なるオリジン(scheme://hostname:port の組み合わせ、URL)には送信されないということです。

以上のことから、Cookieには以下のような問題点があります。

  • CookieはHTTPリクエストヘッダ・レスポンスヘッダを利用するので盗聴、漏洩リスクが高い
  • Cookieをブラウザに保存する場合、テキストで保存される上簡単な操作で見られるので漏洩の危険性がある
  • Cookieに格納できる上限は4096バイト(超えた部分は削除)

よって、重要データや容量の大きなデータをやり取りする場合はCookieは適していないことがわかります。

なので、ログイン状態の保持は認証情報を直接やり取りする方法ではなく、セッションIDを用いるセッションベース認証が使われることが多いです。

セッションベース認証とは

上記の特徴により認証情報そのものをCookieでやり取りするのは危険なので、そこで考え出されたのがセッションベース認証です。 セッションベース認証では、セッションIDと呼ばれる文字列を生成しそれをユーザ情報と紐付け、都度リクエスト時にセッションIDをCookieに含めることで認証状態を保持します。

f:id:surfi8000:20220119111529p:plain

このように、セッションIDをCookieでやり取りすることで、リクエストを受けたサーバはセッションIDに紐づくユーザ情報を探し存在していればそのユーザをログイン済みと見なします。

セッション情報はインメモリDBで管理

セッション情報はユーザからのリクエストのたびに参照されるため、高速に処理する必要があります。 ですので、セッション管理はRDBではなくRedisのようなインメモリデータベースで管理することが多いです。

このようなセッションを利用した認証をセッションベース認証と呼びます。

【エラー】COPY failed: file not found in build context or excluded by .dockerignore: stat file_name: file does not exist を解決

現象

以下のような記述のDockerfileを用意しアプリケーションをコンテナ化しgcloud run deployでCloud Run上にデプロイしたところ、特定のファイル(今回はinit.incファイル)がアップされていないことに気が付きました。

FROM php:7.0-apache

# Configure PHP for Cloud Run.
# Precompile PHP code with opcache.
RUN docker-php-ext-install -j "$(nproc)" opcache pdo_mysql
RUN set -ex; \
  { \
    echo "; Cloud Run enforces memory & timeouts"; \
    echo "memory_limit = -1"; \
    echo "max_execution_time = 0"; \
    echo "; File upload at Cloud Run network limit"; \
    echo "upload_max_filesize = 32M"; \
    echo "post_max_size = 32M"; \
    echo "; Configure Opcache for Containers"; \
    echo "opcache.enable = On"; \
    echo "opcache.validate_timestamps = Off"; \
    echo "; Configure Opcache Memory (Application-specific)"; \
    echo "opcache.memory_consumption = 32"; \
  } > "$PHP_INI_DIR/conf.d/cloud-run.ini"

# Copy in custom code from the host machine.
WORKDIR /var/www/html
COPY . ./

# Use the PORT environment variable in Apache configuration files.
# https://cloud.google.com/run/docs/reference/container-contract#port
RUN sed -i 's/80/${PORT}/g' /etc/apache2/sites-available/000-default.conf /etc/apache2/ports.conf

# Configure PHP for development.
# Switch to the production php.ini for production operations.
# RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
# https://github.com/docker-library/docs/blob/master/php/README.md#configuration
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"

COPY . ./ でコピーされてるはずだが、試しに以下のようにinit.incファイルを直接指定しCOPYすると、COPY failed: file not found in build context or excluded by .dockerignore: stat init.inc: file does not existというエラーが発生しました。

...
COPY . ./
COPY ./init.inc /
...

原因

上記エラーが起きている原因は、.gitignoreでinit.incを除外しているためのようです。.gitignoreで除外しているファイルはGCP上にアップされないようで、そのためfile does not existとなっていたようです。

対応

対策として、.gcloudignoreファイルをアプリケーションのルートディレクトリに作成し、.gitignoreを記載することで.gitignoreに記載したファイルもアップされるようになりました。また、GCP上にアップする必要のないファイル、ディレクトリも追記しました。

.gcloudignore

.gcloudignore
.git
.gitignore

参考

【GCP】SpringBoot + App Engine + CloudSQL + VPCネットワーク

はじめに

MySQLを用いたCRUDを行うSpringBootアプリケーションを、App Engineスタンダード環境にデプロイし、プライベートIPでCloud SQLへ接続を行う大枠の手順についてまとめます。

環境

アプリケーション環境

  • 言語: Java8
  • FW: SpringBoot
  • ビルドツール: Maven3.5.4
  • DB: MySQL5.7

GCP環境

  • AppEngine: Java8スタンダード環境
  • Cloud SQL: MySQL5.7, プライベートIP付与
  • VPCネットワーク

やりたいこと

今回やりたいことは、上記環境に基づいて作成したSpringBootアプリケーションをApp Engine上で動かし、Cloud SQLにプライベートIPでアクセスすることです。

App Engine上でデータベースを使う際は、App Engine内でミドルウェアとしてのデータベースを使うのは一般的ではなくGCPのストレージサービスを使用することになりますので、今回はデータベースとしてCloud SQLを使います。

手順

  1. App Engineの準備
  2. VPCネットワーク準備
  3. Cloud SQLの準備
  4. サーバレスVPCアクセスコネクタ作成

App Engineの準備

  1. App Engineインスタンス作成
  2. Spring BootアプリケーションにGAE用の設定をpom.xmlに追記、appengine-web.xml作成
  3. App Engineにデプロイ

この段階でデプロイしてもデータベースの設定がローカルのままなのでエラーになりますが、以降のステップでCloud SQLと連携すると動くようになります。

なお、#2でSpringBootの実行ファイル形式をjarからwarに変更します。

また、SpringBootアプリケーションのビルドやデプロイは以下のコマンドを使いました。

mvn clean install
mvn package appengine:deploy

VPCネットワークの準備

  1. VPCネットワークの作成

Cloud SQLの準備

  1. Cloud SQLでMySQLインスタンスの作成
  2. プライベートIPに作成したVPCネットワークを割り当てる
  3. Cloud ShellでMySQLインスタンスにアクセスしデータベースを作成
  4. SpringBootアプリケーションにCloud SQL用の設定をpom.xmlに追加

サーバレスVPCアクセスコネクタ作成

  1. コネクタの作成
  2. App EngineのVPCネットワーク接続

ここまでの設定を行い、GAEにデプロイすると動くかと思います。今回はざっくりとした手順のみまとめましたが、今後詳しい設定手順をまとめようと思います。

【GCP】GAE Java8フレキシブル環境からスタンダード環境に変更する

はじめに

Java8, SpringBootで作ったアプリをGoogle App EngineのJava8フレキシブル環境にデプロイしていたのですが、デプロイ先の環境をスタンダード環境に変更する必要がありました。これの手順についてメモします。

環境

以下アプリケーションの環境です。

  • 言語 : Java8
  • FW : SpringBoot
  • ビルドツール : Maven3.5.4

なお、SpringBootアプリケーションはこちら(https://start.spring.io/)で作った雛形をベースにしています。Packagingはjarを選択しました。

手順

流れは以下の通りです。

  1. GAE用の設定ファイル修正
  2. pom.xml修正
  3. クラス修正

1. GAE用の設定ファイル修正

GAE用の設定ファイルをフレキシブル環境用のproject_root/src/main/appengine/app.yamlを削除し、Java8スタンダード環境用にproject_root/src/main/webapp/WEB-INF/appengine-web.xmlを作成し必要事項(参考)を記述します。

2. pom.xml修正

雛形生成の際にビルド後の生成物としてjar形式を選択していたので、pom.xmlの<project>タグ内に<packaging>war</packaging>と記述しwar形式を指定します。(デフォルトでは<packaging>タグはありませんが、指定しないとjar形式になるようです)

3. クラス修正

SpringBootでは、パッケージングをjarからwarに変更する際にクラスの修正も必要になります。以下のようにアプリケーション起動処理が書かれているクラスを修正します。

  • SpringBootServletInitializerを継承するよう修正
  • configureメソッドをオーバーライドするよう追記
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

@SpringBootApplication
public class DemoApplication extends SpringBootServletInitializer {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(DemoApplication.class);
    }
}

以上の記述をしないと404エラーが発生します。

1~3のステップを踏んだ後に、mvn clean installしてmvn package appengine:deployでデプロイ成功するはずです。

"Communications link failure"でアプリ起動できない問題

SpringBootで作ったアプリを起動すると、以下のエラーが起こり起動に失敗する現象に遭遇しました。

Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

この現象の原因と解決策についてメモします。

環境

言語: Java8
FW: SpringBoot
DB: MySQL5.7
ビルドツール: Maven

現象

application.propertiesにdb接続の設定を以下のように記述し、

spring.datasource.url=jdbc:mysql://localhost:3306/sample_app
spring.datasource.username=testuser
spring.datasource.password=testuser
spring.jpa.database=MYSQL

mvn spring-boot:run とターミナルで叩きアプリを起動します。

すると以下のようにエラーが発生しました。

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.5.RELEASE)

2021-07-05 18:57:34.756  INFO 10112 --- [  restartedMain] com.example.demo.DemoApplication         : Starting DemoApplication on N0128.local with PID 10112 (/Users/akiyoshi.tokiya/develop/iTICKET/app_in_app/springbootsample/target/classes started by akiyoshi.tokiya in /Users/akiyoshi.tokiya/develop/iTICKET/app_in_app/springbootsample)
2021-07-05 18:57:34.758  INFO 10112 --- [  restartedMain] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2021-07-05 18:57:34.788  INFO 10112 --- [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2021-07-05 18:57:34.789  INFO 10112 --- [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2021-07-05 18:57:35.631  INFO 10112 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2021-07-05 18:57:35.651  INFO 10112 --- [  restartedMain] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-07-05 18:57:35.651  INFO 10112 --- [  restartedMain] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.19]
2021-07-05 18:57:35.718  INFO 10112 --- [  restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2021-07-05 18:57:35.718  INFO 10112 --- [  restartedMain] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 929 ms
2021-07-05 18:57:35.806  INFO 10112 --- [  restartedMain] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2021-07-05 18:57:37.040 ERROR 10112 --- [  restartedMain] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Exception during pool initialization.

com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
    at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:174) ~[mysql-connector-java-8.0.16.jar:8.0.16]
    at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64) ~[mysql-connector-java-8.0.16.jar:8.0.16]
    at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:835) ~[mysql-connector-java-8.0.16.jar:8.0.16]
...
    at com.mysql.cj.NativeSession.connect(NativeSession.java:165) ~[mysql-connector-java-8.0.16.jar:8.0.16]
    at com.mysql.cj.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:955) ~[mysql-connector-java-8.0.16.jar:8.0.16]
    at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:825) ~[mysql-connector-java-8.0.16.jar:8.0.16]
    ... 104 common frames omitted
Caused by: javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)
    at java.base/sun.security.ssl.HandshakeContext.(HandshakeContext.java:172) ~[na:na]
    at java.base/sun.security.ssl.ClientHandshakeContext.(ClientHandshakeContext.java:98) ~[na:na]
    at java.base/sun.security.ssl.TransportContext.kickstart(TransportContext.java:238) ~[na:na]
    at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:434) ~[na:na]
    at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:412) ~[na:na]
    at com.mysql.cj.protocol.ExportControlled.performTlsHandshake(ExportControlled.java:316) ~[mysql-connector-java-8.0.16.jar:8.0.16]
    at com.mysql.cj.protocol.StandardSocketFactory.performTlsHandshake(StandardSocketFactory.java:188) ~[mysql-connector-java-8.0.16.jar:8.0.16]
    at com.mysql.cj.protocol.a.NativeSocketConnection.performTlsHandshake(NativeSocketConnection.java:99) ~[mysql-connector-java-8.0.16.jar:8.0.16]
    at com.mysql.cj.protocol.a.NativeProtocol.negotiateSSLConnection(NativeProtocol.java:352) ~[mysql-connector-java-8.0.16.jar:8.0.16]
    ... 111 common frames omitted

結論

application.propertiesのspring.datasource.url=jdbc:mysql://localhost:3306/sample_appの末尾に?enabledTLSProtocols=TLSv1.2をつけることで解決しました。

spring.datasource.url=jdbc:mysql://localhost:3306/sample_app?enabledTLSProtocols=TLSv1.2

原因

ログを詳しく見ると、Caused by: javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate) というエラーに起因しています。これはRHEL 7 から RHEL 8 でサポートされる最新の TLS プロトコルバージョンが異なるために発生するようで、これにより、TLSハンドシェイクのネゴシエーション中にアプリケーションとT LSプロトコルの不一致が発生し接続が失敗してしまいます。

これの回避策として、データベース接続URLにTLSプロトコルバージョンを手動で指定すると接続できるようになるようです。

また、Communications link failureというエラーに関してですが、これはアプリケーションがdbにアクセスできていないために起こるエラーで、原因としては以下が考えられるようです。

  1. JDBC URLで指定しているIPアドレスやホストアドレスが間違っている
  2. JDBC URLで指定しているホスト名がローカルDNSに認識されていない
  3. JDBC URLで指定しているポート番号が間違っている
  4. DBサーバが起動していない
  5. DBサーバがTCP/IPコネクションを受け付けない
  6. DBサーバのコネクションが足りていない
  7. ファイアウォールやプロキシなどによってアプリケーションとDBの接続がブロックされている

上記の対策、調査方法としては以下があります。

  1. pingを試す
  2. DNSをリフレッシュ、またはJDBC URLに指定しているIPアドレスを指定する
  3. MySQLの場合、my.cnfのポート番号と一致するか確認
  4. DBを起動する
  5. mysqldが--skip-networking optionの指定なしで動いているか確認
  6. DBを再起動し、DB接続後にコネクションをしっかり閉じているか確認
  7. ファイアウォールやプロキシの設定を確認

今回の場合は5が当てはまりますね。

参考
https://access.redhat.com/documentation/ja-jp/red_hat_build_of_thorntail/2.7/html/release_notes_for_thorntail_2.7/connection-between-a-rhel-8-based-application-and-a-rhel-7-based-mysql-5-7-database-fails-due-to-tls-protocol-version-mismatch
https://stackoverflow.com/questions/2983248/com-mysql-jdbc-exceptions-jdbc4-communicationsexception-communications-link-fai

Linux負荷監視コマンドについて(top,vmstat,iostat...)

Linuxの負荷監視コマンドについてまとめます。

キャパシティプランニング(Capacity planning)とは、リソース不足によってシステムの運用に支障が出ないようにリソースを将来的に確保するための設計技法です。

具体的には、以下のようなリソースの状態を測定・監視・記録します。

・CPU使用率
・物理メモリ使用率
スワップ領域の使用率
・ディスクI/O
・ネットワークI/O

これらリソースの測定をするのにtop, vmstat, iostat, free, sar, ...といったコマンドが用意されています。

以下、各リソースとコマンドの対応です。

測定対象コマンド
CPUシステム全体としてのCPU使用率を表示: top, htop, vmstat, iostat, mpstat, sar
プロセス毎のCPU利用率を表示 : top, htop, ps u[ax]
負荷平均(load average)の表示 : top, htop, uptime, w
物理メモリシステム全体としての物理メモリ使用状況を表示 : top, htop, free, vmstat, sar -r
プロセス毎の物理メモリ利用率を表示 : top, htop, ps u[aux]
スワップ領域スワップ領域の使用状況を表示 : top, htop, free, wmstat, sar -S, swapon -s
スワップイン・アウトの状況を表示 : vmstat
ディスクI/Oiostat, iotop, vmstat, sar -b
ネットワークI/Oiptraf, netstat -i | -s, sar -n DEV|EDEV, ss
プロセスのPIDpstree -p, ps[aux], top, lsof