You will be fine

<Spring Security> 8. REST API 로그인 만들기

by BFine
반응형

가. UsernamePasswordAuthenticationFilter

 a. 인증 처리 필터

  -  이전 포스팅에서 HttpSecurity .formLogin 메서드를 추가할때 UsernamePasswordAuthenticationFilter가 추가되는 것을 알 수 있었다.

  -  인증처리가 어떻게 이루어지는지 이 UsernamePasswordAuthenticationFilter 를 먼저 살펴보고 분석 & 테스트 해보았다.   

 

 b. 추가해보기

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilter(new UsernamePasswordAuthenticationFilter());
    }
    
    @Override
    public void configure(WebSecurity web){
        web.debug(true);
    }
}

  -  어쩌다보니 발견하였는데 아래의 debug 설정을 하면 로그에 어떤 Filter가 추가되었는지 볼 수 있었다. (열심히 breakpoint 찍으면서 했는데.. ㅜㅜ )

 -  로그를 보면 추가한 Filter들을 확인할 수 있고 위에서부터 아래의 순서로 reqeust가 처리가 이루어진다.

 -  첫번째 이미지보면 아무런 인증 데이터 없이 요청시 /로 요청을 했으니 오류가 발생 할 것 이다. 재미있는 것 자체적으로 /error를 호출하는 것을 볼 수 있다.

 - 오류가 발생했을때 왜 한번 더 처리되는지는 2021.10.11 - [공부(2021)/Spring Security] - 4. Custom 필터 추가해보기 여기에 자세히 적어두었다.

  

 c. 코드 내부 살펴보기

  -  내부를 보면 http://localhost:8080/login URL에 POST 요청을 보내면 이 UsernamePasswordAuthenticationFilter로 들어올 것 같다. 

    => 좀 더 자세히 알아보기위해 BreakPoint를 찍고 PostMan으로 테스트를 해보았다.

  -  실제로 해보면 BreakPoint로 오지 않고 403 Forbidden이 뜨는 것보고 왜 이렇게 되는지 전혀 이해가 가지 않았다. 

  -  그래서 UsernamePasswordAuthenticationFilter 의 super인 AbstractAuthenticationProcessingFilter.requiresAuthentication 메서드를 보면

  -  .requiresAuthentication가 true가 나와야 처리될텐데 저부분에서 false가 나와서 의아해서 자세히 한번 확인해보았다. 

  -  파고 들어가다 보면 request의 SerlvetPath가 /error로 처리되어있는 것을 볼 수가 있었다. 이부분을 봐도 처음엔 이해가 잘가지 않았다.

  -  왜 저부분이 /error로 처리 되어있을까 고민하면서 엄청 찾아보다가 하나 놓치고 있던 부분을 발견했다. 바로 CsrfFilter였다

  -  아까 Filter 순서 로그를 보면 CsrfFilterUsernamePasswordAuthenticationFilter 보다 먼저 실행되는걸 알 수 있었다.

  -  위의 이미지를 보면 CrsfToken이 없으면 accessDeniedHandler 에서 Forbidden 처리를 하고 종료하는 것을 볼 수 있다.

 -  즉 UsernamePasswordAuthenticationFilter로 들어온 request는 클라이언트 요청이 아닌 시큐리티 내부에서 /error 페이지를 위한 요청이므로

     request의 SerlvetPath가 /error로 셋팅되어서 들어왔던 것이었다!! (참 복잡하다...)

 -  이 문제를 해결하는 방법은 CsrfToken을 설정해주거나 disable 처리를 해주어야 한다. 편의를 위해 disable 처리로 진행했다.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.addFilter(new UsernamePasswordAuthenticationFilter());
    }

    @Override
    public void configure(WebSecurity web){
        web.debug(true);
    }
}

 -  또 UsernamePasswordAuthenticationFilter 내부를 보면 request.getParameter를 이용해서 요청 데이터를 가져오기 때문에

    Content-Type을 form-data 나 x-www-form-urlencoded로 해서 보내야 한다. (JSON은 reqeust.getReader를 통해서 가져올 수 있다.)

       => https://stackoverflow.com/questions/3831680/httpservletrequest-get-json-post-data

  -  이렇게하면 에러가 403에서 500으로 바뀐 것을 볼 수 있고 요청한 값은 제대로 가져오는 것을 확인 할 수 있다.

 -  이제 500 에러를 확인해보면 AuthenticationManager가 null인 것을 볼수가 있다. 이부분을 UsernamePasswordAuthenticationFilter에 추가해보자

 -  AuthenticationManager 를 가져오는 것은 WebSecurityConfigurerAdapter에 메서드가 있기 때문에 이를 활용하면 된다.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter();
        usernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());

        http.csrf().disable();
        http.addFilter(usernamePasswordAuthenticationFilter);
    }

    @Override
    public void configure(WebSecurity web){
        web.debug(true);
    }
}

 -  그리고 제대로 구동시에 나오는 Password를 복사해서 추가한뒤 요청을 해보면 정상적으로 인증처리가 완료된 것을 볼 수 있다.

  => 404인 이유는 인증이 완료되어 /로 리다이렉트가 되었고 아무런 Controller가 없기 때문에 나오는 것이다.

 

