Skip to content

authenticationManager.authenticate 호출 시 디버깅 동작 정리

Carol edited this page Aug 28, 2024 · 1 revision

MemberJwtAuthenticationFilter의 역할

  1. attemptAuthentication
  • /login에서 로그인 POST 요청이 오면 해당 필터가 동작하며attemptAuthentication 메서드가 실행된다.
package org.example.catch_line.filter;

// 스프링 시큐리티의 필터
// /login 요청해서 username, password post로 전송하면
// 해당 필터가 동작
// security config에서 formLogin disable해서 동작 안함.

@Slf4j
@RequiredArgsConstructor
public class MemberJwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;
    private final JwtTokenUtil jwtTokenUtil;

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

        try {
        
		        // request에서 username, password 반환

            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(username, password);

            // UserDetailService의 loadUserByUsername() 함수가 실행됨
            // DB에 있는 username과 password가 일치하면 authentication이 리턴된다.          
            Authentication authentication =
                    authenticationManager.authenticate(authenticationToken);
                    

            MemberUserDetails principalDetail = (MemberUserDetails) authentication.getPrincipal();

            // authentication 객체가 session 영역에 저장됨. -> 로그인이 되었다는 뜻.
            // return 하는 이유는 권한 관리를 security가 대신 해주기 때문에!
            // 굳이 JWT 토큰을 사용하면서 세션을 만들 이유가 없음. 단지 권한 처리 때문에 session에 넣어준다.

            return authentication;

        } catch (Exception e) {
            throw new LoginException("로그인 실패");
        }
    }

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

        MemberUserDetails principalDetail = (MemberUserDetails) authResult.getPrincipal();
        String jwtToken = jwtTokenUtil.generateToken(principalDetail.getUsername());

        response.addHeader("Authorization", "Bearer " + jwtToken);

        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write("{\"token\": \"" + jwtToken + "\"}");

        // jwt 토큰을 쿠키에 저장
        Cookie jwtCookie = new Cookie("JWT_TOKEN", jwtToken);
        jwtCookie.setHttpOnly(true);  // XSS 공격 방지
        jwtCookie.setSecure(true);    // HTTPS에서만 사용
        jwtCookie.setPath("/");       // 애플리케이션의 모든 경로에서 사용 가능
        jwtCookie.setMaxAge(600); // 쿠키의 유효기간을 1시간으로 설정 (필요에 따라 조정 가능)

        response.addCookie(jwtCookie);

//        response.sendRedirect("http://localhost:8080/restaurants" + "?token=Bearer "+ jwtToken);

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

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        // 로그인 실패 시 처리할 로직
        log.error("unsuccessfulAuthentication 실행, 인증 실패: " + failed.getMessage());

        // 에러 메시지를 로그인 페이지로 전달
        response.sendRedirect("/login?error=true&exception=" + java.net.URLEncoder.encode(failed.getMessage(), "UTF-8"));
    }
}

username, password를 통해 인증을 거쳐 authentication 객체를 받아올 수 있다.

Authentication authentication =
                    authenticationManager.authenticate(authenticationToken);
  • authenticationManager로 로그인 시도를 하면 UserDetailsService가 호출된다.
  • loadUserByUserName()이 자동으로 실행된다.
  • UserDetails를 세션에 담고 (세션에 담지 않을 경우 권한 관리가 되지 않는다. 권한 관리 할 필요가 없다면 세션에 담을 필요는 없다.)
  1. successfulAuthentication

authentication에 성공하면 동작한다.

successfulAuthentication에서 JWT 토큰을 만들고 response의 헤더와 쿠키에 담는다.

  1. unsuccessfulAuthentication

로그인 실패 시 동작한다.

[Authentication Manager에 의한 authenticate 무한 호출 문제](https://www.notion.so/Authentication-Manager-authenticate-1f16aa12980345988bc5e99599aa3763?pvs=21)

https://hbyun.tistory.com/178

다만 authenticate 메서드가 무한으로 호출되는 문재가 발생했고 이를 해결하기 위해 authentication manager를 수정해주었다.

authentication manager는 아래와 같이 작성해주어야 한다.

@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
		DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(memberDefaultLoginService);
    provider.setPasswordEncoder(bCryptPasswordEncoder());
    return new ProviderManager(provider);
}

수정 이후 디버깅을 하면 아래와 같은 과정을 거친다.

1️⃣ MemberJwtAuthenticationFilter

  • UsernamePasswordAuthenticationFilter 상속
Authentication authentication =
                    authenticationManager.authenticate(authenticationToken);

2️⃣ MemberDefaultLoginService

  • UserDetailsService 구현
  • loadUserByUsername 호출
package org.example.catch_line.config.auth;

// http://localhost:8080/login 로그인 요청이 올 때 동작을 한다.
// Spring Security 기본

@Service
@RequiredArgsConstructor
public class MemberDefaultLoginService implements UserDetailsService {

    private final MemberDataProvider memberDataProvider;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MemberEntity member = memberDataProvider.provideMemberByEmail(new Email(username));
        return new MemberUserDetails(member);
    }
}

3️⃣ DaoAuthenticationProvider

UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
return loadedUser;

4️⃣ AbstractUserDetailsAuthenticationProvider

  • AuthenticationException을 던진다.
 try {
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
 } catch (AuthenticationException var7) {
            AuthenticationException ex = var7;
            
            if (!cacheWasUsed) { // cacheWasUsed: false
                throw ex; // ex: org.springframework.security.authentication.BadCredentialsException: 자격 증명에 실패하였습니다.
            }

            cacheWasUsed = false;
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}

5️⃣ ProviderManager

catch (AuthenticationException var15) {
    AuthenticationException ex = var15; // ex: org.springframework.security.authentication.BadCredentialsException: 자격 증명에 실패하였습니다.
		lastException = ex;
}

‼️ parent에 null이 들어가지 않으면 this.parent.authenticate가 동작하면서 UserDetailsService의 loadUserByUsername가 무한 호출된다. authentication manager를 잘 정의해야 한다.

if (result == null && this.parent != null) {
		try {
		    parentResult = this.parent.authenticate(authentication);
        result = parentResult;
    } catch (ProviderNotFoundException var12) {
    } catch (AuthenticationException var13) {
		    parentException = var13;
        lastException = var13;
    }
}

스크린샷 2024-08-26 오후 4.55.55.png

  • lastException을 던진다.
if (lastException == null) { // org.springframework.security.authentication.BadCredentialsException: 자격 증명에 실패하였습니다.
		lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
}

if (parentException == null) {
		this.prepareException((AuthenticationException)lastException, authentication); // UsernamePasswordAuthenticationToken [[email protected], Credentials=[PROTECTED], Authenticated=false, Details=null, Granted Authorities=[]]
}

throw lastException; // org.springframework.security.authentication.BadCredentialsException: 자격 증명에 실패하였습니다.

6️⃣ MemberJwtAuthenticationFilter

catch (Exception e) {
		throw new LoginException("로그인 실패");
}