Spring Security - UsernamPasswordAuthenticationFilter

Sungjin·2021년 7월 15일
1

Spring

목록 보기
2/23
post-thumbnail

UsernamePasswordAuthenticationFilter

  • Id와 paswword를 사용하는 form 기반 인증
    • Spring Boot에서는 Post 기반 /login 으로 요청오면 이 filter가 실행 되도록 defult 처리 되어있음
  1. AuthenticationManager 를 통해 인증 시도
  2. 인증 성공시, 실패시 처리할 수 있는 handler실행 가능
  • 동작 방식 : 간단히
  1. Client의 요청이 들어옴
  2. Client의 요청은 먼저 DispatcherServlet에 보내 지지 않고 내부에 등록된 filter들로부터 filter들을 순회.
  3. 등록된 filter들을 순회하던 중 UsernamePasswordAuthenticationFilter에 도달하게 되면 AbstractAuthenticationProcessingFilter를 통해 먼저 /login url인지 확인하고 확인 되면
  4. UsernamePasswordAuthenticationFilter 처리.

테스트 방법

  1. RestAPI를 통해 실험해볼 것
  2. 요청하는 Client의 IP를 직접 설정해 그 IP에 대해서 Filter가 잘 처리 되는지 실험 해볼 것

CODE

SpringBootApplication

@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(UserServiceApplication.class, args);
	}

	@Bean
	public BCryptPasswordEncoder bCryptPasswordEncoder(){
		return new BCryptPasswordEncoder();
	}

}

먼저 password를 encode 시켜주기 위한 encoder를 bean으로 등록해줘야 합니다. Bean으로 등록되어야 password encode를 시키려는 클래스에서 주입이 가능하기 때문입니다.

BCryptPasswordEncoder

  • password를 해싱하는 알고리즘
  • 랜덤 salt를 부여하기 위해 여러번 Hash를 적용한 암호화 방식

UserServiceImpl

@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService{

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder passwordEncoder;
    private final Environment env;


    @Override
    public UserDto createUser(UserDto userDto) {
        userDto.setUserId(UUID.randomUUID().toString());

        ModelMapper mapper=new ModelMapper();
        mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
        UserEntity userEntity = mapper.map(userDto, UserEntity.class);
        userEntity.setEncryptedPwd(passwordEncoder.encode(userDto.getPassword()));

        userRepository.save(userEntity);

        UserDto map = mapper.map(userEntity, userDto.getClass());
        return map;
    }
    
        @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        UserEntity entity = userRepository.findByEmail(userName);

        if(entity==null){
            throw new UsernameNotFoundException(userName);
        }

     
        return new User(entity.getEmail(), entity.getEncryptedPwd(),
                true,true,true,true,
                new ArrayList<>());

      
    }
}
  • createUser
    Client의 회원가입 요청 시 User를 DB에 저장하여 주기 위함. DB에 저장할 때 전달받은 password를 encode하여 저장 시켜 줌.
  • loadByUsername
    provider에서 호출. 로그인 시 입력한 user Id를 통해서 DB에 접근하여 유저 정보를 UserDetails형태로 객체를 호출함. 로드된 UserDetails 객체를 통하여 로그인 시 입력한 패스워드와 비교.

RequestLogin

@Data
public class RequestLogin {
    @NotNull(message = "Email Cannot Be Null!")
    private String email;

    @NotNull(message = "Password Cannot Be Null!")
    private String password;
}

요청받은 login 입력 정보를 매핑시킬 객체.

AuthentiationFilter

@Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private UserService userService;
    private Environment env;

    public AuthenticationFilter(AuthenticationManager authenticationManager, UserService userService, Environment env) {
        super.setAuthenticationManager(authenticationManager);
        this.userService = userService;
        this.env = env;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        //요청 정보를 보냈을 때 처리 시켜 줄 수 있는 메소드
        try {
            RequestLogin creds = new ObjectMapper().readValue(request.getInputStream(), RequestLogin.class);
            
           return getAuthenticationManager().authenticate(
                   new UsernamePasswordAuthenticationToken(creds.getEmail(),creds.getPassword(),new ArrayList<>())
           );
         
        }catch (IOException e){
            throw new RuntimeException(e);
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        //실제 Login성공했을 때 토큰을 만드는 등 토큰의 만료시간등등을 정할것
        
    }


}

