티스토리 뷰

이전 포스팅에서 정리한 내용을 기반으로 로직을 작성해겠다. 회원 로그인을 검증하기 위한 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를 발급해보도록 하겠다.

반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday