JWT 파헤치기 2편 (WIL_항해 6주차 회고)

김형준·2022년 6월 19일
0

TIL&WIL

목록 보기
38/45
post-thumbnail

Week6 TIL 리스트업


1. 회고


1) 36, 37일차

  • 프론트와 첫 협업
  • 그동안 사용했던 일반적인 form login 방식 (쿠키 세션을 이용한 방식)은 사용이 어렵다는 점을 알게 되었다.
  • 백엔드 서버측에서 JSESSIONID를 Set-Cookie에 담아 보내줘도, 프론트 엔드 서버측에서 이를 쿠키로 설정해주지 않는다.
  • 따라서 기존의 방식을 몇일 고집하다, JWT 인증 방식으로 전면 수정하게 되었다.

2) 38, 39일차

  • JWT 파헤치기
  • 로그인 시 JWT를 발급하여 프론트 엔드 서버로 던져주고, 프론트 엔드에서 이를 요청 헤더에 담아 보내며, 요청마다 JWT의 유효성을 확인하는 방법
  • 이를 구현하기 위한 JWT 및 Spring Security의 설정 구현부를 개발 플로우에 맞춰 정리했다.
  • 오늘은 이어서 실제 로그인 시 사용되는 부분을 다룬다.

2. JWT 파헤치기 2편


1) 실제로 사용자의 로그인 요청이 들어왔을 때

  1. UserController, UserService, UserDetailsServiceImpl 구현

    1) UserController에 회원가입, 로그인, 재발급을 처리하는 API를 구현한다.

    • 이 때 @RequestBody로 받는 DTO 객체도 만들어준다.

    2) UserService에 회원가입, 로그인, 재발급을 처리하는 서비스 로직을 구현한다.

    • 회원가입의 경우 간단한 중복검사만 수행하고, Member 객체를 생성하여 DB에 저장해주는 로직.
    • 로그인의 경우 RequestDto로 받아온 username, password로 UsernamePasswordAuthenticationToken을 생성한다.
    • Authentication 객체를 정의하는데, 우항에는 authenticationManagerBuilder의 authenticate()에 위에서 생성한 토큰을 넘겨 검증한다.
    • 위에서 생성한 Authentication 객체를 TokenProvider의 generateTokenDto 함수에 넘겨주며 tokenDto를 생성한다.
    • tokenDto에는 accessToken, refreshToken 등의 정보가 담겨있다.
    • tokenDto에 담겨있는 refreshToken을 DB에 저장한다.
    • 리턴 값으로 tokenDto를 넘겨주고, Controller는 이를 ResponseEntity 객체로 넘겨준다. (프론트 서버에서 ResponseBody에 담겨있는 것을 확인할 수 있다.)
    • 재발급의 경우 RefreshToken을 검증하고, DB에 저장된 RefreshToken을 가져와 일치하는지 추가 검사한다. 일치한다면 새로운 토큰을 생성하여 RefreshToken을 업데이트해주고 다시 controller의 ResponseEntity에 담아 보내준다.

    3) UserDetailsServcieImpl 구현하기

    • UserDetailsService 인터페이스를 구현한 구현체 클래스를 구현한다.
    • loadUserByUsername 메소드를 오버라이드 하는데 이 함수는 위 userService의 로그인 과정에서 authenticate()를 통해 트리거 된다.
    • 그렇다면 정확히 어디에서 loadUserByUsername()이 호출되는 것일까??
    • DaoAuthenticationProvider의 retrieveUser() 메소드 내부에 this.getUserDetailsService().loadUserByUsername()이 호출되는 것을 확인할 수 있다.
    • retrieveUser()는 DaoAuthenticationProvider의 부모 클래스인 AbstractUserDetailsAuthenticationProvider에서 호출한다.
    • AbstractUserDetailsAuthenticationProvider에서 retrieveUser()를 통해 불러온 user 객체를 다시 additonalAuthenticationChecks()에 파라미터로 넘겨주는데, 이는 DaoAuthenticationProvider에 오버라이드로 구현되어있다.
    • 즉 DaoAuthenticationProvider의 오버라이드 메서드인 additonalAuthenticationChecks()에 의해 비밀번호 검증이 이루어진다. (passwordEncoder의 matches 메서드를 통해 비교, 일반 스트링 비교는 정상적으로 비교되지 않는다.)
    • 그렇다면 AbstractUserDetailsAuthenticationProvider의 authenticate()는 어디에서 호출되는 것인가?
    • ProviderManager의 authenticate()에서 AuthenticationProvider라는 인터페이스를 호출하는데, 이는 AbstractUserDetailsAuthenticationProvider의 상위 인터페이스이며,
    • memberService가 login 메서드에서 authenticationManagerBuilder.getObject().authenticate(authenticationToken)를 통해 ProviderManager의 authenticate()를 호출하는 것을 알 수 있다.
    • ** 여기에서 memberService는 UserController의 로직을 처리하는 서비스다. (기존에 있던 서비스를 그대로 사용하려다보니 클래스명이 맞지 않았다 😅)

