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

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すれば、
無事にデータが登録されます。