login시 사용 할 UserNamePasswordAuthenticationFilter 구현.
UserNamePasswordAuthenticationFilter를 상속 받아 클래스를 만듭니다.

RestAPI형태로 개발되었기 때문에 ObjectMapper를 통해 getInputStream()을 읽은 다음 로그인 정보를 매핑하기 위해 만들어놓은 객체에다가 매핑.

전달 받은 값을 사용하기 위해 입력받은 정보를 Spring Security Web Authentication 패키지 안에 있는 UsernamePasswordAuthenticationToken으로 변경이 필요합니다.

변경된 토큰을 인증 처리하기 위하여 AuthenticationManager에게 토큰 값을 넘겨 주게 되고 Manager로부터 인증을 처리하게 되는 것입니다.

AuthenticationManager?
전달받은 데이터를 실질적으로 인증하기위한 Interface입니다.
이 Interface의 구현체는 ProviderManager로 Spring에서 인증과 관련된 작업을 하는 class로 보시면 될 것 같습니다.
또, ProviderManager는 멤버 변수로 가지고 있는 등록된 Authenticationprovider들에게 '처리가 가능한지?'물어보고 처리가 가능한 Provider에게 전달받은 데이터의 인증을 위임하게 됩니다.
AuthenticationProvider 에서는 이제 UserServiceImpl에서 구현된 loadByUsesrname 메소드를 호출하여 Login을 위한 인증 처리를 하고, 실패시 Exception을 발생시키게 됩니다.

그럼 또 따로 AuthenticationProvider를 등록해야해??!
아닙니다.
AuthenticationProvider는 Interface로서

public interface AuthenticationProvider {

	Authentication authenticate(Authentication authentication) throws AuthenticationException;

	boolean supports(Class<?> authentication);

}

이러한 형태를 가지고 있으며 Provider가 전달 받은 데이터 위임 시, supports 메소드를 호출하여 처리가능 여부를 반환하고 authenticate를 호출하게 됩니다.
authentiate의 구현체로는 UsernamePasswordToken을 처리하여 주는 DaoAuthenticationProvider가 있습니다.
즉 DaoAuthenticationProvider에서 loadByUsername을 호출하여 인증을 최종적으로 마쳐준다고 생각하시면 됩니다!

WebSecurity

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurity extends WebSecurityConfigurerAdapter {

    private final UserService userService;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    private final Environment env;
    
    //권한에 관련된 작업
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
       // http.authorizeRequests().antMatchers("/users/**").permitAll();
       //인증이 된 상태에서만 통과 가능하도록
        http.authorizeRequests().antMatchers("/actuator/**").permitAll();
        http.authorizeRequests().antMatchers("/**")
                .hasIpAddress("clientIp")
                .and()
                .addFilter(getAuthenticationFilter());

        http.headers().frameOptions().disable();
    }

    private AuthenticationFilter getAuthenticationFilter() throws Exception{

        AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager(),userService,env);

        return authenticationFilter;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
       
    }
}

WebSecurityConfigurerAdater라는 클래스를 상속받은 클래스. 즉, Spring Security에 관련된 여러 동작들을 설정하는 클래스라 보시면 될 것 같습니다. Spring Security로 설정된 Filter들이 이 클래스에서 설정된 대로 동작하게 됩니다!

  • configure(HttpSecurity http)
    권한과 관련한 작업을 처리합니다. 이 코드에서는 모든 url에 대하여 clientIp를 갖고 있어야 하며 AuthenticationFilter()를 추가하여 놓았습니다.

  • configure(AuthenticationManagerBuilder auth)

    AuthnticationManager를 생성하기 위해서는 AuthenticationManagerBuilder를 사용

인증과 관련한 작업을 처리합니다. loadByUsername이 구현된 클래스와 loadByUsername에서 사용할 passwordEncoder(아까 bean으로 등록했져!)를 등록하시면 됩니다!
DaoAuthenticationProvider가 이곳에서 사용된다고 보시면 될 것 같습니다!

이렇게 단계적으로 UsernamePasswordAuthentucationFilter가 어떻게 사용되는 지 보셨습니다. 이상으로 포스팅을 마치겠습니다. 감사합니다:)

profile
WEB STUDY & etc.. HELLO!

0개의 댓글