loadUserByUsername이 호출되는 과정

  • memberService에서 AuthenticationManagerBuilder를 주입받고, AuthenticationManagerBuilder에서 AuthenticationManager를 구현한 ProviderManager를 생성한다.
  • ProviderManager는 AbstractUserDetailsAuthenticationProvider의 자식 클래스인 DaoAuthenticationProvider를 주입받아 호출한다.
  • DaoAuthenticationProvider의 authenticate()에서 retrieveUser()로 DB에 있는 사용자 정보를 가져오고, additonalAuthenticationChecks()를 통해 비밀번호를 비교한다.
  • 이 때 retireveUser() 내부에서 UserDetailsService 인터페이스를 직접 구현한 UserDetailsServiceImpl의 오버라이드 메소드인 loadUserByUsername()이 호출되는 것이다.
  • loadUserByUsername()는 호출되면, UserDetailsImpl()에 찾아온 member객체를 파라미터로 던지며 리턴하는데,
  • 해당 리턴으로 Authentication에 UserDetailsImpl()이 담기고, 이는 SecurityContextHolder의 SecurityContext에 담기게 된다. 이 과정에서 인증이 완료되면 HttpSession에 저장되어 애플리케이션 전반에 걸쳐 전역적인 참조가 가능해진다. 서버측 세션 저장소에는 SESSIONID를 Key, member 정보를 Value로 저장하게된다.
  • 이 때, HttpSession에는 SPRING_SECURITY_CONTEXT라는 이름으로 저장된다.
  • (이를 브라우저의 쿠키에 Set하여 인증하던 방식이 스프링 시큐리티의 쿠키 세션 인증 방식)
//DaoAuthenticationProvider 클래스
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
	...

	@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}
    ...
}

2) 구현 코드


3. 회고 (40, 41, 42일차)

  • 이번 주는 거의 스프링 시큐리티에 허덕이는 시간이 대부분이었다.
  • 아직 스프링 시큐리티도 완전히 이해하지 못한 상황에서, 프론트와 협업하며 다른 도메인간 생기는 여러 제한들에 많이 힘들었다
  • 사실 JWT로만 구현을 하는 방법도 있었지만, 굳이 스프링 시큐리티와 같이 사용한 이유는, 스프링 시큐리티의 로직을 조금이나마 더 이해하고 싶었기 때문이다.
  • 이번주는 처음으로 매일 꾸준히 쓰던 TIL을 2일, 3일에 한번씩 쓰게되었는데, 그만큼 오류를 많이 마주하여 작성할 시간이 없었던 것 같다..
  • 다시 페이스 조절하며 꾸준히 TIL을 작성해보는 시간을 가져야겠다.
  • 이번 주는 클론 프로젝트가 시작되었는데, 벨로그를 클론하기로 했다.
  • 3차 (최종) 목표는 벨로그를 사용하며 느꼈던 불편함을 해소하는 것이다.
  • 이번주도 화이팅!!!!!
profile
BackEnd Developer

0개의 댓글