親バカエンジニアのナレッジ帳

webのエンジニアをやっており、日頃の開発で詰まったことや書き残しておきたいことを載せています。育児のイロハという育児サイト(https://ikujip.jp)の開発も行っているため、その開発で使用されている技術についても掲載しています。

Spring Securityを使ったログイン機能 (2)ユーザ情報登録フォームの実装

ti-tomo-knowledge.hatenablog.com

にて、ログイン認証されていない時にログイン画面に遷移される処理の説明をしましたので、
次にログイン画面の実装...に行きたいのですがその前に、
そもそも認証先のデータを用意する必要がありますよね。
そこで今回はユーザデータを登録する処理を実装しようと思います。
いわゆるサインアップですね。

認証先のテーブルについてですが、accountsテーブルというものを用意しました。

create table accounts (
    id integer primary key,
    mail_address varchar(255),
    password char(60),
    updated_at timestamp not null default current_timestamp,
    created_at timestamp not null default current_timestamp
);

登録するデータとしてはメールアドレスとパスワードですね。
もちろんパスワードはハッシュ化した値を入れることになります。

後ほど出てくる話題になりますが、
パスワードはBCrypt(Blowfish暗号)という暗号化を行って保存をします。
暗号化をすると文字数は60文字になるので定義はchar(60)としています。

まずはコントローラについてです。

package test.package;
 
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
 
@Controller
public class accountController {
 
    @ModelAttribute
    public AccountForm setupForm() {
        return new AccountForm();
    }
 
    @RequestMapping(value="account")
    String accountForm() {
        return "account/accountForm";
    }
}

ポイントは@ModelAttributeアノテーションをつけているsetupForm()メソッドの箇所ですね。
AccountFormは後ほど詳細は記載しますが、formからPOSTされた時に値を受け取るクラスになります。
@RequestMappingでformのあるthymeleafにアクセスする場合、
事前にsetupForm()でインスタンス化をしておけば、
AccountFormで設定したフィールドをformで利用することができるのです。

AccountFormの内容は以下になります。

package test.account;
 
import lombok.Data;
 
import java.io.Serializable;
 
@Data
public class AccountForm implements Serializable {
    private String email;
    private String password;
}

java.io.Serializableインタフェースを実装してシリアライズさせておりますが、
メソッドや定数をもたないインタフェースであるため、オーバーライドするメソッドはありません。
インスタンス変数がシリアライズの対象になるのですが、今回は2つのフィールドが対象となります。
それぞれgetter/setterを設置しなければエラーが発生してしまいますが、
@Dataを見ていただければわかると思いますが、私は楽をするためにLombokを使って記載を省略しています。
今回はサインアップ周りが主題なので、バリデーションなどは一旦省略します。
(もちろん後からちゃんと入れますよw)

accountFormのthymeleafの中身は以下になります。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>ユーザー登録</title>
</head>
<body>
<div>
 
    <form th:action="@{/account}" action="/account" th:object="${accountForm}" method="post">
 
        <h1>ユーザー登録</h1>
 
        <div th:classappend="${#fields.hasErrors('email')}? 'has-error'">
            <label for="email">E-mail</label>
 
            <div>
                <input id="email" type="email" th:field="*{email}" name="email"/>
            <span th:if="${#fields.hasErrors('email')}" th:errors="*{email}">error!</span>
            </div>
        </div>
        <div th:classappend="${#fields.hasErrors('password')}? 'has-error'">
            <label for="password">パスワード</label>
 
            <div>
                <input id="password" type="password" th:field="*{password}" name="password"/>
            <span th:if="${#fields.hasErrors('password')}" th:errors="*{password}">error!</span>
            </div>
        </div>
        <input type="submit" value="新規登録"/>
    </form>
</div>
</body>
</html>

の部分については、
htmlにthymeleafの名前空間を与えているのです。
これで「th:〜」の記述をするとプログラムで使えるようになります。


localhost:8080/account」にアクセスすると、accountFormが開くようになっています。

f:id:tomotomo1129:20180611205532p:plain

後はsubmitした後の登録処理ですが、
事前にModelとサービスクラス、また、リポジトリクラスを定義してください。

まずはModelです。

package test.model;
 
import lombok.Data;
 
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
 
@Entity
@Table(name = "accounts")
@Data
public class Account implements Serializable {
    @Id
    @GeneratedValue
    private Integer id;
 
    @Column(nullable=false)
    private String email;
 
    @Column(nullable=false, length=20)
    private String password;
 
    @Column(nullable=false, updatable=false)
    private Date created_at;
 
    @Column(nullable=false)
    private Date updated_at;
}

ここでもsetter/getterの定義を省略するためにlombakの@Dataを使います。
「@GeneratedValue」はidにつけておけば一意の値が自動で付与されます。

続いてリポジトリです。

package test.repository;
 
 
import org.springframework.data.jpa.repository.JpaRepository;
import test.model.Account;
 
public interface AccountRepository extends JpaRepository<Account, Integer> {
}

これだけでいいのです。
JPAを使ったリポジトリは、JpaRepositoryを継承して対象となるモデルをimportしておけばfineOne、findAll、save、deleteを自動で使えるようになります。

続いてサービスクラスですが、その前に...

Spring Securityを利用するにあたり、
「WebSecurityConfigurerAdapter」を継承して認証設定をしているクラスがあると思いますが、
その中に以下を追加してください。

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

これでパスワードのハッシュ化でBCryptを使うことができます。

さあ、改めてサービスクラスです。

package test.service;
 
import test.model.Account;
import test.repository.AccountRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
@Service
@Transactional
public class AccountService {
    @Autowired
    AccountRepository accountRepository;
    @Autowired
    PasswordEncoder passwordEncoder;
 
    public Account create(Account account, String rawPassword) {
        String encodedPassword = passwordEncoder.encode(rawPassword);
        account.setPassword(encodedPassword);
        return accountRepository.save(account);
    }
}

リポジトリをimportし、保存処理を書いています。
また、パスワードはそのまま保存するわけにはいかないので、ハッシュ化するための関数passwordEncoderも使っています。
「PasswordEncoder passwordEncoder;」において、先ほど定義したBCryptPasswordEncoderのインスタンス化が行われています。

次にコントローラにPOSTされた時の処理を記載します。

@RequestMapping(value = "account", method = RequestMethod.POST)
String create(@Validated AccountForm form, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        return "account/accountForm";
    }
    Account account = new Account();
    account.setEmail(form.getEmail());
    accountService.create(account, form.getPassword());
    return "redirect:/acount/complete";
}
 
