2장 안녕! 스프링 시큐리티
이 블로그 내용은 아래 도서를 학습한 내용입니다.
학습 도서 정보
스프링 시큐리티 인 액션(Spring Security in Action)
로렌티우 스필카 지음
최민석 옮김
위키북스
1. 스프링 시큐리티와 웹 종속성만 있는 프로젝트를 만들어서 추가된 구성이 없으면 어떻게 작동하는지 확인한다. 이를 통해 인증과 권한 부여의 기본 구성에서 무엇을 기대해야할 지 이해할 수 있다.
2. 기본 구성을 재정의하고 맞춤형 사용자와 암호를 정의해서 프로젝트에 사용자 관리 기능을 추가한다.
3. 애플리케이션이 기본적으로 모든 엔드포인트를 인증하는 것을 확인하고 이 동작도 맞춤 구성할 수 있다는 것을 배운다.
4. 모범 사례를 이해할 수 있도록 동일한 구성에 대해 다른 스타일을 적용해 본다.
2.1 첫 번째 프로젝트 시작
아래와 같은 스프링 시큐리티 종속성을 추가하면, 스프링 시큐리티가 작동한다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
스프링 시큐리티는 기본적으로 모든 엔드포인트를 보호한다.
올바른 자격증명을 제공하지 않고 HTTP를 호출하면 401 권한없음이 반환된다.
자격증명은 아래와 같이 전송한다.
방법1
curl -u user:49ceebbc-f593-4dcb-ac5d-5b1b5a15939b http://localhost:8080/hello
방법2
echo -n user:49ceebbc-f593-4dcb-ac5d-5b1b5a15939b | base64
curl -H "Authorization: dXNlcjo0OWNlZWJiYy1mNTkzLTRkY2ItYWM1ZC01YjFiNWExNTkzOWI=" http://localhost:8080/hello
2.2 기본 구성이란?

AuthenticationProvider는 인증공급자이다.
UserDetailsService는 사용자 세부 정보 서비스이다.
PasswordEncoder는 암호 인코더이다.
- 인증 필터는 인증 요청을 인증 관리자에 위임하고 응답을 바탕으로 보안 컨텍스트를 구성한다.
- 인증 관리자는 인증 공급자를 이용해 인증을 처리한다.
- 인증 공급자는 인증 논리를 구현한다.
- 인증 공급자는 사용자 관리 책임을 구현하는 사용자 세부 정보 서비스를 인증 논리에 이용한다.
- 보안 컨텍스트는 인증 프로세스 후 인증 데이터를 유지한다.
사용자 세부 정보 서비스는 UserDetailsService 계약을 구현하는 객체가 관리한다.
PasswordEncoder는 다음 두 가지 일을 한다.
- 암호를 인코딩 한다.
- 암호가 기존 인코딩과 일치하는지 확인한다.
2.3 기본 구성 재정의
2.3.1 UserDetailsService 구성 요소 재 정의
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public UserDetailsService userDetailsService() {
var userDetailsService = new InMemoryUserDetailsManager();
var user = User.withUsername("john")
.password("12345")
.authorities("read")
.build();
userDetailsService.createUser(user);
return userDetailsService;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
curl -u john:12345 http://localhost:8080/hello
2.3.2 엔드포인트 권한 부여 구성 재정의
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic();
http.authorizeRequests().anyRequest().authenticated(); // 모든 요청에 인증이 필요하다.
http.authorizeRequests().anyRequest().permitAll(); // 모든 요청에 인증이 필요하다.
}
...
2.3.3 다른 방법으로 구성 설정
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
var userDetailsService = new InMemoryUserDetailsManager();
var user = User.withUsername("john")
.password("12345")
.authorities("read")
.build();
userDetailsService.createUser(user);
auth.userDetailsService(userDetailsService)
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
위와 같이 configure에서 UserDetailsService 및 PasswordEncoder를 설정하면 어디서 UserDetailsService 및 PasswordEncoder가 연결되어있는지 알기 쉽지 않다.
2.3.1에서 구성한 것과 같이 책임을 분리해서 작성해야 한다. (1책임당 1클래스로 구성하는게 좋다)
2.3.4 AuthenticationProvider 구현 재정의
AuthenticationProvider는 인증 논리를 구현하고 사용자 관리와 암호 관리를 각각 UserDetailsService 및 PasswordEncoder에 위임한다.
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// Principal 인터페이스의 getName() 메서드를 Authentication에서 상속받는다.
String username = authentication.getName();
String password = String.valueOf(authentication.getCredentials());
// 이 조건은 일반적으로 UserDetialsService 및 PasswordEncoder를 호출해서 사용자 이름과 암호를 테스트한다.
if ("john".equals(username) && "12345".equals(password)) {
return new UsernamePasswordAuthenticationToken(username, password, Arrays.asList());
} else {
throw new AuthenticationCredentialsNotFoundException("Error!");
}
}
@Override
public boolean supports(Class<?> authenticationType) {
// Authentication 형식의 구현을 추가할 위치
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authenticationType);
}
}
authentication(Authentication authentication) 메서드는 인증의 전체 논리를 나타낸다.
새로운 구성
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomAuthenticationProvider authenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic();
http.authorizeRequests()
.anyRequest().authenticated();
}
}
요약
- 스프링 시큐리티를 애플리케이션의 종속성으로 추가하면 스프링 부트가 약간의 기본 구성을 제공한다.
- 인증과 권한 부여를 위한 기본 구성요소인 UserDetailsService, PasswordEncoder, AuthenticationProvider를 구현했다.
- User 클래스로 사용자를 정의할 수 있다. 사용자는 사용자 이름, 암호, 권한을 가져야 한다. 권한은 사용자가 애플리케이션의 컨텍스트에서 수행할 수 있는 작업을 지정한다.
- 스프링 시큐리티는 userDetailsService의 간단한 구현인 InMemoryuserDetailsManager를 제공한다. UserDetailsService의 인스턴스와 같은 사용자를 추가해서 애플리케이션의 메모리에 사용자를 관리할 수 있다.
- NoOpPasswordEncoder는 PasswordEncoder 계약을 구현하며 암호를 일반 텍스트로 처리한다. 이 구현은 학습 예제와 개념 증명에 적합하지만 운영 단계 애플리케이션에는 적합하지 않다.
- AuthenticationProvider 계약을 이용해 애플리케이션의 맞춤형 인증 논리를 구현할 수 있다.
- 구성을 작성하는 방법은 여러 가지가 있지만, 한 애플리케이션에서는 한 방법을 선택하고 고수해야 코드를 깔끔하고 이해하기 쉽게 만들 수 있다.