2
1

More than 1 year has passed since last update.

SpringBoot Rest-Apiメール認証付きユーザー登録機能の実装

Last updated at Posted at 2023-02-13

概要

SpringBoot Rest-APIでメール認証付きのユーザー登録機能を実装します。

開発環境

OS:windows10
IDE:IntelliJ Community
spring-boot-starter-parent 2.75
java : 11
postman : E2Eテストツール

データベース
mysql:8.0.29
クライアントソフト: MySQL Workbench

ユースケース

大まかな流れとしては以下の様になっています。
1.ユーザーがEmailとパスワードを入力する
2.システム側がDBにユーザー情報が仮登録し、入力されたEmailに認証メールを送る
3.ユーザーが認証メールのリンクを開く。
4.システム側が顧客の状態を更新する。
5.アカウント登録完了
image.png

実装

作成したファイル一覧

ファイル名 説明
Customer.java Entityクラス データベースの属性に該当します。
CustomerRepository.java リポジトリレイヤー。データベースとの接続口 SpringJpaを利用
CustomerService.java サービスクラス(インターフェイス)サービスの返り値と引数を定義
CustomerServiceImplement.java サービスクラス(具象クラス)。ビジネスロジックの具体的な実装を定義
CustomerRestController.java コントローラーレイヤー。URI HttpMethod 返り値を定義するクラス
SignUpRequestBody.java ユーザー登録する時のapiのRequestBodyを定義するクラス
VerifyRequestBody.java ユーザーを認証する時のapiのRequestBodyを定義するクラス
application.property 送信元のEmailアドレス、アプリパスワードを設定するファイル

実装の方針

メール認証機能を実装するために、Customerテーブルにenabled(bool)とverification_code(64桁の文字列)を追加します。
最初に登録されたときはenabled=true verification_codeは64桁の文字列です。
認証コードを照合するapiが実行され 成功した場合に enabled=true verification_code=nullになります。

送信元のメールはGmailを使います。

アプリパスワードはいつもログインの際に使うパスワードとは別物で、新規作成する必要があります。
アプリパスワードは16桁の文字列です。

コード

Customer.java

Customer.java
package com.example.restapi.domain.customer;
@Entity
@Table(name = "customers")
@Data
public class Customer {

    // ID データベースで使う
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id", nullable = false)
    private Integer id;

    // Loginするときのパスワードして使う
    @Column(nullable = false, length = 64)
    private String password;

    // ログインするときのIDとして使用する
    @Column(nullable = false, unique = true, length = 45)
    private String email;

    // 認証コード
    @Column(name = "verification_code", length = 64)
    private String verificationCode;

    // 初期値はfalse 認証が成功すると trueになる。
    private boolean enabled;

    public Customer(Integer id, String password, String email, String verificationCode, boolean enabled) {
        this.id = id;
        this.password = password;
        this.email = email;
        this.verificationCode = verificationCode;
        this.enabled = enabled;
    }

    public Customer() {
    }

    public Customer( String email,String password) {
        this.password = password;
        this.email = email;
    }

    public Customer(String email,String password,String verificationCode, boolean enabled) {
        this.password = password;
        this.email = email;
        this.verificationCode = verificationCode;
        this.enabled = enabled;
    }
}


CustomerRepository.java

CustomerRepository.java
package com.example.restapi.domain.customer;
@Repository
public interface CustomerRepository extends CrudRepository<Customer, Integer> {

    @Query("SELECT c FROM Customer c WHERE c.email = ?1")
    public Customer findByEmail(String email);
    boolean existsByEmail(String email);
    Customer save(Customer customer);
    @Query("SELECT c FROM Customer c WHERE c.verificationCode = ?1")
    Customer  findByVerificationCode(String verificationCode);

    @Query("UPDATE Customer c SET  c.verificationCode = null,c.enabled= true  WHERE c.id = ?1")
    @Modifying(clearAutomatically = true)
    public void enabled(Integer id);
}

CustomerService.java

CustomerService.java
package com.example.restapi.domain.customer;

// todo extract register and login method from controller
@Service
public interface CustomerService {

        // ユーザー登録
        public Customer registerCustomer(String email,String password);

        // 認証コードを引数として、ユーザー情報を認証済みにする。
        public boolean verify(String verificationCode);
}


CustomerServiceImplement.java

CustomerServiceImplement.java
package com.example.restapi.implement.customer;


@Service
@Transactional
public class CustomerServiceImplement implements CustomerService {

    @Autowired
    CustomerRepository customerRepository;

    @Autowired
    PasswordEncoder passwordEncoder;
    @Override
    public LoginDto findByEmail() {
        return null;
    }

    @Autowired
    private JavaMailSender javaMailSender;

    @Autowired
    ResourceLoader resourceLoader;

    @Value("${spring.mail.username}")
    private String fromEmail;

