Spring Boot でJDBCを使ってユーザー登録・ユーザー認証する方法
まずは公式に従う。
≫ https://spring.io/guides/gs/securing-web/
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
DBは、HSQLDBを使用。
appliaction.properties は以下の通り。
spring.datasource.url=jdbc:hsqldb:hsql://localhost/auth spring.datasource.username=sa spring.datasource.password= spring.datasource.driver-class-name=org.hsqldb.jdbc.JDBCDriver spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.HSQLDialect spring.jpa.hibernate.ddl-auto=update
トップページを(index.html)を作成。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>トップページ</title> </head> <body> <h1>トップページ</h1> <a href="/mypage">マイページ</a> </body> </html>
認証が必要なページ(mypage.html)を作成。
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>login</title> </head> <body> <h1>マイページ</h1> <form th:action="@{/logout}" method="post"> <input type="submit" value="ログアウト" /> </form> </body> </html>
ログイン画面(login.html)を作成。
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>login</title> </head> <body> <div th:if="${param.error}"> エラー: ユーザ名・パスワードが違います。 </div> <form th:action="@{/login}" method="post"> <div><label>ユーザ名: <input type="text" name="username"/> </label></div> <div><label>パスワード: <input type="password" name="password"/> </label></div> <div><input type="submit" value="ログイン"/></div> </form> <div><a href="/newuser">新規ユーザー登録</a></div> <a href="/">戻る</a> </body> </html>
新規ユーザー登録画面(newuser.html)を作成。
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>新規ユーザー登録</title> </head> <body> <h1>新規ユーザー登録</h1> <form th:action="@{/newuser}" method="post"> <div><label>ユーザ名: <input type="text" name="username"/> </label></div> <div><label>パスワード: <input type="password" name="password"/> </label></div> <div><label>パスワード再入力: <input type="password" name="password2"/> </label></div> <div><input type="submit" value="登録"/></div> </form> <a href="/">戻る</a> </body> </html>
コントローラを作成。
package hoge; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class MyController { @RequestMapping(value = "/") public String index() { return "index"; } @RequestMapping(value = "/login") public String login() { return "login"; } @RequestMapping(value = "/newuser") public String newuser() { return "newuser"; } @RequestMapping(value = "/mypage") public String top() { return "mypage"; } }
この状態でSpring Bootアプリを起動すると、認証の制限がかかってないので、各ページを自由に行き来できる。
/ と /newuser は誰でもアクセス可能。
/mypage は、ログインしないとアクセスできないようにする。
package hoge; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/", "/newuser").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .and() .logout() .permitAll(); }
上記のWebSecurityConfig を作ってから再起動すると、/ と /newuser にはアクセス可能。/mypage にアクセスしようとすると、/login に飛ばされる。
JDBCで認証できるようにする。
公式にはちらっとJDBC認証の方法がある。
≫ https://docs.spring.io/spring-security/site/docs/current/reference/html/jc.html
@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { UserBuilder users = User.withDefaultPasswordEncoder(); auth .jdbcAuthentication() .dataSource(dataSource) .withDefaultSchema() .withUser(users.username("user").password("password").roles("USER")); }
しかし、このコードだと一発目は動作するけど、二回目からは例外が発生してSpring Boot アプリが起動しなくなる。
withDefaultSchema()とwithUser()あたりが例外の原因だった。
以下の1行だけにすれば二回目からも問題なく動作する。
@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication().dataSource(dataSource); }
この先の実装方法がわからなくていろいろぐぐってみた。
が、公式のドキュメントが少ないし、ぐぐってみても割と複雑なコードで実装してる人が多くて困った。
なんとなく、もっと簡単なコードで実装できそうな気がすると思ったので適当にコード書いたら動いちゃった。
新規ユーザー登録できるようにする。
とりあえずJdbcUserDetailsManager をコントローラにDIしてみる。
@Autowired private JdbcUserDetailsManager userManager;
/newuser の POST でユーザー登録する処理を書く。
JdbcUserDetailsManagerにcreateUser()というメソッドがあったので、これ使えばできるんじゃないかなと。
上記の公式の説明でInMemoryUserDetailsManagerでもそうしてたし。
スーパークラスが同じだからたぶんいけるはず!
@RequestMapping(value = "/newuser", method = RequestMethod.GET) public String newuser() { return "newuser"; } @RequestMapping(value = "/newuser", method = RequestMethod.POST) public String register(@RequestParam("username") String username, @RequestParam("password") String password) { UserBuilder users = User.withDefaultPasswordEncoder(); userManager.createUser(users.username(username).password(password).roles("USER").build()); return "login"; }
User.withDefaultPasswordEncoder()でdeprecatedの警告が出るので、気になる人は適当なPasswordEncoderを設定してね。
これで起動しようとしたら、JdbcUserDetailsManagerをDIしたいけど、定義がないからできないよって怒られる。
JdbcUserDetailsManager と DataSource をWebSecurityConfigに用意する。
よくわからんけど、インスタンス生成してDataSourceだけ設定しとけば動くんじゃね?的な。
@Autowired private DataSource dataSource; @Bean public JdbcUserDetailsManager jdbcUserDetailsManager() throws Exception { JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(); jdbcUserDetailsManager.setDataSource(dataSource); return jdbcUserDetailsManager; }
問題なく起動できた。
でも、DBにユーザーが存在しないのでログインできない。
/newuser から新規ユーザー登録する。
新規ユーザー登録後は、USERSテーブルに登録されているのが確認できる。パスワードも暗号化されてる。
というわけで、ユーザー登録は成功。
登録したユーザーでログインすると、/mypage に行けるようになった。
ユーザー認証も成功。
登録済みのユーザー名で再び登録しようとすると例外が発生する。
そのエラーハンドリングと、パスワード再入力のチェックを追加する。
@RequestMapping(value = "/newuser", method = RequestMethod.POST) public ModelAndView register( ModelAndView mav, @RequestParam("username") String username, @RequestParam("password") String password, @RequestParam("password2") String password2) { if (!password.equals(password2)) { mav.setViewName("newuser"); mav.addObject("error", "パスワードが一致していません。"); return mav; } UserBuilder users = User.withDefaultPasswordEncoder(); try { userManager.createUser(users.username(username).password(password).roles("USER").build()); mav.setViewName("login"); } catch (Exception e) { mav.setViewName("newuser"); mav.addObject("error", "ユーザー名は使用できません。:" + username); } return mav; }
これでJDBCを使用したユーザー登録とユーザー認証の動作ができるようになった。
WebSecurityConfig の全体。
package hoge; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.provisioning.JdbcUserDetailsManager; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired private DataSource dataSource; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/", "/newuser").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .and() .logout() .permitAll(); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication() .dataSource(dataSource); } @Bean public JdbcUserDetailsManager jdbcUserDetailsManager() throws Exception { JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(); jdbcUserDetailsManager.setDataSource(dataSource); return jdbcUserDetailsManager; } }
コントローラの全体。
package hoge; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.User.UserBuilder; import org.springframework.security.provisioning.JdbcUserDetailsManager; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @Controller public class MyController { @Autowired private JdbcUserDetailsManager userManager; @RequestMapping(value = "/") public String index() { return "index"; } @RequestMapping(value = "/login") public String login() { return "login"; } @RequestMapping(value = "/newuser", method = RequestMethod.GET) public String newuser() { return "newuser"; } @RequestMapping(value = "/newuser", method = RequestMethod.POST) public ModelAndView register( ModelAndView mav, @RequestParam("username") String username, @RequestParam("password") String password, @RequestParam("password2") String password2) { if (!password.equals(password2)) { mav.setViewName("newuser"); mav.addObject("error", "パスワードが一致していません。"); return mav; } UserBuilder users = User.withDefaultPasswordEncoder(); try { userManager.createUser(users.username(username).password(password).roles("USER").build()); mav.setViewName("login"); } catch (Exception e) { mav.setViewName("newuser"); mav.addObject("error", "ユーザー名は使用できません。:" + username); } return mav; } @RequestMapping(value = "/mypage") public String mypage() { return "mypage"; } }