Springboot JWT 로그인 - (4) UsernamePasswordAuthenticationFilter를 통한 로그인 로직 구현

2022. 6. 16. 09:53·프로젝트/JWT 방식 인증&인가 시리즈

이전 포스팅에서 정리한 내용을 기반으로 로직을 작성해겠다. 회원 로그인을 검증하기 위한 UsernamePasswordAuthenticationFilter를 상속받아 로그인 검증을 하고 토큰을 발급받는 JwtAthenticationFilter 클래스를 구현해본다.

로그인 검증 로직

https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0
  1. 로그인 요청 (username, password)
  2. UsernamePasswordAuthenticationFilter에서 [username, password]를 이용해서 정상적인 로그인 여부를 검증
    • DI로 받은 AuthenticationManager 객체를 통해 로그인을 시도한다.
    • UserDetailsService를 상속받은 PrincipalDetailsService 클래스가 호출되고 loadUserByUsername() 메소드가 실행된다. 
    • 재정의된 loadUserByUsername() 메소드에서 회원_Repository에 접근하여 회원을 찾고 검증 후
      UserDetails를 상속받은 PrincipalDetails 객체에 회원을 담아서 반환한다.
    • 스프링 시큐리티가 PasswordEncoder를 통해 password를 검증하고 확인이 되면 Authentication을 반환한다.
  3. UsernamePasswordAuthFilter에서 Authentication을 반환받고 이를 스프링 시큐리티에 다시 반환해준다.
    • authentication을 반환하므로서 시큐리티_Session에 저장한다.
      (권한 관리를 위해서 세션에 저장)
  4. 검증이 완료되었으므로 JWT를 발급받는다.

JwtAuthenticationFilter 클래스 (UsernamePasswordAuthenticatonFilter 역할)

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

	private final AuthenticationManager authenticationManager;

	// login 요청을 하면 로그인 시도를 위해서 실행되는 함수
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
		log.info("로그인 시도 : JwtAuthenticationFilter.attemptAuthentication");

		ObjectMapper om = new ObjectMapper();
		try {
		// 1. username, password 받는다.

		// 2. 정상적인 로그인 여부를 검증한다.

		// 3. 로그인 성공

		// 4. authentication을 반환해준다.

		return super.attemptAuthentication(request, response);
	}
    
	// attemptAuthentication()에서 인증이 성공되면 다음 수행되는 메서드, JWT를 발급해준다.
	@Override
	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
		log.info("인증 완료 : JwtAuthenticationFilter.successfulAuthentication");
		// 5. JWT 발급

		super.successfulAuthentication(request, response, chain, authResult);
	}
}

작성한 주석내용들을 채워나가보겠다.


1. Username, Password를 받는다.

request로 넘어오는 [username, password]를 받아서 로그인 요청 객체를 생성 후 Authenticate를 위한 UsernamePasswordAuthenticationToken을 발행한다.

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

	private final AuthenticationManager authenticationManager;

	// login 요청을 하면 로그인 시도를 위해서 실행되는 함수
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
		log.info("로그인 시도 : JwtAuthenticationFilter.attemptAuthentication");

		ObjectMapper om = new ObjectMapper();
		try {
			// 1. username, password 받는다.
			log.info("1. username, password 받는다.");
			LoginReq login = om.readValue(request.getInputStream(), LoginReq.class);
			log.info(login.toString());
            
			// username, password를 이용해서 token 발급
			UsernamePasswordAuthenticationToken authenticationToken =
					new UsernamePasswordAuthenticationToken(login.getUsername(), login.getPassword());
			log.info(authenticationToken.getPrincipal().toString());
			log.info(authenticationToken.getCredentials().toString());
			log.info("============================================================\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
		// 2. 정상적인 로그인 여부를 검증한다.

		// 3. 로그인 성공

		// 4. authentication을 반환해준다.

		return super.attemptAuthentication(request, response);
	}
}
  1. ObjectMapper를 이용하여 HttpServletRequest 요청으로부터 LoginReq 객체를 생성
  2. 생성된 LoginReq 객체에 접근하여 username, password를 꺼내서 UsernamePasswordAuthenticationToken 발급
    • 이때 username은 principal, password는 credentials가 된다.

요청 & 응답

2. 정상적인 로그인 여부를 검증한다.

