-
Notifications
You must be signed in to change notification settings - Fork 3
authenticationManager.authenticate 호출 시 디버깅 동작 정리
Carol edited this page Aug 28, 2024
·
1 revision
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
를 세션에 담고 (세션에 담지 않을 경우 권한 관리가 되지 않는다. 권한 관리 할 필요가 없다면 세션에 담을 필요는 없다.)
successfulAuthentication
authentication에 성공하면 동작한다.
successfulAuthentication
에서 JWT 토큰을 만들고 response의 헤더와 쿠키에 담는다.
unsuccessfulAuthentication
로그인 실패 시 동작한다.
[Authentication Manager에 의한 authenticate 무한 호출 문제](https://www.notion.so/Authentication-Manager-authenticate-1f16aa12980345988bc5e99599aa3763?pvs=21)
다만 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);
}
수정 이후 디버깅을 하면 아래와 같은 과정을 거친다.
-
UsernamePasswordAuthenticationFilter
상속
Authentication authentication =
authenticationManager.authenticate(authenticationToken);
-
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);
}
}
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
return loadedUser;
-
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);
}
catch (AuthenticationException var15) {
AuthenticationException ex = var15; // ex: org.springframework.security.authentication.BadCredentialsException: 자격 증명에 실패하였습니다.
lastException = ex;
}
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;
}
}
-
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: 자격 증명에 실패하였습니다.
catch (Exception e) {
throw new LoginException("로그인 실패");
}