33. Spring Security
스프링 부트 시큐리티는 스프링 부트에 적용되는 인증 라이브러리로
여러가지 종류가 있다.
· 웹 시큐리티
· 메소드 시큐리티
· 다양한 인증 방법 지원
· LDAP, 폼 인증, Basic 인증, Oauth 등…
적용방식
스프링 부트 시큐리티는 아주 간편하게 사용 할 수 있도록 제공도 해주는데
의존성 라이브러리에 등록만 해줘도 자동설정으로 웹 전체에 적용이 된다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
스프링 부트 시큐리티 의존성을 정의하면 자동설정이 적용되어 모든 페이지에 접근시 인증(로그인)이 필요하다.
인증하지 않고 요청시 로그인 /login 페이지로 리다이렉트 한다.
이떄 로그인 페이지의 로그인 정보는 기본은 user / <콘솔창에 출력된 비밀번호> 이다.
3xx 의 3으로 시작하는 모든 요청상태는 리다이렉션의 종류이다.
request의 accept 타입에 따라 응답되는 헤더가 다르게 나오는데
테스트로 요청시 401 에러가 나오며 accecpt 타입을 HTML 로 한다면 304에러가 나며 리다이렉션 시켜준다.
SecurityAutoConfiguration
스프링 시큐리티가 의존성 등록 되어있을때 등록이 실행되는 설정파일로 DefaultAuthenticationEventPublisher 가 bean으로 생성되고 있다. 또한 이러한 설정 클래스는 스프링 시큐리티가 가지고 있기 때문에 굳이 부트를 사용해 의존성을 받지 않아도 DefaultAuthenticationEventPublisher 를 bean으로 등록하면 사용 할 수 있다.
DefaultAuthenticationEventPublisher 이벤트 퍼블러셔는 로그인실패, 만료, 락 등등 유저의 이벤트를 발생시키고 우리는 그러한 이벤트로 핸들러를 작성하여 변경 하는등에 사용 할 수있다.
UserDetailServiceAutoConfigration
초기에 인-메모리 유저 매니저를 만들어서 유저를 생성한다.
스프링 부트가 지원하는 스프링 시큐리티 관련된 기능들은 별로 쓸일이 없다고함
Spring Boot Security Test
스프링 부트 시큐리티를 테스트 하려면 의존성을 별도로 정의 해주어야함
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>${spring-security.version}</version>
<scope>test</scope>
</dependency>
테스트 메소드에 @WithMockUser를 정의하면 mock 유저를 넣어 테스트함
@Test
@WithMockUser
void hello() throws Exception {
MvcResult mvcResult = mockMvc.perform(get("/hello")
.accept(MediaType.TEXT_HTML))
.andExpect(status().isOk())
.andDo(print())
.andExpect(view().name("hello"))
.andReturn();
}
get() or post()에 with에 user의 정보를 넣을 수 도 있다.
@Test
void hello() throws Exception {
MvcResult mvcResult = mockMvc.perform(get("/hello").with(user("user").password("123"))
.accept(MediaType.TEXT_HTML))
.andExpect(status().isOk())
.andDo(print())
.andExpect(view().name("hello"))
.andReturn();
}
스프링 부트 시큐리티 설정
스프링 부트 시큐리티의 설정을 커스텀마이징을 하기 위해서는 @Configuration 어노테이션을 정의한 클래스로 설정을 셋팅한다.
그 후 WebSecurityConfigrerAdapter 를 상속 받으면, 스프링 부트 시큐리티가 기본적으로 셋팅한 값을 덮어씌운다.
스프링 부트 시큐리티가 기본으로 설정하는 인증 설정
// @formatter:off
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();
}
스프링 부트 시큐리티에서 권장하고 있는 암호화 인코딩을 bean으로 생성한다. (기본이 bcrypt)
만약 별도의 암호화 인코딩을 정의하지 않는다면 스프링 부트 시큐리티는 로그인 할때 비밀번호의 값을
NoOpPasswordEncoder 로 인식하여 복호화를 진행한다. 그런데 이때 해당 암호화 인코더 bean을 찾을 수 없어 에러가 난다.
최소한 NoOpPasswordEncoder 는 bean으로 올려줘야한다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/hello").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
@Bean
public PasswordEncoder passwordEncoder(){
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
andMatchers(String ….) 로 매핑들을 작성하고 permitAll 로 접근 허용
anyRequest().authenticated() 로 그 이외로 요청할때는 인증을 필수로 받는다.
html을 accept 타입으로 온다면 formLogin() 에서 반응하여 /login 페이지로 리다이렉션 시켜주고
그 이외는 httpBasic() 으로 핸들링 시킨다.
UserDetailsService 설정
스프링 부트 시큐리티는 기본적으로 유저 계정을 하나 가지고 간다.
이러한 계정을 생성하기 위해서는 bean으로 올라가는 클래스에 UserDetailsService 인터페이스를 상 받아 초기 계정을 가져와야한다.
상속 받은 인터페이스로 오버라이딩을 하여 loadUserByUsername 메소드를 생성 후 정의
이 메소드는 /login 에서 입력한 ID를 매개변수로 받는다.
Optional<> 에서 *.orElseThrow( () -> new UsernameNotFoundException(**) ) 으로 해당 유저가 없다면 예외처리
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.Collection;
import java.util.Optional;
@Service
public class AccountService implements UserDetailsService {
@Autowired
private AccountRepository repository;
@Autowired
PasswordEncoder passwordEncoder;
public Account createAccount(String username, String password){
Account account = new Account();
account.setUsername(username);
account.setPassword(passwordEncoder.encode(password));
return repository.save(account);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Account> byAccount = repository.findByUsername(username);
Account account = byAccount.orElseThrow(() -> new UsernameNotFoundException(username));
return new User(account.getUsername(), account.getPassword(), authorities());
}
private Collection<? extends GrantedAuthority> authorities() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
}
}
Runner 를 사용하여 어플리케이션 실행시 유저 생성
import me.tony.demospringboot.account.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class AccountRunner implements ApplicationRunner {
@Autowired
AccountService service;
@Override
public void run(ApplicationArguments args) throws Exception {
service.createAccount("tony", "123");
}
}