스프링 시큐리티 활성화하기

<dependency> 
	<groupId>org.springframework.boot</groupId> 
	<artifactId>spring-boot-starter-security</artifactId> 
</dependency>
  • Spring 자동 구성은 다음과 같이 구성된다.
    • 모든 HTTP 요청 경로는 인증되어야 한다.
    • 어떤 특정 역할이나 권한이 없다.
    • 스프링 시큐리티의 기본 HTTP 기본 인증을 사용해서 인증된다.
    • 사용자는 하나만 있으며, 이름은 user다. 비밀번호는 암호화해 준다.
  • 시큐리티가 활성화되면 기본적으로 설정되는 프로퍼티가 있다.
spring.security.user.name
spring.security.user.password
  • 따로 설정하지 않았으면 비밀번호가 랜덤으로 생성되면 콘솔 로그에 출력된다.

  • 페이지로 접속하면 HTTP 기본 인증 대화상자로 리다이렉트 되는 것을 확인할 수 있다.

스프링 시큐리티 구성하기

자동 구성 비활성화

@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])  
class StudyApplication
  • 이 경우 다른 자동 구성 클래스가 필요할 수도 있어, 실행이 되지 않을 수 있다.
    • 이를 해결하려면 ManagementWebSecurityAutoConfiguration도 제외시켜야된다.

자동 구성 비활성화 vs 덮어쓰기

  • 자동 구성을 비활성화 하는 하는 것은 Spring Security 의존을 추가하고 모든 설정을 처음부터하는 것과 같다.
  • 자동 구성을 비활성화하는 경우가 유용한 경우
    • 별도의 security provider을 Spring security와 통합할 때
    • 레거시 Spring 애플리케이션을 Spring Boot로 마이그레이션 중일 때
  • 그외에는 대부분 비활성화하지 않는 것이 좋다.
    • 자동 구성이 custom configuration 클래스를 추가해준다.
    • 기본 보안 설정에 커스텀할 부분만 작성하면 되기 때문에 더 간단하다.

구성하기

@EnableWebSecurity  
@Configuration  
class SecurityConfig {  
  
   @Bean  
   fun userDetailsService(passwordEncoder: PasswordEncoder): UserDetailsManager {  
      val user: UserDetails = User.withUsername("user1")  
         .password("{noop}password1")  
         .authorities("ROLE_USER")  
         .build()  
  
      return InMemoryUserDetailsManager(user)  
   }  
  
   @Bean  
   fun filterChain(http: HttpSecurity): SecurityFilterChain {  
      http.authorizeRequests()  
         .antMatchers("/design", "/orders")  
         .access("hasRole('ROLE_USER')")  
         .antMatchers("/", "/**")  
         .access("permitAll")  
         .and()  
         .httpBasic()  
      return http.build()  
   }  
  
   @Bean  
   fun passwordEncoder(): PasswordEncoder {  
      return PasswordEncoderFactories.createDelegatingPasswordEncoder()  
   }  
}
  • @EnableWebSecurity: 자동 구성을 비활성화하면 필요하다.
  • Spring Boot 2에서는 패스워드를 인코딩하기 위한 PasswordEncoder가 필요하다.
  • InMemoryUserDetailsManager: 유저별 데이터를 로드하기 위한 인터페이스인 UserDetailsService의 in-memory map 구현체
  • SecurityFilterChain: 해당 보안을 적용할지 여부를 결정하기위한 필터 체인.
  • 스프링 5부터는 비밀번호를 암호화해야 하므로 password() 메서드를 호출하여 암호화하지 않으면 403 또는 500 HTTP 응답이 발생한다.
    • 간단한 테스트롤 위해 {noop}을 지정하여 비밀번호를 암호화하지 않았다.
  • 스프링 시큐리티에서는 여러 가지의 사용자 스토어 구성 방법을 제공한다.
    • 인메모리 사용자 스토어
    • JDBC 기반 사용자 스토어
    • LDAP 기반 사용자 스토어
    • 커스텀 사용자 명세 서비스

JDBC 기반 사용자 스토어

  • 사용자 정보는 관계형 데이터베이스로 관리되는 경우가 많으므로 JDBC 기반의 사용자 스토어를 주로 사용한다.
  • 인메모리 사용자 스토어 구성에서 UserDetailsManager만 수정하면된다.
    • DataSource를 자동 주입으로 가져온다.
    • JdbcUserDetailManager 생성자를 호출시에 DataSource가 필요하다.
@Bean  
fun userDetailsService(passwordEncoder: PasswordEncoder): UserDetailsManager {  
   val user: UserDetails = User.withUsername("user1")  
      .password("{noop}password1")  
      .authorities("ROLE_USER")  
      .build()  
  
   val jdbcUserDetailsManager = JdbcUserDetailsManager(dataSource)  
   jdbcUserDetailsManager.createUser(user)  
   return jdbcUserDetailsManager  
}
val jdbcUserDetailsManager = JdbcUserDetailsManager(dataSource).apply {  
   createUser(user)  
   setUsersByUsernameQuery("select username,password,enabled from users where username = ?")  
   setAuthoritiesByUsernameQuery("select username,authority from authorities where username = ?")  
}
  • PasswordEncoder: 비밀번호를 안전하게 저장하기위해 단방향 인코딩을 제공해주는 인터페이스
    • encode(): raw 패스워드를 인코딩하는 메서드
    • match(): 제출한 raw 패스워드를 인코딩해서, 인코딩되어 있는 패스워드와 일치하는지 비교
  • DelegatingPasswordEncoder: Spring security 에서 제공해주는 PasswordEncoder. 다음의 기능을 제공해준다.
  • 커스텀 PasswordEncoder를 사용하려면, PasswordEncoder을 구현하고, bean으로 등록하면된다.

