SpringSecurityを使ったシンプルな認証の作り方
spring securityの使い方
spring securityを使用して認証機能を実装するまでです。
※ 少しだけ認可するためのコードを書いています。
この記事ではDBへの接続は行わずにコードを書いています。
Spring Securityの実装方法を調べると、インメモリDBを使う方法や、jdbcを使う方法、mybatisを使う方法など様々な方法があります。
ただ、これらが関わると分かりづらい(理解しづらい)ので、省いて作ります。
準備
spring initializerを使ってプロジェクトを用意したあと、以下の依存関係を追加します。
※ spring-boot-starter-securityの中にspring-securityを使うための依存関係が含まれています。
※ 個別に管理したい場合はリンク先を開いて、追加してください。
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security 'org.springframework.boot:spring-boot-starter-security:2.0.3.RELEASE', // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf 'org.springframework.boot:spring-boot-starter-thymeleaf:2.0.3.RELEASE',
thymeleafの用意
application.ymlか、application.propertiesに以下の内容を追記します。
spring: thymeleaf: cache: false check-template: true check-template-location: true content-type: text/html enabled: true encoding: UTF-8 mode: HTML prefix: classpath:/templates/ suffix: .html
この設定は、https://memorynotfound.com/spring-boot-thymeleaf-configuration-example/ こちらのサイトを参考にしました。
templateファイルを用意する
resourceフォルダ以下にtemplatesフォルダを用意します。
templatesフォルダの中に、login.htmlとhome.htmlを用意します。 - login.html: ログイン用のページ - home.html: ログイン処理後のホーム画面(ログインしないと見れないページ)
それぞれのファイルは以下の内容を書きます。
login.html
<!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form th:action="@{/login}" method="post"> <fieldset> <h1>Please Sign In</h1> <div th:if="${param.error}"> <div> Invalid username and password. </div> </div> <div th:if="${param.logout}"> <div> You have been logged out. </div> </div> <div> <input type="text" name="username" id="username" placeholder="UserName" required="true" autofocus="true"/> </div> <div> <input type="password" name="password" id="password" placeholder="Password" required="true"/> </div> <div> <div> <input type="submit" value="Sign In"/> </div> </div> </fieldset> </form> </body> </html>
home.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> home view </body> </html>
これでhtmlファイルの準備は完了です。
spring securityの設定について
以下の5ファイルを作ります。
- UserController
- SecurityConfig.java
- CustomUserDetailsModel
- CustomUserDetialsService
- GrantedAuthorityCode
それぞれのファイルのソースコードを書きます。
UserController.java
// package ry...; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; // @Controllerを使って画面を表示するためのpathを作ります @Controller public class UserController { @RequestMapping(path = "/login") // methodの指定が無いとGET methodが対象になります。 public String loginView() { // resources/templates/login.htmlが開かれます。 return "login"; } @RequestMapping(path = "/home") public String homeView() { // 本来はHomeControllerのようなファイルを作ること // 今回はファイル数を減らして、わかりやすくするために同じControllerに作ってます。 return "home"; } }
SecurityConfig.java
// package ry...; import com.example.starter.app.user.service.CustomUserDetailsService; 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.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomUserDetailsService customUserDetailsService; @Override protected UserDetailsService userDetailsService() { // 独自に作ったUserDetailsServiceを実行してもらうようにします return customUserDetailsService; } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest() .authenticated() .and() .formLogin() .loginPage("/login") .usernameParameter("email") .permitAll() .defaultSuccessUrl("/home", true) .and() .logout() .permitAll() .logoutUrl("/logout") .deleteCookies("JSESSIONID") ; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(customUserDetailsService) .passwordEncoder(passwordEncoder()) ; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
CustomUserDetailsModel.java
// package ry...; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User; import java.util.Collection; public class CustomUserDetailsModel extends User { public CustomUserDetailsModel(String username, String password, Collection<? extends GrantedAuthority> authorities) { // Userクラスを継承しているので、とてもシンプルな実装になっています。 // すべてを実装したい場合は、UserDetailsクラスをimplementsすると良いです。 super(username, password, authorities); } }
CustomUserDetialsService.java
// package ry...; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.Objects; @Service public class CustomUserDetialsService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; // passwordEncoderはSecurityConfigクラスでBeanを定義しているのでAutowiredできます。 // AutowiredするにはSecurityConfigクラスに@Configurationが必要です。 @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { if (Objects.isNull(email)) { throw new UsernameNotFoundException("error"); } if (email.equals("test@test.com")) { // DBがかかわらないようにしたかったので、email, passwordともにべた書きです。 // DBからselectしたデータを使っても良いです。 String password = passwordEncoder.encode("password"); // ログインしたユーザの権限を設定しています。 List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(GrantedAuthorityCode.ADMIN); // ↓のコードが実行されると、spring側がpasswordの検証までやってくれます。 // 検証が成功すると、ログイン成功時のページにリダイレクトされます。 return new CustomUserDetailsModel(email, password, authorities); } throw new UsernameNotFoundException("user not found."); } }
GrantedAuthorityCode.java
// package ry...; import org.springframework.security.core.GrantedAuthority; public enum GrantedAuthorityCode implements GrantedAuthority { // 権限ごとにクラスを作ってGrantedAuthorityを実装するよりも、 // enumでまとめて管理した方が簡単です。 ADMIN("ADMIN"), ; private String authority; GrantedAuthorityCode(String authority) { this.authority = authority; } @Override public String getAuthority() { return authority; } }
次にやるべきこと
DBにハッシュ化したパスワードを保存して、認証できるようにすること。
これは結構簡単にできます。
※ ヒントです
BCryptPasswordEncoderを使ってエンコードしているので、DBにパスワードを保存する際はハッシュ化して保存します。
String hashPassword = passwordEncoder.encode("test");
もしくは
String salt = BCrypt.gensalt();
String hashPassword = BCrypt.hashpw("test", salt);
でハッシュ化できます。
spring securityを使ったパスワードの検証では、passwordEncoder.match(A, hashA)
で検証されているので、
DBにsaltキーを保存する必要はありません。
感想
spring securityを使った認証は苦労しましたが、振り返ると結構簡単に実装できると思いました。
ですが、実装するまで調べたりすることが多かったので、生産性はDjango,railsより劣ると思いました。
(慣れの問題かもしれませんが)
javaのコーディングは補完が強力なこと、事前にコンパイルされるので、ヌルポ以外でエラーが起きづらいといったことが助かりました。
...
認証後に生成されたJSESSIONIDのExpires / Max-Age
が1969-12-31T23:59:59.000Z
になっているのが謎です。
どうみても過去です。