【Spring Security】DBから認証情報を取得して認証するのをやってみる

DBから認証情報を取得して認証する場合はSpring Securityを使ってカスタムの認証プロバイダ(AuthenticationProvider)を実装することが一般的です

この認証プロバイダはユーザー名とパスワードを取得し、DBに格納された認証情報と照合して認証を行います

以下にDBから認証情報を取得して認証するための実装例を紹介します


1. こんなテーブルを準備

まず、ユーザー情報を格納するためのデータベーステーブルを準備します
例えば、usersテーブルを作成し、ユーザー名とパスワード(暗号化されていることが一般的)を保存します

CREATE TABLE users (
    username VARCHAR(50) PRIMARY KEY,
    password VARCHAR(100)
);

2. UserDetailsServiceの実装

次に、Spring SecurityのUserDetailsServiceを実装して、DBからユーザー情報を取得するクラスを作成します

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
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 javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private DataSource dataSource;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try (Connection conn = dataSource.getConnection()) {
            PreparedStatement stmt = conn.prepareStatement("SELECT username, password FROM users WHERE username = ?");
            stmt.setString(1, username);
            ResultSet rs = stmt.executeQuery();

            if (rs.next()) {
                String dbUsername = rs.getString("username");
                String dbPassword = rs.getString("password");

                return User.withUsername(dbUsername)
                           .password(dbPassword)
                           .roles("USER") // ロールを設定(必要に応じて)
                           .build();
            } else {
                throw new UsernameNotFoundException("User not found with username: " + username);
            }
        } catch (SQLException e) {
            throw new RuntimeException("Error accessing database", e);
        }
    }
}

このCustomUserDetailsServiceクラスではUserDetailsServiceを実装してloadUserByUsernameメソッドをオーバーライドしています
このメソッド内でDBからユーザー情報を取得し、UserDetailsオブジェクトを構築して返します


3. SecurityConfigの設定

SecurityConfigクラスでカスタムのUserDetailsServiceを設定します

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;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .httpBasic();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

configure(AuthenticationManagerBuilder auth)メソッドで、CustomUserDetailsServiceを設定し、passwordEncoder()メソッドでパスワードエンコーダー(例えばBCryptPasswordEncoder)を設定します

このようにして実装していきます


4. Spring Securityで投げられる例外とHTTPレスポンス

UsernameNotFoundException

ユーザー名が見つからない場合にスローされます
デフォルトのHTTPレスポンスコードは401 (Unauthorized)です

BadCredentialsException

資格情報が無効な場合(例えば、間違ったパスワード)にスローされます
デフォルトのHTTPレスポンスコードは401 (Unauthorized)です

その他の認証例外

その他の認証関連の例外(例えば、アカウントがロックアウトされている場合の LockedException など)に対しても、デフォルトのHTTPレスポンスコードは401 (Unauthorized)です

アクセス拒否(AccessDeniedException)

認証は成功したが、リソースにアクセスする権限がない場合にスローされます
デフォルトのHTTPレスポンスコードは403 (Forbidden)です

これらのデフォルトの動作はカスタマイズ可能

exceptionHandling()メソッドを使用してカスタムのエラーハンドリングやエラーページを設定することで、異なるレスポンスを返すことができます

以下は実装例です

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .anyRequest().authenticated()
            .and()
        .formLogin()
            .loginPage("/login")
            .permitAll()
            .and()
        .logout()
            .logoutUrl("/logout")
            .logoutSuccessUrl("/login?logout")
            .permitAll()
            .and()
        .exceptionHandling()
            .accessDeniedPage("/access-denied") // アクセス拒否時のページ
            .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)); // 認証エラー時のHTTPステータス
}

上記の例では、.accessDeniedPage(“/access-denied”)によってアクセス拒否時にはカスタムのエラーページ/access-deniedにリダイレクトされます
また、.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))によって認証エラー時にはデフォルトのHTTPステータスコードとして401 (Unauthorized)が返されるように設定されています


いかがでしたでしょうか?

他にもいろんなことができます

是非参考ください

是非フォローしてください

最新の情報をお伝えします