Spring BootでTodoアプリ1 -ログイン機能作成-
前置き
以前Spring徹底入門を読んでから、業務で触れることなかったのでいったんSpring勉強してなかった。
週末にまとまった勉強時間が取れるようになって、やりたい勉強できるようになったのでSpring Bootでアプリ作ってみた。
相も変わらず作成したアプリはHerokuにアップ。
Spring徹底入門はリファレンスとしてアプリ作りながら読むとすごく勉強になる。
アプリの内容
Todoアプリ。ログイン機能とTodoを作る。
DBはHerokuで使用できるPostgreSQL。
本記事ではログイン機能を作成。
テーブル作成
Todoアプリ用DBを作って、ユーザー管理テーブルを作成。
CREATE DATABASE tododb; DROP TABLE IF EXISTS usr CASCADE; CREATE TABLE IF NOT EXISTS usr( user_id VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, role_name VARCHAR(255) NOT NULL, PRIMARY KEY (user_id) );
インサートするデータはこんな感じ。
INSERT INTO usr (user_id, first_name, last_name, role_name, password) VALUES ('user', '太郎', '山田', 'USER', '$2a$10$ZqU0cizLk7c6YEydWPfpr.3Gmomn5JtYOfBqEExy3muSNKm3sZY7a');
passwordはBCryptでハッシュ値に変換したものを入れとく。
上記のパスワードは「password」をハッシュ化したもの。
ハッシュ化は下記のサイトで計算できる。(ありがたい)
BCrypt ハッシュ値 計算 | tekboy
ストレッチング回数は10回に指定してハッシュ化。
プロジェクト作成
eclipseで開発。最近のpleiadesならSTS標準で入ってるはず。
Spring Boot 開発環境構築 Eclipse STS インストール - 公式ガイド
プロジェクトを作成。
pom.xmlのdependenciesタグ内にSpring Securityを追加。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency>
application.propertiesにPostgreSQLの接続情報を追加。
ついでにログ出力も設定。
# DB接続設定 spring.datasource.url=jdbc:postgresql://localhost:5432/tododb spring.datasource.username=postgres spring.datasource.password=postgres spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.hibernate.ddl-auto=none spring.jpa.show-sql=true # ログ出力の設定 logging.file.name=C:/logs/todoSpring.log #logging.level.org.springframework.web=DEBUG logging.level.org.springframework.web=INFO
HTMLの作成
ログイン画面のHTMLを作成。
「/SpringTodo/src/main/resources/templates」内にlogin.htmlを作成。
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <head> <meta charset="UTF-8"> <title>Login Form</title> </head> <body> <div th:if="${param.error}"> Invalid username and password. </div> <div th:if="${param.logout}"> You have been logged out. </div> <form th:action="@{/login}" method="post"> <div><label> User Name : <input type="text" name="username" /></label></div> <div><label> Password : <input type="password" name="password" /></label></div> <div><input type="submit" value="Sign In"></div> </form> </body> </html>
ログインが成功したときの遷移先を作成。
todo.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <head> <title>Hello World!</title> </head> <body> <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1> <form th:action="@{/logout}" method="post"> <input type="submit" value="Sign Out"/> </form> </body> </html>
Javaを書く
作成するソースは下記。
/SpringTodo/src/main/java/nyata/
- MvcConfig.java
- SpringTodoApplication.java(今回は編集しない)
- WebSecurityConfig.java
/SpringTodo/src/main/java/nyata/domain/model
- User.java
- RoleName.java
/SpringTodo/src/main/java/nyata/domain/repositoriy
- UserRepository.java
/SpringTodo/src/main/java/nyata/domain/service
- TodoUserDetails.java
- TodoUserDetailsService.java
MvcConfig.java
URLの遷移先を管理
package nyata; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * View Controller * @author nyata */ @Configuration public class MvcConfig implements WebMvcConfigurer { public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/login").setViewName("login"); registry.addViewController("/").setViewName("login"); registry.addViewController("/todo").setViewName("todo"); } }
WebSecurityConfig.java
Spring Securityの設定情報。
パスワードのハッシュ化としてBCryptを使用。
package nyata; 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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import nyata.domain.service.TodoUserDetailsService; /** * Security Config * @author nyata */ @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired TodoUserDetailsService userDetailsService; @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/js/**", "/css/**").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .usernameParameter("username") .passwordParameter("password") .defaultSuccessUrl("/todo", true) .permitAll() .and() .logout() .permitAll(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } }
RoleName.java
package nyata.domain.model; /** * ユーザー権限一覧 * @author nyata */ public enum RoleName { ADMIN, USER }
User.java
package nyata.domain.model; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.Id; import javax.persistence.Table; import org.hibernate.annotations.Proxy; /** * ユーザー情報のエンティティ * @author nyata */ @Entity @Proxy(lazy = false) @Table(name = "usr") public class User { /* ユーザーID */ @Id private String userId; /* パスワード */ private String password; /* ファーストネーム */ private String firstName; /* ラストネーム */ private String lastName; /* ユーザー権限 */ @Enumerated(EnumType.STRING) private RoleName roleName; // setter, getter public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public RoleName getRoleName() { return roleName; } public void setRoleName(RoleName roleName) { this.roleName = roleName; } }
UserRepository.java
package nyata.domain.repositoriy; import org.springframework.data.jpa.repository.JpaRepository; import nyata.domain.model.User; /** * ユーザー情報のリポジトリ * @author nyata */ public interface UserRepository extends JpaRepository<User, String> { }
TodoUserDetails.java
ユーザー認証の設定
package nyata.domain.service; import java.util.Collection; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.UserDetails; import nyata.domain.model.User; /** * ユーザー認証情報 * @author nyata */ public class TodoUserDetails implements UserDetails { private final User user; public TodoUserDetails(User user) { this.user = user; } public User getUser() { return user; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return AuthorityUtils.createAuthorityList("ROLE_" + this.user.getRoleName().name()); } @Override public String getPassword() { return this.user.getPassword(); } @Override public String getUsername() { return this.user.getUserId(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
TodoUserDetailsService.java
ユーザー認証処理を受け持つ
package nyata.domain.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import nyata.domain.model.User; import nyata.domain.repositoriy.UserRepository; /** * ユーザー認証のサービス * @author nyata */ @Service public class TodoUserDetailsService implements UserDetailsService { @Autowired UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.getOne(username); if (user == null) { throw new UsernameNotFoundException(username + " is not found."); } return new TodoUserDetails(user); } }
ひとまずこれでログイン機能完成。次はTodo機能を作っていく。
参考文献
Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発(株式会社NTTデータ)|翔泳社の本
Spring Boot ログイン画面 - 公式サンプルコード
BCrypt ハッシュ値 計算 | tekboy
Spring Bootでログを出力する | DevelopersIO
SpringSecurity-AuthenticatedPricipalは非推奨 - Javaer101