@RequestMapping(value = "account/complete", method = RequestMethod.GET)
String createFinish() {
    return "account/accountComplete";
}

もちろんaccountServiceをインスタンス化してからこのメソッドを定義してください。
以上でフォームにメールアドレスとパスワードを入力してPOSTすれば、
無事にデータが登録されます。

Spring BootのCSRFトークン

Spring BootにおけるCSRF対策でのformタグへのトークン付与はとても簡単です。
「spring-boot-starter-security」というライブラリを使い、
formタグに対して、「th:action=〜」の記述をすれば自動的に付与されるのです。

実際に試してみると、

<input type="hidden" name="_csrf" value="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">

のようなタグが入っているかと思います。

最近のフレームワークはやっぱり便利ですね〜。

Spring BootでFlywayを使ったマイグレーション

開発をするにあたってDBのバージョン管理をしっかりするためにもマイグレーションは必要です。
Spring Bootでは、Flywayを使えばとても楽にマイグレーションを行うことができます。

まず、Flywayを使うためにpom.xmlに以下を追加してください。

<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>

次に、src/main/resourcesにdb.migrationパッケージを作成してください。

これで準備OKです。
めちゃめちゃ簡単ですね。

マイグレーションファイルは先頭に「V + バージョン番号」を記入してください。
バージョン情報はschema_versionというテーブルで管理され、
アプリケーション実行時に未反映のマイグレーション処理が実行されます。

例としてUsersテーブルを作成するマイグレーションファイルを作成します。

V1__create-users.sql

create table users (
    id integer primary key,
    mail_address varchar(255),
    password char(30),
    updated_at timestamp not null default current_timestamp,
    created_at timestamp not null default current_timestamp
);

この状態でアプリケーションを起動させると、
usersテーブルが作成されます!

また、初回の実行時にはschema_versionテーブルも作成されてマイグレーションが実行された履歴が残ります。

Spring BootでJPAを使用したデータベース設定

Spring Bootでデータベースを利用するために、JPAの設定をしようと思います。
そもそもJPAとは「Java Persistence API(Javaの永続化のAPI)」の略であり、
Javaの純正技術として浸透しているORMの仕様です。
なので、実際にはORMの技術で実装されたライブラリ(いわゆるプロバイダ)を使用します。
有名なライブラリとしてはHibernateなどがありますね。
Spring Bootでは、Hibernateの技術を駆使したライブラリとしてSpring Data JPAが用意されており、今回はこれを設定します。

まずはpom.xmlに以下を追加してSpring Data JPAのライブラリをインストールしましょう。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

また、JavaアプリケーションからMySQLに接続するために、
JDBCドライバのインストールもしましょう。

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

scopeのruntimeは、実行時のみに必要な場合の指定で、JDBCドライバの場合はruntimeを使うことが普通です。

