<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 순서 로그를 보면 CsrfFilter가 UsernamePasswordAuthenticationFilter 보다 먼저 실행되는걸 알 수 있었다.
- 위의 이미지를 보면 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가 발생하여 인증에는 성공했다는 것을 확인할 수 있다.
'공부 > Spring Security' 카테고리의 다른 글
<Spring Security> 10. JWT 살펴보기 (0) | 2021.12.11 |
---|---|
<Spring Security> 9. REST API 로그인 만들기(2) (0) | 2021.12.01 |
<Spring Security> 7. HttpSecurity 메서드 분석하기 (0) | 2021.11.14 |
<Spring Security> 6. Filter 추가 되는 과정 살펴보기 (0) | 2021.11.03 |
<Spring Security> 5. FilterChainProxy에 들어가는 Filter들 분석하기 (0) | 2021.10.21 |
블로그의 정보
57개월 BackEnd
BFine