먼저 요청이 들어오면 필터가 요청을 가로채고 인증 책임이 AuthenticationManager
로 위임된다. 인증 논리 구현하는 AuthenticationProvider
를 이용하는데 이 때 UserDetailsService
에서 사용자를 찾고 PasswordEncoder
로 암호를 검증한다. 모든 인증이 끝나면 SecurityContext
에 UserDetails
가 저장된다.
사용자 관리는 UserDetailsManager
와 UserDetailsService
를 사용한다. UserDetailsService
는 사용자 이름으로 사용자를 찾는 역할만 하고 UserDetailsManager
는 사용자의 삭제, 추가, 수정 작업을 수행한다.
사용자는 애플리케이션 내에서 수행할 수 있는 작업을 나타내는 이용 권리의 집합을 가진다. 이를 권한이라고 한다.
사용자 정의하기
사용자를 나타내는 것은 인증 구현의 첫 번째 단계이다. 스프링 시큐리티에서 사용자 정의는 UserDetails
계약을 준수해야 한다.
UserDetails
의 메서드는 두 부분으로 나뉜다. 인증과정에서 사용되는 PgtaPssword()
, getUsername()
과 리소스에 접근할 수 있도록 권한을 부여하는 나머지 다섯 메서드로 나눌 수 있다.
권한 정의
사용자가 수행할 수 있는 작업을 권한이라고 한다. 애플리케이션 마다 사용자가 가진 권한이 다를 수 있다. 스프링 시큐리티에서는 GrantedAuthority
인터페이스로 권한을 나타낸다. 사용자는 여러 권한을 가질 수 있고 일반적으로 하나 이상의 권한을 가진다.
권한을 구현하기 위해서는 람다식을 사용하거나 SimpleGrantedAuthority
클래스를 이용해 인스턴스를 만드는 것도 가능하다. 구현 시에는 이름을 반환하게만 구현하면 된다.
사용자 책임 분리
애플리케이션에서 사용자는 여러 책임을 가질 수 있다. 데이터베이스에 저장되는 유저 엔티티 책임과 인증에 사용되는 UserDetails
책임을 가질 수 있다. 그러나 모든 책임을 한 클래스에 넣는 것은 바람직하지 않다.
두 가지 책임을 분리해서 User 클래스는 JPA 엔티티 책임만을 가지고 이를 래핑해 인증 관련 책임을 처리하는 클래스를 만들 수 있다.
@Entity
public class User {
@Id
private Long id;
private String username;
private String password;
private String authority;
// getters & setters
}
public class SecurityUser implements UserDetails {
private final User user;
public SecurityUser(User user) {
this.user = user;
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> user.getAuthority());
}
....
}
스프링 시큐리티 사용자 관리
이제 앞서 만든 사용자를 UserDetailsService
를 구현해 관리하는 방법을 지정해줘야 한다. 여기에 더 많은 기능을 사용하려면 UserDetailsManager
를 구현하면 된다.
UserDetailsService
는 loadUserByUsername()
한 개의 메서드만을 가진다.
인증 구현은 loadUserByUsername()
메서드를 호출해 주어진 이름과 일치하는 UserDetails
를 얻는다. 이 때 주어진 이름이 없으면 UsernameNotFoundException
이 발생한다.
UserDetailsService 구현
스프링 시큐리티에서 필요한 것은 어떤 방식으로든 사용자 이름으로 사용자를 조회하는 기능이다.
public class InMemoryUserDetailsService implements UserDetailsService {
private final List<UserDetails> users;
public InMemoryUserDetailsService(List<UserDetails> users) {
this.users = users;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return users.stream()
.filter(
u -> u.getUsername().equals(username)
).findFirst()
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
}
메모리 상으로 찾을 때는 위와 같이 구현할 수 있다.
UserDetailsManager 구현
스프링 시큐리티에서 제공하는 serDetailsManager
인터페이스는 UserDetailsService
인터페이스를 extends한다.
구현체로 InMemoryUserDetailsManager
와 JdbcUserDetailsManager
를 제공한다. JdbcUserDetailsManager
는 SQL 데이터베이스에 저장된 사용자를 관리하며 JDBC를 통해 데이터베이스에 직접 연결한다.
위에서 살펴봤던 그림과 조금 달라졌다. UserDetails
얻는 과정에서 JdbcUserDetailsManager
가 데이터베이스에서 사용자 이름으로 조회하는 부분이 추가되었다.
직접 Jdbc를 설정해줘야 하므로 의존성을 추가해줘야 한다.
dependencies {
runtimeOnly 'com.h2database:h2'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
...
}
사용할 데이터베이스에 따라 다른 드라이버를 설치해주면 되는데 h2가 더 간편하기 때문에 h2Driver를 추가해줬다.
그리고 이제는 application.yml에 dataSource를 설정해줘야 한다.
spring:
datasource:
driver-class-name: org.h2.Driver
url: '<사용할 url>'
username: 'sa'
password:
sql:
init:
mode: always
여기서 spring.datasource.initialization-mode
를 always
로 두려고 했는데 deprecated되었다고 한다. 대신 spring.sql.init.mode
로 바뀌었다.
또한 프로젝트에 사용될 UserDetailsService
를 빈으로 등록해줘야 한다.
@Configuration
public class ProjectConfig {
@Bean
public UserDetailsService userDetailsService(DataSource dataSource) {
return new JdbcUserDetailsManager(dataSource);
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpsPasswordEncoder.getInstance();
}
}
또는 JdbcUserDetailsService
에 이용되는 쿼리도 구성할 수 있다.
@Bean
public UserDetailsService userDetailsService() {
String usersByUsernameQuery =
"select username, password, enabled from users where username = ?";
String authByUserQuery =
"select username, authority from authority where username = ?";
var userDetailsManager = new JdbcUserDetailsManager(dataSource);
userDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery);
userDetailsManager.setAuthoritiesByUsernameQuery(authByUserQuery);
return userDetailsManager;
}
'Java > Spring' 카테고리의 다른 글
스프링 시큐리티 인 액션] 5장_인증 (0) | 2024.06.28 |
---|---|
스프링 시큐리티 인 액션] 4장_암호처리 (0) | 2024.06.21 |
MVC 패턴이란? (0) | 2023.11.05 |