    @Override
    public Customer registerCustomer(String email, String password) {
        String randomCode = RandomString.make(64);
        Customer newCustomer = new Customer(email, passwordEncoder.encode(password), randomCode, false);
        customerRepository.save(newCustomer);
        sendVerifyMail(email,randomCode);

        return newCustomer;
    }
    //
    @Override
    public boolean verify(String verificationCode) {
        Customer customer = customerRepository.findByVerificationCode(verificationCode);
        if (customer == null || customer.isEnabled()) {
            return false;
        } else {
            customerRepository.enabled(customer.getId());
            return true;
        }

    }

    private boolean sendVerifyMail(String customerEmail,String verificationCode) {

        MimeMessage message = javaMailSender.createMimeMessage();
        MimeMessageHelper helper = null;


        // リンク先のページはフロントエンド(Next.js)側で実装する(dynamicRouting使う)
        // リンク先に飛んだときにverifyApiを実行する.
        // apiを実行する際にRequestBodyにverifyCodeを詰める

        try {
            helper = new MimeMessageHelper(message, true);
            helper.setFrom(fromEmail);
            helper.setTo(customerEmail);
            helper.setSubject("Ecshop Verification");
            String insertMessage = "<html>"
                    + "<head></head>"
                    + "<body>"
                    + "<h3>Hello " + customerEmail + "</h3>"
                    +"<div>ECショップのユーザー仮登録が完了しました。下リンクをクリックして有効化してください</div>"
                    +"<a href=http://localhost:3000/customer/verify/"+verificationCode+">ユーザの有効化</a>"
                    + "</body>"
                    + "</html>";

            helper.setText(insertMessage, true);
            javaMailSender.send(message);

        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        return true;
    }


CustomerRestController.java

CustomerRestController.java
package com.example.restapi.implement.customer;

@RestController
public class CustomerRestController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private CustomerRepository customerRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private CustomerService customerService;

    @Autowired
    private MailSender mailSender;

    public static final String LINE_SEPARATOR = System.getProperty("line.separator");

    @PostMapping("/api/auth/signup")
    public ResponseEntity<?> registerUser(@RequestBody SignupRequestBody signupRequestBody){

        if(customerRepository.existsByEmail(signupRequestBody.getEmail())){
            return new ResponseEntity<>("すでに登録されているEmailです。",HttpStatus.BAD_REQUEST);
        }
        customerService.registerCustomer(signupRequestBody.getEmail(), signupRequestBody.getPassword());


        return new ResponseEntity<>("Customer registered successfully",HttpStatus.OK);
    }

    @PutMapping("/api/auth/verify")
    public ResponseEntity<?> verifyUser(  @RequestBody VerifyRequestBody verifyRequestBody ){
        if(!customerService.verify(verifyRequestBody.verifyCode)){
            return new ResponseEntity<>("認証コードが正しくありません",HttpStatus.BAD_REQUEST);
        }
        return new ResponseEntity<>("認証コードを承認しました。",HttpStatus.OK);
    }


SignUpRequestBody.java

SignUpRequestBody.java
package com.example.restapi.implement.customer;

@Data
public class SignupRequestBody {
    private String email;
    private String password;
}


VerifyRequestBody.java

VerifyRequestBody.java
package com.example.restapi.implement.customer;

import lombok.Data;

@Data
public class VerifyRequestBody {
    String verifyCode;
}


application.property

ハイライト部は他の人に使われたら困るので適当な値を入れています。

application.property

spring.mail.host=smtp.gmail.com
spring.mail.port=587
+spring.mail.username=xxxxxxx@gmail.com
+spring.mail.password=0123456789ABCDEF
spring.mail.properties.mail.smtp.auth = true
spring.mail.properties.mail.smtp.starttls.enable = true
spring.mail.defaultEncoding=UTF-8
spring.mail.properties.mail.smtp.timeout=5000
spring.mail.properties.mail.smtp.socketFactory.port=465
spring.mail.properties.mail.smtp.socketFactory.class =javax.net.ssl.SSLSocketFactory

動かして検証

Postman、捨てメアドを使って期待する動きをするか確認します。

Postmanでユーザー情報を入力してDBに追加する

email: : m32d8ausub@sute.jp(捨てメアド)
password:test
DBに追加されていることを確認できます。
パスワードは難読化されており、入力時とは一致していません。
以下gifアニメ
左側はPostman 右側はMySqlWorkbenchでDBの動きを確認しています。
register-User1.gif

メールを確認する

認証コードが入ったリンクを確認できます。
image.png

Postmanを使ってユーザーを有効化する

メールのリンクに入っている認証コードをRequestBodyに詰めてverifyApiを実行します。
enabled=trueになったことを確認できます。

register-verify.gif

課題

一定時間経過した未認証のユーザーを削除した方が良いと思った。
ただ実装方法がわからない。(SpringBatch使う?わからん)

参考資料

アプリパスワードの使い方

SpringBootでHTMLメールを送信をする方法

application.propertyの設定を読み出す方法
@Valueを使う

検証に使った捨てメアドサイト

udemy
Section 15 Code Customer Registration

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1