(1번)에서 전달받은 로그인 정보를 이용해서 생성한 토큰을 가지고 로그인이 유효한지 검증하면된다.
이부분은 매우 명확하다. 회원의 존재 여부와 존재할 경우 해당 토큰의 (Principal == username && credentials == password)를 검증하면 된다. 하지만 패스워드를 비교하는 로직은 시큐리티 내부에서 검증되므로 따로 넣지않아도된다.
아이디/패스워드가 일치하면 알아서 authentication을 반환해주고, 아니면 연결이 종료된다.

 

authenticationManager 클래스의 authenticate()에 토큰을 넘기면 자동으로 
UserDetailsService.class -> loadUserByUsername() 메소드가 실행된다.

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

	private final AuthenticationManager authenticationManager;

	// login 요청을 하면 로그인 시도를 위해서 실행되는 함수
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
		log.info("로그인 시도 : JwtAuthenticationFilter.attemptAuthentication");

		ObjectMapper om = new ObjectMapper();
		try {
			// 1. username, password 받는다.
			// 생략...

			// 2. 정상적인 로그인 시도 여부를 검증한다.
			log.info("2. 정상적인 로그인 시도 여부를 검증한다.");
			// -> 로그인 정보를 가지고 임시로 Auth 토큰을 생성해서 인증을 확인한다.
			// -> DI 받은 authenticationManager로 로그인 시도한다.
			// -> DetailsService를 상속받은 PrincipalDetailsService가 호출되고 loadUserByUsername() 함수가 실행된다.
			// authenticate()에 토큰을 넘기면 PrincipalDetailsService.class -> loadUserByUsername() 메소드 실행된다.
			// DB에 저장되어있는 username & password가 일치하면 authentication이 생성된다.
			log.info("->> Authenticate Start");
			Authentication authentication =
					authenticationManager.authenticate(authenticationToken);
			log.info("<<-- Authenticate End");
			log.info("============================================================\n");

			// 3. PrincipalDetails를 세션에 저장한다. (권한 관리를 위해서 세션에 저장)

			// 4. JWT 토큰을 만들어서 응답해준다.

		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}
}

UserDetailsService를 상속받은 PrincipalUserDetailsService 클래스

@Service
@RequiredArgsConstructor
public class PrincipalDetailService implements UserDetailsService {

	private final MemberRepository memberRepository;

	// 시큐리티 session -> Authentication -> UserDetails
	// 시큐리티 세션(내부 Authentication(내부 UserDetails(PrincipalDetails)))
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		log.info("PrincipalDetailService.loadUserByUsername");
		log.info("LOGIN");
		Member member = memberRepository.findByUsername(username)
				.orElseThrow(() -> new BussinessException(ExMessage.MEMBER_ERROR_NOT_FOUND));

		return new PrincipalDetails(member);
	}
}

응답

3. 로그인 성공

이 부분이 수행된다는 것은 loadUserByUsername() 메서드에서 성공적으로 회원조회 및 username, password를 통한 검증이 이뤄졌다는 것을 의미한다.

따라서, 여기서 따로 수행해야할 부분은 없다. 다만 직접 확인을 위해 반환받은 authentication 객체에서 PrincipalDetails 객체를 꺼내서 username, password를 출력해보도록 하자.

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

	private final AuthenticationManager authenticationManager;

	// login 요청을 하면 로그인 시도를 위해서 실행되는 함수
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
		log.info("로그인 시도 : JwtAuthenticationFilter.attemptAuthentication");

		ObjectMapper om = new ObjectMapper();
		try {
			// 1. username, password 받는다.
			// 생략

			// 2. 정상인지 로그인 시도를 해본다.
			Authentication authentication =
					authenticationManager.authenticate(authenticationToken);
			// 생략

			// 3. 로그인이 되었다.
			log.info("3. 로그인 성공.");
			// 로그인이 되었다.
			// Authentication에 있는 인증된 Principal 객체를 PrincipalDetails 객체로 꺼낸다.
			PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
			log.info("username : " + principalDetails.getMember().getUsername());
			log.info("password : " + principalDetails.getMember().getPassword());
			log.info("============================================================\n");

			// 4. authentication을 반환해준다.
			return authentication;
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}

	// attemptAuthentication() 실행 후 인증이 정상적으로 완료되면 실행된다.
	// 따라서, 여기서 JWT 토큰을 만들어서 request 요청한 사용자에게 JWT 토큰을 response 해준다.
	@Override
	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
		log.info("인증 완료 : JwtAuthenticationFilter.successfulAuthentication");

		super.successfulAuthentication(request, response, chain, authResult);
	}
}