この状態でアプリケーションを起動させてください。

そうすると、以下のようなエラーが発生します。

***************************
APPLICATION FAILED TO START
***************************
 
Description:
 
Cannot determine embedded database driver class for database type NONE
 
Action:
 
If you want an embedded database please put a supported one on the classpath. If you have database settings to be loaded from a particular profile you may need to active it (no profiles are currently active).
 
 
Process finished with exit code 1

DB接続のライブラリはあるが、設定情報が書かれていないために、
どこに接続したら良いのかわからないと言っていますね。

DB接続の設定は、src/main/resources以下にapplication.ymlか、または、application.propertiesを作成します。

application.ymlに記載する場合は以下のようになります。

spring:
  datasource:
    url: jdbc:mysql://localhost/test_database?useSSL=false
    username: 環境ごとのユーザ名
    password: 環境ごとのパスワード
    driverClassName: com.mysql.jdbc.Driver

urlの末尾にある「?useSSL=false」はSSL接続をするか否かの設定ですが、
SSL接続をしない場合は基本的に指定は不要ですが、
実行時にWARNINGのメッセージが大量に出て不気味なのでfalseの指定をすることにしました。

これでアプリケーションを起動し、エラーなどが発生しなければひとまず設定はOKです。

ちなみにapplication.propertiesに設定する場合は、以下のように記載してください。

spring.datasource.url=jdbc:postgresql://localhost:3306/test_database
spring.datasource.username=環境ごとのユーザ名
spring.datasource.password=環境ごとのパスワード
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

Spring Securityを使ったログイン機能 (1)未ログイン時の画面遷移

Spring BootにはSpring securityという認証と認可を司るコンポーネントがありますが、
これを使いながら数回に分けてログイン機能を実装したいと思います。
未ログイン時にログインフォームに遷移する動きから、最終的にはデータベースのユーザデータと認証をするところまで実装したいと思います。

まずはApplication起動用のパッケージを用意します。
Spring Bootでは何をするにもSpringApplication.runで起動しなければ始まりませんからね。
僕はsrc/main/java/パッケージ直下にApplication.javaというファイル名で配置しました。

package test.package;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

次にログインフォームを設置するので(今回は単純にHello Worldを表示するだけですが...)、
template/login直下にloginForm.htmlというテンプレートを用意します。

コントローラーはsrc/main/java/app/loginというパッケージを用意して、
LoginController.javaを作りました。

package test.package;
 
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
 
@Controller
@RequestMapping("login")
public class LoginController {
 
    @RequestMapping(value="")
    String loginForm() {
        return "login/loginForm";
    }
}

まずはこの時点でコンパイルし、localhost:8080/loginでログイン画面が表示されるか確認しましょう。
(何度も書きますが、今回は単純にHello Worldを表示するだけですが...w)、

f:id:tomotomo1129:20180609200926j:plain

さて、今普通に見れていたこのページをログインしていない状態では閲覧できないようにします。
(通常ログインフォームを閲覧する場合はログインはしていませんがw実験です)
Spring Securityを使うには、spring-boot-starter-securityが必要なため、
pom.xmlに以下を追加してインポートします。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

続いて、WebSecurityConfigurerAdapterクラスを継承したクラスを作り、
各種必要な設定をしましょう。

まずはクラスの作成です。

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
 
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
}

こちらは設定クラスの作成なので@Configurationを付与します。
(後に@Beanアノテーションを付与したメソッドも定義するので、その意味でも@Configurationは必要です。)
@EnableWebSecurityとThymeleafを共に使うことで、formの中にCSRFトークンが自動で埋め込まれます。
まだformは作りませんが、後で忘れないように入れておきましょう。

※@Configurationとともに@EnableWebMvcSecurityのアノテーションを貼るソースコードをいくつか見かけましたが、
僕が使った最新のSpring Bootのバージョン(1.3.1.RELEASE)では非推奨になったようです。

