Spring boot Security

dependency 추가

implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

Spring boot Security 자동 설정

자동 설정 클래스는 SecurityAutoConfiguration이다.
SecurityAutoConfiguration은 SpringBootWebSecurityConfiguration 클래스를 import하고 있다.

@Configuration
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({ SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,
		SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean(AuthenticationEventPublisher.class)
	public DefaultAuthenticationEventPublisher authenticationEventPublisher(
			ApplicationEventPublisher publisher) {
		return new DefaultAuthenticationEventPublisher(publisher);
	}

}

SpringBootWebSecurityConfiguration은 WebSecurityConfigurerAdapter를 상속받아서 오버라이드 하지 않고 사용하고 있다.

@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
public class SpringBootWebSecurityConfiguration {

	@Configuration
	@Order(SecurityProperties.BASIC_AUTH_ORDER)
	static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {

	}

}

Spring Security을 추가하게 되면 기본적으로 모든 경로에 인증이 필요한데 WebSecurityConfigurerAdapter의 기본설정이 그렇기 때문이다.

public abstract class WebSecurityConfigurerAdapter implements
		WebSecurityConfigurer<WebSecurity> {
    ...
    protected void configure(HttpSecurity http) throws Exception {
        logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .formLogin().and()
            .httpBasic();
    }
    ... 
}

user라는 사용자가 자동으로 추가되는 이유는 spring-boot-autoconfigure의 security쪽 자동 설정중 하나인 UserDetailsServiceAutoConfiguration의 역할이다.

@Configuration
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean({ AuthenticationManager.class, AuthenticationProvider.class,
		UserDetailsService.class })
public class UserDetailsServiceAutoConfiguration {
    ...
    @Bean
    @ConditionalOnMissingBean(type = "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository")
    @Lazy
    public InMemoryUserDetailsManager inMemoryUserDetailsManager(
            SecurityProperties properties,
            ObjectProvider<PasswordEncoder> passwordEncoder) {
        SecurityProperties.User user = properties.getUser();
        List<String> roles = user.getRoles();
        return new InMemoryUserDetailsManager(User.withUsername(user.getName())
                .password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
                .roles(StringUtils.toStringArray(roles)).build());
    }

    private String getOrDeducePassword(SecurityProperties.User user,
            PasswordEncoder encoder) {
        String password = user.getPassword();
        if (user.isPasswordGenerated()) {
            logger.info(String.format("%n%nUsing generated security password: %s%n",
                    user.getPassword()));
        }
        if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
            return password;
        }
        return NOOP_PASSWORD_PREFIX + password;
    }
    ...
}

Spring Security 커스터마이징

웹 시큐리티와 PasswordEncoder 설정

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // "/", "/hello"는 아무나 접근 가능하다.
                .antMatchers("/", "/hello").permitAll()
                // "/", "/hello" 외의 페이지는 인증된 사용자만 접근 가능하다.
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .and()
            .httpBasic();
    }

    // PasswordEncoder 설정
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

PasswordEncoderFactories.createDelegatingPasswordEncoder()는 DelegatingPasswordEncoder를 생성한다.
여러가지 이유로 암호화 알고리즘을 변경해야 하는데 DelegatingPasswordEncoder가 알아서 적당한 알고리즘을 적용해준다.
Spring Security 5.3.4 기준으로 Bcrypt가 기본 암호화 알고리즘으로 사용된다.

UserDetailsService 구현

Account Entity

@Entity
public class Account {
    
    @Id @GeneratedValue
    private Long id;

    private String username;

    private String password;
    ...
}

Account Repository

public interface AccountRepository implements JpaRepository<Account, Long> {
    Optional<Account> findByUsername(String username);
}

Account Service

@Service
public class AccountService implements UserDetailsService {

    private final AccountRepository accountRepository;
    private final PasswordEncoder passwordEncoder;
    
    public AccountService(AccountRepository accountRepository, PasswordEncoder passwordEncoder) {
        this.accountRepository = accountRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @Transactional
    public Account signup(String username, String password) {
        Account account = new Account(
            username,
            passwordEncoder.encode(password)
        );

        return accountRepository.save(account);
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<Account> account = accountRepository.findByUsername(username).orElseThrow(
                () -> new UsernameNotFoundException(username)
        );
       
        return new User(account.getUsername(), account.getPassword(), authorities());
    }

    private Collection<? extends GrantedAuthority> authorities() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
    }
}