응답

  • 처음 요청을 받았을 때 출력했던 principal, credentials 중 credentials가 암호화된것을 확인할 수 있다. 이는 실제 DB에 저장된 회원을 잘 조회해서 가져왔다는 것으로 회원 객체는 저장될 때 PasswordEncoder에 의해 password를 암호화해서 저장하기 때문이다.

4. authentication을 반환해준다.

authentication 객체를 시큐리티_session에 저장해야 하므로 반환한다. 세션에 저장하면 편리하게 권한관리를 할 수 있다.

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

	private final AuthenticationManager authenticationManager;

	// login 요청을 하면 로그인 시도를 위해서 실행되는 함수
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
		log.info("로그인 시도 : JwtAuthenticationFilter.attemptAuthentication");

		ObjectMapper om = new ObjectMapper();
		try {
			// 1. username, password 받는다.

			// 2. 정상인지 로그인 시도를 해본다.

			// 3. 로그인이 되었다.

			// 4. authentication을 반환해준다.
			log.info("4. authentication 반환");
			return authentication;
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}

	// attemptAuthentication() 실행 후 인증이 정상적으로 완료되면 실행된다.
	// 따라서, 여기서 JWT 토큰을 만들어서 request 요청한 사용자에게 JWT 토큰을 response 해준다.
	@Override
	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
		log.info("인증 완료 : JwtAuthenticationFilter.successfulAuthentication");

		super.successfulAuthentication(request, response, chain, authResult);
	}
}

반환된 Authentication 객체가 세션에 저장된다.

응답

  • 정상적으로 모든 인증이 되고 자동적으로 successfulAuthentication() 메소드가 이어서 수행되는 것을 볼 수 있다.
  • 여기서 JWT 토큰을 최종적으로 발행해서 반환해주면 클라이언트는 그걸 가지고 요청을 주면 된다.

전체 코드

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

	private final AuthenticationManager authenticationManager;

	// login 요청을 하면 로그인 시도를 위해서 실행되는 함수
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
		log.info("로그인 시도 : JwtAuthenticationFilter.attemptAuthentication");

		ObjectMapper om = new ObjectMapper();
		try {
			// 1. username, password 받는다.
			log.info("1. username, password 받는다.");
			LoginReq login = om.readValue(request.getInputStream(), LoginReq.class);
			log.info(login.toString());
			// username, password를 이용해서 token 발급
			UsernamePasswordAuthenticationToken authenticationToken =
					new UsernamePasswordAuthenticationToken(login.getUsername(), login.getPassword());
			log.info(authenticationToken.getPrincipal().toString());
			log.info(authenticationToken.getCredentials().toString());
			log.info("============================================================\n");

			// 2. 정상인지 로그인 시도를 해본다.
			log.info("2. 정상인지 로그인 시도를 해본다.");
			// -> 로그인 정보를 가지고 임시로 Auth 토큰을 생성해서 인증을 확인한다.
			// -> DI 받은 authenticationManager로 로그인 시도한다.
			// -> DetailsService를 상속받은 PrincipalDetailsService가 호출되고 loadUserByUsername() 함수가 실행된다.
			// authenticate()에 토큰을 넘기면 PrincipalDetailsService.class -> loadUserByUsername() 메소드 실행된다.
			// DB에 저장되어있는 username & password가 일치하면 authentication이 생성된다.
			log.info("->> Authenticate Start");
			Authentication authentication =
					authenticationManager.authenticate(authenticationToken);
			log.info("<<-- Authenticate End");
			log.info("============================================================\n");

			// 3. 로그인이 되었다.
			log.info("3. 로그인 성공.");
			// 로그인이 되었다.
			// Authentication에 있는 인증된 Principal 객체를 PrincipalDetails 객체로 꺼낸다.
			PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
			log.info("username : " + principalDetails.getMember().getUsername());
			log.info("password : " + principalDetails.getMember().getPassword());
			log.info("============================================================\n");

			// 4. authentication을 반환해준다.
			// authentication 객체를 session에 저장해야 하므로 반환한다. 세션에 저장하면 편리하게 권한관리를 할 수 있다.
			// 반환된 Authentication 객체가 세션에 저장된다.
			log.info("4. authentication 반환");
			return authentication;
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}

	// attemptAuthentication() 실행 후 인증이 정상적으로 완료되면 실행된다.
	// 따라서, 여기서 JWT 토큰을 만들어서 request 요청한 사용자에게 JWT 토큰을 response 해준다.
	@Override
	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
		log.info("인증 완료 : JwtAuthenticationFilter.successfulAuthentication");

		super.successfulAuthentication(request, response, chain, authResult);
	}
}

