(spring security) UserDetailsService및UserDetailsManager이해

jint·2024년 10월 14일

보안

목록 보기
2/15

동작원리

인증필더가 먼저 요청을 가로챈다.

인증 필터는 authenticationManager에 인증 책임을 위임하고 authenticationManager는 authenticationProvider를 이용해 인증을 처리한다.

authenticationProvider는 UserDetailsService를 이용해 사용자를 찾고 passwordEncoder를 이용해 암호를 검증한다.

사용자에 대한 세부 정보는 UserDetailsService 빈이 관리한다.

재정의 하지않으면 스프링 부트의 기본구현(user, uuid password)을 통해 기본 자격증명을 등록한다.

passwordEncoder 빈은 암호를 인코딩하고 기존 인코딩과 일치하는지 확인하는 작업을 한다.

AuthenticationProvider의 스프링 부트 기본 구현은 UserDetailsService와 PasswordEncoder에 제공된 기본 구현을 이용하여 인증 논리를 정의하고 사용자의 암호 관리를 위임한다.

먼저 UserDetailsService와 authenticationProvider를 재정의하여 동작원리를 살펴보고 자세한 인터페이스를 보며 계약이 어떻게 짜여있는지 확인한다.

UserDetailsService재정의하기

직접 구현체를 만들 수도 있지만 스프링 시큐리티에 있는 구현을 이용할 수도 있다.

InMemoryUserDetailsManager 객체를 이용하기

InMemoryUserDetailsManager는 말 그대로 실행시에 메모리에 유저의 자격증명을 올려두고 인증할 때 이용하는 구현체이다.

운영 단계에서는 보안,스케일링,기능 문제로 인해 사용하지 않는다.

@Configuration
public class ProjectConfig {
	@Bean
	public UserDetailsService userDetailsService() {
		var userDetailsService =  new InmemoryUserDetailsManager();
		var user = User.withUsername("john")
			.password("1234")
			.authorities("read") //읽기 권한 부여
			.build();
		userDetailsService.createUser(user); //유저 등록
		return userDetailsService;
	}
}
	 

UserDetailsService를 재정의하면 passwordEncoder도 자동 구성되지 않기 때문에 다시 선언해야한다.

@Bean
public PasswordEncoder passwordEncoder() {
	return NoOpPasswordEncoder.getInstance();
}

NoOpPasswordEncoder는 암호화를 적용하지 않고 일반 텍스트 처럼 처리한다.

String class의 기본 equals메서드로 문자열비교를 하여 암호를 비교한다.

authenticationProvider재정의하기

authenticationProvider는 인증논리를 나타낸다.

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
	@Override
	public Authentication authenicate(Authentication authentication) {
		String username = authentication.getName();
		String password = String.valueOf(authentication.getCredentials());
		
		//UserDetailsService 및 PasswordEncoder가 하는 일
		if("john".equals(username) && "1234".equals(password)) {
			return new UsernamePasswordAuthenticationToken(username,password,Arrays.asList();
		} else {
			throw new AuthenticationCredentialsNotFoundException("error");
		}
	}
	
	//supports구현생략
}

WebSecurityConfigurerAdapter를 구현하여 위의 CustomAuthenticationProvider를 등록하기

@Configuration
public class ProjectConfig implements WebSecurityConfigurerAdapter {
	private CustomAuthenticationProvider authenticationProvider;
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) {
		auth.authenticationProvider(authenticationProvder);
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.httpBasic();
		//모든 요청에 대해 적용
		http.authorizeRequests().anyReqeust().authenticated();
	}
}

스프링 시큐리티에서의 UserDetailsService 및 UserDetailsManager의 역활

UserDetailsManager는 UserDetailsService인터페이스를 확장하여 사용자 추가,수정,삭제 작업을 한다.

UserDetailsService인터페이스는 UserDetails인터페이스를 이용하여 사용자를 인증하는 기능을 추가한다.

UserDetails인터페이스는 하나이상의 GrantedAuthority를 가져 사용자를 기술한다.

따라서 개발자가 필요한 범위에 동작만 구현할 수 있게 인터페이스 분리의 원칙에 따라 설계되었다.

UserDetails 인터페이스

public interface UserDetails extends Serializable {
	String getUsername();
	String getPassword();
	
	Collection<? extends GrantedAuthority> getAuthorities();
	boolean isAccountNonExpired();
	boolean isAccountNonLocked();
	boolean isCrefentialsNonExpired();
	boolean isEnabled();
}

사용자의 name과 password,사용자에게 부여된 권한들에 대한 컬랙션에 대한 getter메서드와

사용자의 계정에 대한 제한을 구현할 수 있는 메서드가 있다.

isCrefentialsNonExpired은 이중부정이지만 다른 메서드처럼 실패시 false를 성공시 true를 반환시키기 위해 이름이 이렇게 정해졌다고 한다.

GrantedAuthority 인터페이스

public interface GrantedAuthority extend Serializable {
	String getAuthority();
}

권한 부여 규칙을 해당 권한의 이름을 바탕으로 적용하기 때문에 해당 권한의 이름만 String으로 리턴하면 된다.

UserDetails구현해보기

이름은 nick이고 암호는 1234, 권한은 READ권한을 가지는 DummyUser클래스를 만든다

public class DummyUser implements UserDetails {
	@Override
	public String getUsername() {
		return "nick";
	}
	@Override 
	public String getPassword() {
		return "1234";
	}
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		//인터페이스가 단일 추상 메서드만 가지기 떄문에 람다식을 통한 구현 
		return List.of(()->"READ");
	}
	
	//나머지 제약 메서드들은 항상 true를 반환한다(생략)
}

빌더를 이용해 구현할 수도 있다

UserDetails u = User.withUsername("nick")
									.password("1234")
									.authorities("read")
									.build();

제약메서드들의 기본값은 true이다.

실제 운영에서는 데이터베이스에서 유저 데이터를 가져오기때문에 지속성 엔터티를 나타내는 클래스가 필요한데

책임을 분리하기위해 user엔터티를 래핑하여 작성한다.

public class SecurityUser implements UserDetails {

	private final User user;
	
	public SecurityUser(User user) {
		this.user = user;
	}
	
	//나머지 메서드 생략
}

UserDetailsService 인터페이스

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

위에서 언급한것 처럼 UserDetailsService은 유저이름을 바탕으로 유저를 찾아 반환하는 역활을 한다.

authenticationProvider는 UserDetailsService의 loadUserByUsername메서드를 사용하여 유저를 찾고 passwordEncoder를 통해 일치를 확인한다.

유저정보를 메모리에 저장하는 InMemoryDetailsService구현하기

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));
	}
}
		

빈으로 등록하기

@Configuration
public class ProjectConfig {
	@Bean
	public UserDetailsService userDetailsService() {
		//User는 UserDetails의 구현체
		UserDetails u = new User("nick", "1234", "READ");
		List<UserDetails> users = List.of(u);
		return new InMemoryUserDetailsService(users);
	}
	
	//UserDetailsService를 직접 등록하면 passwordEncoder도 등록해줘야함(생략)
}

UserDetailsManager 인터페이스

UserDetailsManager는 위에서 언급한대로 UserDetailsService의 계약을 확장하여 유저 추가,수정,삭제 메서드를 추가한다.

public interface UserDetailsManager implements UserDetailsService {
	void createUser(UserDetails user);
	void updateUser(UserDetails user);
	void deleteUser(UserDetails user);
	void changePassword(String oldPassword, String newPassword);
	boolean userExists(String username);
}

0개의 댓글