次に例外となるディレクトリやファイルを設定します。
下記のようなディレクトリやファイルについては、ログインの有無は問いません。

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/favicon.ico", "/css/**", "/js/**", "/images/**", "/fonts/**");
}

次は認証が必要となるURLページの設定です。
「http.authorizeRequests()」で認証が必要となるURLを設定する関数で、
「antMatchers("〜").permitAll()」は認証が不要の例外ページ、
「anyRequest().authenticated();」で、それ以外のページは認証された状態でいる必要がある、ということになります。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests() // 認証が必要となるURLを設定します
        .antMatchers("/account/**").permitAll() // /account以下のURLも認証不要
        .anyRequest().authenticated(); // それ以外はすべて認証された状態じゃなきゃダメだよ〜
}

一旦この時点でプログラムを動かしてみましょう。
ログイン認証なしでアクセスが許されているのは、静的ファイルと/account/以下のURLのみです。
よって、当初は通っていたloginが通らなくなっているはずです。

コンパイルしてlocalhost/loginにアクセスすると...

f:id:tomotomo1129:20180609201324j:plain

ということでアクセスできなくなりました。
想定通りですね。
通常ログインフォームの画面に行く際は当然ログイン状態である必要はないので、
認証不要のページにログインフォームも追加します。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests() // 認証が必要となるURLを設定します
        // ↓追加
        .antMatchers("/login").permitAll() // /loginFormは認証不要
        // ↑追加
        .antMatchers("/account/**").permitAll() // /account以下のURLも認証不要
        .anyRequest().authenticated(); // それ以外はすべて認証された状態じゃなきゃダメだよ〜
}

これでアクセスすれば再度問題なく画面が開きます。

Spring BootにおけるBeanクラスとは

JavaにおけるBean

まずはJavaにおけるBeanをご存知でしょうか。
BeanとはJavaBeansの略であり、インスタンス化して使用するクラスです。

変数のアクセス修飾子はprivateとし、その変数の値を変更したり、取得する際にはpublicなメソッドを使用します。
また、引き数なしのコンストラクタを持ちます。
いわゆるゲッター(getter)やセッター(setter)を保持するクラスですね。

JavaBeansを使うことについては賛否がありますが、
私自身はソースコードの共通化や可読性のためにも利用するのは好きですね。

Spring FrameworkにおけるBean

Spring Bootにおける使用方法の前に、Spring FrameworkにおけるBeanの使い方ですが、使用するBeanの定義をXMLファイルに宣言する必要があるのです。
以下のような感じで毎回。。。

<bean id="Beanの名前" class="パッケージ + クラス名">

詳細な設定方法については、ググればたくさん詳しいものが出てくると思うのでここでは割愛します。
とにかくインスタンス化したいクラスを作成するたびにXMLファイルを編集する必要があり、面倒くさいんですね。
Spring FrameworkXML地獄で面倒だという理由がよくわかります。

Spring BootでBeanの管理をもっと簡単に

そこで、Spring Bootです。
Spring Bootではアノテーション方式で設定が可能なのです。

まず、Beanインスタンス作成するためのメソッドの前に@Beanアノテーションを追加し、Beanインスタンスを返値として指定します。

また、そのメソッドを保持するクラスの前には@Configurationアノテーションをつけてください。
これで設定ファイル(XMLファイル)を使わずにBean設定をするクラスと認識させることができるのです。

package パッケージ;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class TestBeanConfig {
 
    @Bean
    public TestBeanClass testBean() {
        return new Beanクラス; // Beanインスタンスを返す
    }
 
}

ちなみに、@Component、@Service、@Repository、@Controllerをつけるクラスについては、このアノテーションをつけた時点でBeanとして登録されるため、@Beanを付与する必要はありません。

Spring Boot 2 プログラミング入門

Spring Boot 2 プログラミング入門

特定のポートを使用しているプロセスを確認して強制終了させる

プロセスが起動中なのにPIDがわからないから削除できない

プロセスで困ったこととして、例えば以下のことがありませんか?

  • IDEでアプリケーションを起動して開発をしていたが、IDEが途中で落ちてしまった。アプリケーションを終了していないのでプロセスが起動中だが、プロセス(PID)がわからない
  • サーバ上でアプリを起動させようとした時に、「そのポートは使用中だよ」的なメッセージが出て起動できない

プログラミングをよくやられる方ならどちらも一度は経験しているのではないでしょうか。
いずれにせよ、このような場合はポートを使用しているプロセスを確認して強制終了させるしかありません。

ポート番号からプロセスを確認

使いたいポート番号のプロセスを確認したい時は以下でOK!
※ポートは例として8080にしています。「-i」はネットワークソケットファイルの表示という意味ですが、ちょっと難しいのであまり意味を考えなくてもいいのではないでしょうか。

lsof -i:8080

すると、以下のように使用中のプロセスを確認できます。

killを使ってプロセスを強制終了

あとはkillを使えばそのプロセスを終了させられます。
今回で言えば、上記キャプチャのPIDに当たる部分、つまり49137を指定します。

kill -9 49137

これでOKです!
とても簡単な手順ですよね。

新しいLinuxの教科書

新しいLinuxの教科書

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

日経Linux 2018年 7 月号

日経Linux 2018年 7 月号

入門者のLinux 素朴な疑問を解消しながら学ぶ (ブルーバックス)

入門者のLinux 素朴な疑問を解消しながら学ぶ (ブルーバックス)