概要
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.アカウント登録完了
実装
作成したファイル一覧
ファイル名 | 説明 |
---|---|
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
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
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
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
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
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
package com.example.restapi.implement.customer;
@Data
public class SignupRequestBody {
private String email;
private String password;
}
VerifyRequestBody.java
package com.example.restapi.implement.customer;
import lombok.Data;
@Data
public class VerifyRequestBody {
String verifyCode;
}
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の動きを確認しています。
メールを確認する
Postmanを使ってユーザーを有効化する
メールのリンクに入っている認証コードをRequestBodyに詰めてverifyApiを実行します。
enabled=trueになったことを確認できます。
課題
一定時間経過した未認証のユーザーを削除した方が良いと思った。
ただ実装方法がわからない。(SpringBatch使う?わからん)
参考資料
アプリパスワードの使い方
SpringBootでHTMLメールを送信をする方法
application.propertyの設定を読み出す方法
@Valueを使う
検証に使った捨てメアドサイト
udemy
Section 15 Code Customer Registration