DB連携認証処理


実習環境

build.gradle以下
plugins {
	id 'org.springframework.boot' version '2.6.4'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'me.ramos'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
	implementation 'org.modelmapper:modelmapper:2.3.0'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2'
	runtimeOnly 'mysql:mysql-connector-java'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

PasswordEncoder

  • パスワードを安全に暗号化する機能を提供する.
  • Spring Security 5.0以前サポート基本PasswordEncoder加評文NoOpPasswordEncoder
  • 作成
  • PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
  • 複数のPasswordEncoderタイプを発表した後、状況に応じて使用するEncoderを選択できる.
  • 暗号化形式:{id}符号化Password
  • 基本形式はBcrypt:{bcrypt}$2a$10$dXJ3SW6G7P50IGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
  • アルゴリズム種別:bcrypt、noop、pbkdf 2、scrypt、sha 256
  • コネクタ
  • encode(password):暗号化
  • matches(rawPassword, encodedPassword):パスワード比較
  • DB連携認証処理

    inMemoryAuthentication()実際のデータベースではなくアカウント連動を作成する.
    現在はForm方式で書かれているが、JWTやOAuth 2方式の基本的な基礎もそうである.

    CustomUserDetailsService



    会員登録時には、ユーザ情報をユーザ詳細情報タイプに作成して返却するため、CustomUserDetailsServiceを実施した.『UserDetailsService』の実施を受け、loadUserByUsername()ユーザエンティティ(Account)と大げさにマッチングする.
    package me.ramos.securitystudy.security.service;
    
    import lombok.RequiredArgsConstructor;
    import me.ramos.securitystudy.domain.Account;
    import me.ramos.securitystudy.repository.UserRepository;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    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 java.util.ArrayList;
    import java.util.List;
    
    @Service("userDetailsService")
    @RequiredArgsConstructor
    public class CustomUserDetailsService implements UserDetailsService {
    
        private final UserRepository userRepository;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    		// DB에서 Account 객체 조회
            Account account = userRepository.findByUsername(username);
    
            if (account == null) {
                throw new UsernameNotFoundException("UsernameNotFoundException");
            }
    		
            // 권한 정보 등록
            List<GrantedAuthority> roles = new ArrayList<>();
            roles.add(new SimpleGrantedAuthority(account.getRole()));
    
    		// AccountContext 생성자로 UserDetails 타입 생성
            AccountContext accountContext = new AccountContext(account, roles);
    
            return accountContext;
        }
    }

    AccountContext

    CustomUserDetailsService最終返却すべきUserDetailsタイプ.したがって、作成AccountContext、作成UserDetails型でオブジェクトを作成する実装体が必要となる.
    Spring SecurityのUserクラスを継承し、ジェネレータを実装します.
    package me.ramos.securitystudy.security.service;
    
    import me.ramos.securitystudy.domain.Account;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.User;
    
    import java.util.Collection;
    
    public class AccountContext extends User {
    
        private final Account account;
    
        public AccountContext(Account account, Collection<? extends GrantedAuthority> authorities) {
            super(account.getUsername(), account.getPassword(), authorities);
            this.account = account;
        }
    
        public Account getAccount() {
            return account;
        }
    }

    SecurityConfig設定

    CustomUserDetailsServiceSecurityConfig設定類に登録して使用する.
    package me.ramos.securitystudy.security.configs;
    
    import lombok.RequiredArgsConstructor;
    import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
    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.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.factory.PasswordEncoderFactories;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    @Configuration
    @RequiredArgsConstructor
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        private final UserDetailsService userDetailsService;
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService);
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return PasswordEncoderFactories.createDelegatingPasswordEncoder();
        }
    
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .antMatchers("/", "/users", "user/login/**").permitAll()
                    .antMatchers("/mypage").hasRole("USER")
                    .antMatchers("/messages").hasRole("MANAGER")
                    .antMatchers("/config").hasRole("ADMIN")
                    .anyRequest().authenticated()
    
                    .and()
                    .formLogin();
        }
    }
    📌 SecurityConfigDI受け入れ時の質問事項?CustomUserDetailsService商団に貼られている@Service("userDetailsService")説明書.その後、SecurityConfigのコードにより、UserDetailsServiceを注入する方式はCustomUserDetailsServiceではなく、UserDetailsServiceの名称である.
    一般に、@Service("userDetailsService")などのように、値を指定しないと、対応するクラス名の空が生成されます.したがって、CustomerDetailsServiceの名前で空の登録が行われます.
    ここに複数のUserDetailsServiceタイプによって生成された空があると、タイプ重複によりエラーが発生します.ただし、上記のサンプルコードでは、この名前で生成された空は1つしかないので、DIを行う際にエラーは発生しません.

    AuthenticationProvider



    まず生成CustomAuthenticationProvider
    package me.ramos.securitystudy.security.provider;
    
    import me.ramos.securitystudy.security.service.AccountContext;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.AuthenticationProvider;
    import org.springframework.security.authentication.BadCredentialsException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    public class CustomAuthenticationProvider implements AuthenticationProvider {
    
        @Autowired
        private UserDetailsService userDetailsService;
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    
            String username = authentication.getName();
            String password = (String) authentication.getCredentials();
    
            AccountContext accountContext = (AccountContext) userDetailsService.loadUserByUsername(username);
    
            if (!passwordEncoder.matches(password, accountContext.getAccount().getPassword())) {
                throw new BadCredentialsException("BadCredentialsException");
            }
    
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(accountContext.getAccount(), null, accountContext.getAuthorities());
    
            return authenticationToken;
        }
    
        @Override
        public boolean supports(Class<?> authentication) {
            return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
        }
    }
    まず、UsernamePasswordAuthenticationTokenは、実現Authenticationインタフェースの認証対象である.Flowはわかりにくいが、核心はAuthenticationオブジェクトから認証に必要な情報(ユーザ名、パスワードなど)を取得し、userDetailsServiceインタフェースを実現するオブジェクト(CustomUserDetailsService)からデータベースに格納ユーザ情報を取得した後、passwordを比較し、検証が完了した後に検証が完了した認証オブジェクトを返します.
    以降、SecurityConfigを以下のように修正する.
    package me.ramos.securitystudy.security.configs;
    
    import lombok.RequiredArgsConstructor;
    import me.ramos.securitystudy.security.provider.CustomAuthenticationProvider;
    import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationProvider;
    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.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.factory.PasswordEncoderFactories;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.authenticationProvider(authenticationProvider());
        }
    
        @Bean
        public AuthenticationProvider authenticationProvider() {
            return new CustomAuthenticationProvider();
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return PasswordEncoderFactories.createDelegatingPasswordEncoder();
        }
    
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .antMatchers("/", "/users", "user/login/**").permitAll()
                    .antMatchers("/mypage").hasRole("USER")
                    .antMatchers("/messages").hasRole("MANAGER")
                    .antMatchers("/config").hasRole("ADMIN")
                    .anyRequest().authenticated()
    
                    .and()
                    .formLogin();
        }
    }

    References

  • 整数要素に基づくスプリング安全‐スプリング・ボot開発のSpring Security