스프링 시큐리티 활성화하기
- Spring 자동 구성을 이용하여 활성화한다.
- https://www.baeldung.com/spring-boot-security-autoconfiguration
- 아래 의존에는
SecurityAutoConfiguration
클래스가 포함되어 있어서, 자동으로 시큐리티 구성이 된다.
<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
}
JdbcDaoImpl
:JdbcUserDetailManager
의 슈퍼 클래스로 JDBC 쿼리로 유저의 상세 정보를 검색할 수 잇도록 구현되어 있다.- https://docs.spring.io/spring-security/site/docs/6.1.0/api/org/springframework/security/core/userdetails/jdbc/JdbcDaoImpl.html
- 기본 스키마로 “users”, “authorities” 테이블을 사용한다.
- users의 컬럼: username, password, enabled
- authorities의 컬럼: username, authority
enableGroups
프로퍼티를true
로 설정하면 그룹 기반 인증도 지원한다.- 이 때는 groups, group_members, group_authorities 테이블 사용한다.
- 사용하게되는 기본 스키마는 아래 링크를 참고한다.
다음과 같이 유저를 인증하고 권한 확인하는 쿼리를 수정할 수 있다.
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
. 다음의 기능을 제공해준다.- https://docs.spring.io/spring-security/reference/features/authentication/password-storage.html#authentication-password-storage
- 현재 비밀번호가 권장된 방법대로 인코딩되어있는지 확인
- 최신 및 레거시 형식의 비밀번호 유효성 검사 허용
- 향후 인코딩 업그레이드 허용
{id}encodedPassword
형식을 가진다. id는 어떤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()
: 로그인 요청 urlusernameParameter()
: 로그인 요청 시 유저 이름 파라미터 명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
애노테이션을 메서드에 지정한다.