사용자 인증의 커스터마이징

  • UserDetailsService의 구현체를 bean으로 등록하면 커스터 마이징된 사용자 인증이 가능하다.
@Service  
class UserRepositoryUserDetailsService(  
   val userRepository: UserRepository  
): UserDetailsService {  
  
   override fun loadUserByUsername(username: String): UserDetails {  
      return userRepository.findByUsername(username)  
         ?: throw UsernameNotFoundException("User '${username}' not found")  
   }  
}

웹 요청 보안 처리하기

웹 요청 보안 처리하기

  • HttpSecurity를 통해서 다음을 구성할 수 있다.
    • HTTP 요청 처리를 허용하기 전에 충족되어야 할 특정 보안 조건을 구성할 수 있다.
    • 커스텀 로그인 페이지를 구성한다.
    • 사용자가 애플리케이션의 로그아웃을 할 수 있도록 한다.
    • CSRF 공격으로부터 보호하도록 구성한다.
@Bean  
fun filterChain(http: HttpSecurity): SecurityFilterChain {  
	http.authorizeRequests()  
		.antMatchers("/design", "/orders")  
		.access("hasRole('ROLE_USER')")  
		.antMatchers("/", "/**")  
		.access("permitAll")  
		.and()  
		.httpBasic()  
	return http.build()  
}  
  • antMatchers(): 지정된 경로의 패턴 일치를 검사한다.
    • 먼저 지정된 보안 규칙이 우선적으로 처리된다.
메서드하는 일
access(String)인자로 전달된 SpEL 표현식이 true면 접근을 허용한다.
anonymous()익명의 사용자에게 접근을 허용한다.
authenticated()익명이 아닌 사용자로 인증된 경우 접근을 허용한다.
denyAll()무조건 접근을 거부한다.
fullyAuthenticated()익명이 아니거나 또는 remember-me가 아닌 사용자로 인증되면 접근을 허용한다.
hasAnyAuthority(String…)지정된 권한 중 어떤 것이라도 사용자가 갖고 있으면 접근을 허용한다.
hasAnyRole(String…)지정된 역할 중 어느 하나라도 사용자가 갖고 있으면 접근을 허용한다.
hasAuthority(String)지정된 권한을 사용자가 갖고 있으면 접근을 허용한다.
hasIpAddress(String)지정된 IP 주소로부터 요청이 오면 접근을 허용한다.
hasRole(String)지정된 역할을 사용자가 갖고 있으면 접근을 허용한다.
not다른 접근 메서드들의 효력을 무효화한다.
permitAll()무조건 접근을 허용한다.
rememberMe()remember-me(이전 로그인 정보를 쿠키나 데이터베이스로 저장한 후 일정 기간 내에 다시 접근 시 저장된 정보로 자동 로그인됨)를 통해 인증된 사용자의 접근을 허용한다.

커스텀 로그인 페이지 생성하기

http.authorizeRequests()  
	.antMatchers("/design", "/orders")  
	.hasRole("ROLE_USER")  
	.antMatchers("/", "/**")  
	.permitAll()  
	.and()  
	.formLogin()  
	.loginPage("/login")  
	.loginProcessingUrl("/authenticate")  
	.usernameParameter("user")  
	.passwordParameter("pwd")
  • loginPage(): 커스텀 로그인 페이지 경로 지정
  • loginProcessingUrl(): 로그인 요청 url
  • usernameParameter(): 로그인 요청 시 유저 이름 파라미터 명
  • passwordParameter(): 로그인 요청 시 패스워드 파라미터 명
  • defaultsuccessUrl(): 사용자가 직접 로그인 페이지로 이동한 후 로그인을 성공했을 때 이동할 페이지
    • 기본적으로는, 사용자가 로그인 전에 어떤 페이지에 머물렀다면 그 페이지로 돌아간다.
    • 로그인 전에 어떤 페이지에 있었는 지와 무건하게 이동하려면 두 번째 인자로 true를 전달하면 된다.
      • defaultSuccessUrl("/design", true)

로그아웃하기

http.authorizeRequests()  
	.antMatchers("/design", "/orders")  
	.hasRole("USER")  
	.antMatchers("/", "/**")  
	.permitAll()  
	.and()  
	.formLogin()  
	.loginPage("/login")  
	.loginProcessingUrl("/authenticate")  
	.usernameParameter("user")  
	.passwordParameter("pwd")  
	.and()  
	.logout()  
	.logoutSuccessUrl("/")
  • logout(): 기본값으로 ‘POST /logout’ 요청을 가로채서 보안 필터를 설정한다.
  • logoutSucessUrl(): 로그아웃 이후 이동할 페이지 지정
    • 가본값으로는 로그인 페이지로 이동한다.

CSRF 공격 방어하기

http.authorizeRequests()  
	.antMatchers("/design", "/orders")  
	.hasRole("USER")  
	.antMatchers("/", "/**")  
	.permitAll()  
	.and()  
	// ...
	.csrf()

사용자 인지하기

  • 유저 정보 가져오는 방법
    • Principal 객체를 컨트롤러 메서드에 주입힌다.
    • Authentication 객체를 컨트롤러 메서드에 주입한다.
    • SecurityContextHolder를 사용해서 보안 컨텍스트를 얻는다.
    • @AuthenticationPrincipal 애노테이션을 메서드에 지정한다.