utamaro’s blog

誰かの役に立つ情報を発信するブログ

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-Age1969-12-31T23:59:59.000Zになっているのが謎です。

どうみても過去です。