전체 응답

 

다음 포스팅에서 JWT를 발급해보도록 하겠다.

반응형
저작자표시 비영리 변경금지 (새창열림)

'프로젝트 > JWT 방식 인증&인가 시리즈' 카테고리의 다른 글

Springboot JWT 로그인 - (6) RefreshToken 방식을 사용하도록 변경 & 코드 정리  (5) 2022.06.24
Springboot JWT 로그인 - (5) 클라이언트 요청 시 BasicAuthenticationFilter를 이용한 JWT 검증  (6) 2022.06.20
Springboot JWT 로그인 - (3) UserDetails, UserDetailsService 이해  (0) 2022.06.13
Springboot JWT 로그인 - (2) Filter 적용 테스트  (0) 2022.06.09
Springboot JWT 로그인 - (1) Filter에 대한 이해  (1) 2022.06.06
'프로젝트/JWT 방식 인증&인가 시리즈' 카테고리의 다른 글
  • Springboot JWT 로그인 - (6) RefreshToken 방식을 사용하도록 변경 & 코드 정리
  • Springboot JWT 로그인 - (5) 클라이언트 요청 시 BasicAuthenticationFilter를 이용한 JWT 검증
  • Springboot JWT 로그인 - (3) UserDetails, UserDetailsService 이해
  • Springboot JWT 로그인 - (2) Filter 적용 테스트
구름뭉치
구름뭉치
구름의 개발일기장
  • 구름뭉치
    구름 개발일기장
    구름뭉치
  • 전체
    오늘
    어제
    • ALL (283)
      • 프로젝트 (23)
        • 토스페이먼츠 PG 연동 시리즈 (12)
        • JWT 방식 인증&인가 시리즈 (6)
        • 스우미 웹 애플리케이션 프로젝트 (1)
        • 스프링부트 기본 보일러 플레이트 구축 시리즈 (2)
        • 마이크로서비스 아키텍쳐 시리즈 (1)
      • 스프링 (43)
        • 스프링부트 API 설계 정리 (8)
        • 스프링부트 RestAPI 프로젝트 (18)
        • 스프링부트 WebSocket 적용기 (3)
        • 스프링 JPA 정리 시리즈 (5)
        • 스프링 MVC (5)
        • 스프링 배치 (2)
        • 토비의 스프링 정리 (2)
      • 기술 학습 (28)
        • 아파치 카프카 (9)
        • 클린 코드 (4)
        • 디자인 패턴의 아름다움 (2)
        • 모던 자바 인 액션 (7)
        • JVM 스레드 딥다이브 (6)
      • Web (25)
        • 정리글 (20)
        • GraphQL 정리글 (2)
        • Jenkins 정리글 (3)
      • 취업 (6)
      • CS (77)
        • 네트워크 전공 수업 정리 (11)
        • OSI 7계층 정리 (12)
        • 운영체제 정리 (19)
        • 데이터베이스 정리 (5)
        • MySql 정리 (17)
        • GoF의 Design Pattern 정리 (12)
      • 알고리즘 (70)
        • 백준 (56)
        • 프로그래머스 (12)
        • 알고리즘 정리본 (1)
      • 기초 지식 정리 (2)
      • 일상 (8)
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    키보드 손목 받침대
    동유럽
    크로아티아
    부다페스트
    레이저
    류블라냐
    마우스
    mx master s3 for mac
    마우스 패드
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
구름뭉치
Springboot JWT 로그인 - (4) UsernamePasswordAuthenticationFilter를 통한 로그인 로직 구현
상단으로

티스토리툴바