
인증필더가 먼저 요청을 가로챈다.
인증 필터는 authenticationManager에 인증 책임을 위임하고 authenticationManager는 authenticationProvider를 이용해 인증을 처리한다.
authenticationProvider는 UserDetailsService를 이용해 사용자를 찾고 passwordEncoder를 이용해 암호를 검증한다.
사용자에 대한 세부 정보는 UserDetailsService 빈이 관리한다.
재정의 하지않으면 스프링 부트의 기본구현(user, uuid password)을 통해 기본 자격증명을 등록한다.
passwordEncoder 빈은 암호를 인코딩하고 기존 인코딩과 일치하는지 확인하는 작업을 한다.
AuthenticationProvider의 스프링 부트 기본 구현은 UserDetailsService와 PasswordEncoder에 제공된 기본 구현을 이용하여 인증 논리를 정의하고 사용자의 암호 관리를 위임한다.
먼저 UserDetailsService와 authenticationProvider를 재정의하여 동작원리를 살펴보고 자세한 인터페이스를 보며 계약이 어떻게 짜여있는지 확인한다.
직접 구현체를 만들 수도 있지만 스프링 시큐리티에 있는 구현을 이용할 수도 있다.
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는 인증논리를 나타낸다.
@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구현생략
}
@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();
}
}

UserDetailsManager는 UserDetailsService인터페이스를 확장하여 사용자 추가,수정,삭제 작업을 한다.
UserDetailsService인터페이스는 UserDetails인터페이스를 이용하여 사용자를 인증하는 기능을 추가한다.
UserDetails인터페이스는 하나이상의 GrantedAuthority를 가져 사용자를 기술한다.
따라서 개발자가 필요한 범위에 동작만 구현할 수 있게 인터페이스 분리의 원칙에 따라 설계되었다.
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를 반환시키기 위해 이름이 이렇게 정해졌다고 한다.
public interface GrantedAuthority extend Serializable {
String getAuthority();
}
권한 부여 규칙을 해당 권한의 이름을 바탕으로 적용하기 때문에 해당 권한의 이름만 String으로 리턴하면 된다.
이름은 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;
}
//나머지 메서드 생략
}
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
위에서 언급한것 처럼 UserDetailsService은 유저이름을 바탕으로 유저를 찾아 반환하는 역활을 한다.

authenticationProvider는 UserDetailsService의 loadUserByUsername메서드를 사용하여 유저를 찾고 passwordEncoder를 통해 일치를 확인한다.
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는 위에서 언급한대로 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);
}