[Spring Security] Spring Security Basic - Configuration

들어가며

Spring security를 적용했을 때 변경되는 부분과, 어떻게 하면 원하는 부분에 security를 적용할 수 있는지에 대해서 알아보자.

  1. Spring security dependency만 추가했는데 Spring Security가 적용 되는 이유
  2. WebSecurityConfigurerAdapter를 통해서 설정 값을 변경하는 방법
  3. Authentication을 적용하는 방법

Spring security dependency만 추가했는데 Spring Security가 적용 되는 이유

Spring Security AutoConfiguration

  • Spring Security 관련 Dependency를 해당 프로젝트에 추가 해본 뒤에 web server를 띄운다면 모든 요청에 대해서 login 페이지가 나와서 요청 제한이 걸리는 것을 알 수 있다.
  • 아무런 설정을 추가하지 않았는데 어떻게 이런일이 발생하는지 확인을 해보자~

Spring Security Setting

  • 해당 부분을 테스트 해보기 위해서 Spring Security example을 만들어보자.
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-web'
    ...

	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
	testImplementation 'org.springframework.security:spring-security-test'
}
  • 가장 중요한 부분이 web, security를 추가 후 spring boot를 기동 후 / page에 접근하면 login 페이지로 redirect 되는 것을 알 수 있다.

spring-security-login

SecurityAutoConfiguration

@Configuration(proxyBeanMethods = false)
@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);
	}

}
  • 이유는 SpringBoot에서 제공해주는 SecurityAutoConfiguration에서 @ImportSpringBootWebSecurityConfiguration가 포함되어 있기 때문이다.
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
public class SpringBootWebSecurityConfiguration {

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

	}
}
  • SpringBootWebSecurityConfiguration에서는 @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) 일때 DefaultConfigurerAdapter를 생성하게 된다.
  • Dependency만 추가하게 되면, WebSecurityConfigurerAdapter를 bean으로 등록하는 로직이 없기때문에 DefaultConfigurerAdapter가 등록이 되어서 default Security가 적용이 된다.
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();
}
  • DefaultConfigurerAdapter에 http configure 설정이다.
  • 모든 요청에 대해서 인증을 받아야 하고, 인증이 없는 경우 formLogin을 하며, httpBasic을 이용하게 기본 셋팅이 들어가게 되어 있다.

WebSecurityConfigurerAdapter를 통해서 설정 값을 변경하는 방법

  • 실제 사용할때는 default를 사용하는 것이 아닌 원하는 설정을 해야 하니, SecurityConfig을 만들어 변경해보자~
@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .mvcMatchers("/", "/info", "/account/**").permitAll()
            .mvcMatchers("admin").hasAnyRole("ADMIN")
            .anyRequest().authenticated();

        http.formLogin();
        http.httpBasic();
    }
}
  • WebSecurityConfigurerAdapterBean으로 등록하게 되면, Springboot에서 만들어주는 DefaultConfigurerAdapter가 생성되지 않게 할 수 있다.
  • 따라서 WebSecurityConfigurerAdapter을 상속 받은 SecurityConfig를 만들고, @EnableWebSecurity annotation을 추가하여, @Configuration이 될 수 있도록 한다.
  • 그 후 protected void configure(HttpSecurity http) 부분을 override 하여 원하는 request에 요청을 제한할 수 있다.

Authentication 적용하는 방법

SecurityProperties User

  • Spring Security는 기본적으로 User를 만드는 로직이 포함되어 있는데, 어떻게 생성되는지 알아 보자.
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean(
		value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class },
		type = { "org.springframework.security.oauth2.jwt.JwtDecoder",
				"org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector" })
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());
	}
}
  • UserDetailsServiceAutoConfiguration@Conditional관련 조건에 의해서 해당 Configuration이 Bean으로 등록되면 InMemoryUserDetailsManager가 Bean으로 등록될 때 user가 등록되게 되어 있다.
  • 해당 default user의 정보는 SecurityProperties의 기본 값에 의해서 User 값이 채워져서 생성이 된다.
public static class User {
  private String name = "user";
  private String password = UUID.randomUUID().toString();
  private List<String> roles = new ArrayList<>();
  private boolean passwordGenerated = true;
  ...
  public void setPassword(String password) {
    if (!StringUtils.hasLength(password)) {
      return;
    }
    this.passwordGenerated = false;
    this.password = password;
  }
  • password가 UUID로 random하게 생성되는데 어떻게 로그인을 할 수 있는가? 궁금할 수 있다.
  • passwordGenerated = true로 생성된 경우, InMemoryUserDetailsManager에서 password를 등록할 때 console에 password를 찍어 주도록 되어 있어서 password를 확인할 수 있게 되어 있다.
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;
}
  • default user로만 사용하는 경우 User가 1개만 등록되고, 매번 Password가 바뀌게 되어 있어, 여러명의 유저와 원하는 유저를 등록하는 방법에 대해서 알아보자.

InMemoryUserDetailsManager

  • InMemoryUserDetailsManagerUser 정보를 등록하면 사용가능하다는 것을 앞에서 확인하였다.
  • InMemoryUserDetailsManagerUserDetailsService을 상속 받은 class이기에 UserDetailsServiceAutoConfiguration이 동작하지 않게 된다.
  • WebSecurityConfigurerAdapterprotected void configure(AuthenticationManagerBuilder auth) 부분을 overriding하여 유저 정보를 등록할 수 있다.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  auth.inMemoryAuthentication()
      .withUser("woojin").password("{noop}123").roles("USER").and()
      .withUser("admin").password("{noop}admin").roles("ADMIN");
}
  • 해당 방식은 서버가 가동될 때 유저 정보가 등록되기에 동적으로 유저 정보를 추가 할 수 없는 문제가 있다.
  • 다음으로는 User정보를 database와 같은 저장소로부터 읽어들여, 인증 하는 방법을 확인해보자.

UserDetailsService

  • UserDetailsService가 Bean으로 등록하게 되면 UserDetailsServiceAutoConfiguration이 동작하지 않는것을 확인했었다.
  • UserDetailsService는 method 하나를 갖고 있는 interface이다.
  • UserDetailsService의 역할은 username을 받아서, UserDetails를 만들어 return 해주는 역할이다.
public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
  • 해당 부분을 응용해서, username으로 user를 조회하고 UserDetails를 만드는 Service를 구현하면 된다.
    @Service
    @RequiredArgsConstructor
    public class AccountService implements UserDetailsService {
      private final AccountRepository accountRepository;
    
      @Override
      public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
          final Account account = accountRepository.findByUsername(username);
          if (account == null) {
              throw new UsernameNotFoundException(username);
          }
          return User.builder()
                     .username(account.getUsername())
                     .password(account.getPassword())
                     .roles(account.getRole())
                     .build();
      }
    }
    
  • 여기서 login form창에서 입력한 username을 전달 받아서 해당 username으로 database와 같은 저장소에 있는 유저를 가져와서 데이터를 셋팅해주면 된다.

마치며

  • 백기선의 Spring Security를 보면서 정리한 내용이라서 이해하기 어려울 수 있다.. 까먹지 않기 위한 기록용이다..
  • 궁금한 부분만 찾아보고 정리해보았는데.. 정확한 내용이 아닐 수 있으니 참고해야 한다..

Leave a comment