Spring Security - Spring Security 겉핥기
프로젝트를 다루면서 Spring Security를 알아보자.
Spring Security 동작 확인
- Spring Tool Suite 4
- SpringBoot 3
- Java 17
이번엔 간단히 REST endPoint에 접근하는 것으로 Spring Security를 알아볼 것이다.
xml에 필요한 종속성은 아래와 같다.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
이후 endPoint를 위한 Controller를 구현해 보자.
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello!";
}
}
그리고 프로젝트를 실행시키면 아래와 같이 password가 console에 나온다.
이제 http://localhost:8080/hello에 접근하면 아래의 모습을 하는 http://localhost:8080/login으로 이동한다.
여기에서
- Username: user
- Password: { console에 나온 password }
를 입력하면 올바르게 /hello 페이지에 접근할 수 있다.
참고로 http://localhost:8080/로 접근하면 해당 url을 다루는 controller가 없기에 404 에러가 발생한다.
실제로 Application을 구현할 때엔 위의 기능을 전혀 사용하지 않겠지만 Spring Security가 올바르게 동작하고 있음을 알 수 있다.
Spring Security Architecture
Spring Security가 동작하는 과정은 아래와 같다.
- User가 인증을 요청한다.
- Authentication Filter가 인증 요청을 받은 후 Authentication Manager에게 넘긴다.
- Authentication Manager가 Authentication Provider를 사용해 인증을 처리한다.
- Authentication Provider는 User Deatils Service와 Password Encoder를 활용해 사용자를 인증 및 관리한다.
- 결과를 Authentication Manager에게 넘긴다.
- 결과를 Authentication Filter에게 넘긴다.
- Security Context는 인증 데이터를 유지한다.
모든 요소를 차근차근 알아갈 것이다.
이번엔 UserDetailsService와 PasswordEncoder를 겉핥기식으로만 알아보겠다.
(이 둘에 대해서도 자세히 알아볼 것이다.)
UserDetailsService & PasswordEncoder
UserDetailsService는 사용자의 정보를 가져오는 인터페이스이다.
위 프로젝트는 SpringBoot에서 제공하는 기본 구현을 사용한 것이며 Application 내부 메모리에 기본 자격 증명만 등록된다.(ID: User, Password; UUID)
비밀번호는 Spring Context가 로드될 때 무작위로 생성되며 Console에 출력된다.
PasswordEncoder는 두 개의 작업을 수행한다.
- 비밀번호 인코딩(encryption, hashing algorithm 등 활용)
- 비밀번호가 기록된 인코딩과 일치하는지 확인
PasswordEncoder도 기본 인증 흐름에서 필수이며 UserDetailsService와 같이 기본 구현을 제공한다.
기본 구성 Overriding
기본 구현된 UserDetailsService와 PasswordEncoder는 실제로는 활용성이 없으므로 우리는 이를 Overriding 해야 한다.
앞서 진행한 프로젝트에 config package를 만든 후 아래의 다음 클래스를 추가한다.
@Configuration
public class ProjectConfig {
@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager();
}
}
현재는 UserDetailsService와 PasswordEncoder에 대해 알아보는 것이므로 InMemoryUserDetailsManager()를 사용한다. 이에 대해선 추후 알아보겠다.
UserDetailService를 위와 같이 구현하면 더 이상 Console에 Password가 나타나지 않는다.
이는 기본 구현된 것이 아닌 새롭게 추가한 것을 사용한다는 의미이다.
다음으로 Application을 사용할 User가 필요하니 이에 대해 추가해 보자.
@Configuration
public class ProjectConfig {
@Bean
UserDetailsService userDetailsService() {
UserDetails user = User.withUsername("hello")
.password("1234")
.authorities("read")
.build();
return new InMemoryUserDetailsManager(user);
}
}
이름은 "hello", 비밀번호는 "1234", 권한은 "read"를 가진 사용자다.
여기서의 UserDetails는 사용자의 정보를 담는 인터페이스로 builder 패턴을 사용할 수 있다.
사용자도 생겼으니 이제 sign in을 해보자.
어째서인지 sign in은 되지 않고 아래의 오류가 발생했다.
PasswordEncoder를 구현하지 않아서 생긴 문제이다.
UserDetailsService를 직접 구현한다면 반드시 PasswordEncoder도 만들어줘야 한다.
아래 코드를 ProjectConfig 클래스에 추가해 보자.
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
이후 다시 sign in을 하면 올바르게 "Hello!'가 출력된다.
여기서 사용한 NoOpPasswordEncoder는 암호를 인코딩하지 않고 일반 텍스트 처리를 하겠다는 것으로, 후에 이들에 대해서도 알아볼 것이다.
SecurityFilterChain
지금까지 만든 프로젝트는 페이지에 접근하려면 무조건 인증을 해야 했다.
하지만 실제 서비스 Application은 모두에게 열린 endPoint와 그렇지 않은 endPoint로 구분된다.
이를 위해 우리는 SecurityFilterChain Bean을 정의해야 한다.
아래 코드가 기본 형태이다. ProjectConfig클래스의 configure method를 아래와 같이 추가하면 된다.
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
return http.build();
}
아래와 같이 구현하면 기존 인증과 동일한 것이며
@Bean
SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.httpBasic(Customizer.withDefaults());
http.authorizeHttpRequests(c -> c.anyRequest().authenticated());
return http.build();
}
아래와 같이 구현하면 모든 endPoint에 인증 없이 접근할 수 있다.
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.httpBasic(Customizer.withDefaults());
http.authorizeHttpRequests(c -> c.anyRequest().permitAll());
return http.build();
}
httpBasic()은 인증 접근 방식을 구성하는 것이며 Customizer.withDefaults()는 아무 작업도 수행하지 않는다.
아래는 Customizer.withDefaults()의 코드이다.
@FunctionalInterface
public interface Customizer<T> {
void customize(T t);
static <T> Customizer<T> withDefaults() {
return (t) -> {
};
}
}
authorizeHttpRequests는 endPoint level에서 권한 부여 규칙을 구성하는 것으로
anyRequest().authenticated()는 인증을 요구하는 것이며
anyRequest().permitAll()은 인증이 필요 없다는 것이다.
여기선 endPoint마다의 인증 설정을 이런 방식으로 한다 정도로만 알면 된다.
아래와 같이 UserDetailsService를 SecurityFilterChain method 안에 담는 것도 가능하다.
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.httpBasic(Customizer.withDefaults());
http.authorizeHttpRequests(c -> c.anyRequest().authenticated());
var user = User.withUsername("john").password("12345").authorities("read").build();
var userDetailsService = new InMemoryUserDetailsManager(user);
http.userDetailsService(userDetailsService);
return http.build();
}
이번엔 Architecture와 간단한 사용법을 알아보았다. 다음부터 하나씩 파헤쳐보자.