Spring Securityを使ってログイン機能を作っているのですが、ログインが失敗した時に入力フォームの値が消えてしまうのは不便なので、入力値が消えないようにしました。
やってみると意外に難しく6時間くらいハマってしまった…
具体的には、メールアドレスとパスワードでログインしようとして認証が失敗した時に、入力したメールアドレスを消えないようにするというもの。
とりあえず入力値を消えないようにするという目的は達成できたのですが、良いやり方かというとそうではないきがします。
Spring Securityの設定(failureHandlerを使う)
【SecurityConfig.java】
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.formLogin(login -> login
.loginProcessingUrl("/login")
.loginPage("/login").usernameParameter("email").passwordParameter("password")
// 認証失敗時はメールアドレスの入力値を保持するためにカスタムのハンドラを使う
.failureHandler(customAuthenticationFailureHandler()) -①
// .failureUrl("/login?error") -②
.defaultSuccessUrl("/")
.permitAll()
).logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
).authorizeHttpRequests(ahr -> ahr
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.mvcMatchers("/").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public AuthenticationFailureHandler customAuthenticationFailureHandler() {
return new CustomAuthenticationFailureHandler();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
①の.failureHandler(customAuthenticationFailureHandler())でログイン認証失敗時の処理をカスタム実装できます。内容は2.でご紹介します。
特にカスタムしない場合は、①を使わずに②の.failureUrl(“/login?error”)でOKです。ちなみに今回は①を使うので②はコメントアウトしています。
AuthenticationFailureHandlerインターフェースを実装する
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
public class CustomAuthenticationFailureHandler
implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
String forwardUrl = "/login?error";
RequestDispatcher dispatch = request.getRequestDispatcher(fowardUrl);
// フォワードを使うことでメールアドレスの入力値を保持
dispatch.forward(request, response);
}
}
/login?errorにforwardすることで入力されたメールアドレスを保持した状態で画面遷移することができます。
最初はsendRedirectでリダイレクトしようと思ったのですが、それではメールアドレスを保持した状態でlogin画面に遷移できないのでforwardを使いました。
th:valueでパラメータを取得する
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>login page</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<link rel="stylesheet" type="text/css" th:href="@{/css/login.css}" href="../static/css/login.css">
</head>
<body class="text-center bg-info bg-gradient">
<form class="border rounded bg-white form-login" method="post" autocomplete="off" th:action="@{/login}">
<h1>Login</h1>
<div class="mb-3">
<input type="email" name="email" placeholder="メールアドレス" class="form-control" th:value="${param.email != null } ? ${param.email[0]} : ''">
</div>
<div class="mb-3">
<input type="password" name="password" placeholder="パスワード" class="form-control">
</div>
<p th:text="${errorMsg}"></p>
<button type="submit" class="btn btn-primary" name="login-button">ログイン</button>
</form>
</body>
</html>
th:value=”${param.email != null } ? ${param.email[0]} : ”” でメールアドレスに値があった場合、それの値を表示するようにしています。
なお/login?errorを受け取るControllerには下記のようにしました。
@PostMapping(value="/login", params="error")
public String error(Model model) {
model.addAttribute("errorMsg", "ログイン認証に失敗しました");
return "/login";
}
さいごに
Spring Securityに限らずフレームワークはとても便利ですが、細かいところで動きが見えずづらく分かりにくい部分がありますよね
ログイン機能のしても、仕組みを理解していなくても何となくできてしまうので、特に初心者の方はまずフレームワークを使わずに実装して先に仕組みを理解した方がいい気がします。
この記事が少しでも誰かの役に立てば嬉しいです。