나. Custom 유저데이터 사용하기

 a. 인증 플로우 

  -  위의 UsernamePasswordAuthenticationFilter를 가 어떻게 동작하는지 살펴보았다. 여기에 내부를 좀더 자세히 들여다보면 인증이 처리과정을 볼수있다.

     => 인증 Flow 순서는 2021.09.05 - [공부(2021)/Spring Security] - 3. 인증&인가 처리 과정 에 적어두었다. 

  -  위에 정리한 부분은 간략한 부분이고 더 상세하게 인증처리와 연관된 클래스를 나열해보면 아래와 같다.

      1.  UsernamePasswordAuthenticationFilter

      2.  WebSecuriyConfigurerAdapter

      3.  ProviderManager (AuthenticationManger)

      4.  AbstractUserDetailsAuthenticationProvider

      5.  DaoAuthenticationProvider : 6번에서 return된 유저정보로 실제 pw를 비교하는 부분을 담당한다. 

      6.  InMemoryUserDetaillsManger (UserDetailsSerivce) :  username에 대한 유저 정보(pw 포함)를 가져온다.

   -  InMemoryUserDetaillsManger 클래스명에서 보면 알 수 있듯이 InMemory에서 유저정보를 끌어오는 구현체이다.

     => 유저가 많아질 경우 메모리 & 관리 문제가 발생할 수 있기 때문에 DB에서 유저정보를 가져와야한다. 

  

 b. UserDetailsService

  -  먼저 UserDetails 를 보면 유저정보를 담는 인터페이스이다. 여기서 사용되는 구현체는 User이다.

  -  코드를 보면 알수 있듯이 크게 유저이름(ID), 비밀번호, 권한들을 가지고 있는 것을 볼 수 있다.
  -  UserDetailsSerivce 인터페이스는 이러한 UserDetails를 반환하는 메서드를 가지고 있고 이를 통해 Custom하게 User를 다룰 수 있다.

 

 c. DB 데이터로 인증처리 구현 

@Entity
@Getter
public class Member {
   @Id
   @GeneratedValue
   private Long id;
   private String username;
   private String password;
   private String authority;
}


@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        Member member = memberRepository.findByUsername(username);
        if(member == null){
            throw new UsernameNotFoundException("User Not Found");
        }
        List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(member.getAuthority());
        grantedAuthorityList.add(simpleGrantedAuthority);

        return new User(member.getUsername(),member.getPassword(),grantedAuthorityList);
    }
}

  -  먼저 UserDetailsSerivce 인터페이스를 구현하여 UserDetailsServiceImpl 이라는 구현체를 만들고 빈으로 설정하였다.

    => 코드를 보면 JPA를 이용하여 데이터를 가져오도록 처리했다.

  -  DB에 유저 데이터를 추가해준다 (이때 주의할점은 시큐리티는 PasswordEncoder로 decode하여 비밀번호 일치 여부를 판단하기 때문에 
     DB에 입력할때도 비밀번호는 암호화 처리를 해서 저장해야한다.

  - PostMan으로 API를 테스트 해보면 정상적으로 404가 발생하여 인증에는 성공했다는 것을 확인할 수 있다.

 

반응형

블로그의 정보

57개월 BackEnd

BFine

활동하기