프로젝트 중 로그인 구현을 맡았다. 로그인은 이메일과 비밀번호를 입력하는 방식으로 구현했다.
자격 증명에 실패하였습니다. 라는 에러가 발생했다.

디버깅을 해보니 이슈가 발생한 부분은 다음과 같았다.
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(usernamePasswordAuthenticationToken);
UsernamePasswordAuthenticationToken으로 생성한 토큰을 넘겨 권한 인증을 받는 부분이다.
디버깅 모드에서 메서드 내부 동작 흐름을 따라가다보니 retrieveUser()에서 UserDetails를 사용하는 모습을 확인할 수 있었다.

커스텀으로 만든 userDetail은 다음과 같다.
package com.ssafy.naem.global.jwt;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.ssafy.naem.domain.member.entity.BasicLoginMember;
import com.ssafy.naem.domain.member.entity.Member;
import com.ssafy.naem.domain.member.repository.BasicLoginMemberRepository;
import com.ssafy.naem.domain.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
private final BasicLoginMemberRepository basicLoginMemberRepository;
private final PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return memberRepository.findByEmail(email)
.map(this::createUserDetails)
.orElseThrow(() -> new UsernameNotFoundException("해당하는 회원을 찾을 수 없습니다."));
}
// 해당하는 User 의 데이터가 존재한다면 UserDetails 객체로 만들어서 return
private UserDetails createUserDetails(Member member) {
BasicLoginMember basicLoginMember = basicLoginMemberRepository.findByMember(member)
.orElseThrow(() -> new UsernameNotFoundException("해당하는 회원을 찾을 수 없습니다."));
return User.builder()
.username(member.getEmail())
.password(passwordEncoder.encode(basicLoginMember.getEncPassword()))
.build();
}
}
authenticate() 메서드 내부에서 loadUserByUsername() 메서드를 사용하는데
createUserDetails() 메서드에서 새로운 유저를 만들 때
member라는 객체를 파라미터로 받을 때 내부의 password는 이미 암호화가 되어있었는데, 한번 더 암호화를 해버린 것이었다!
그 결과 authenticate() 메서드를 구현하는 AbstractUserDetailsAuthenticationProvider라는 추상 클래스에서
additionalAuthenticationChecks() 라는 메서드 안에서 오류가 발생했다.
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
BcryptEncoder를 통해 userDetail과 UsernamePasswordAuthenticationToken의 비밀번호가 매치하는지 확인하는 과정에서
에러가 발생한 것 같다.
그래서 customUserDetailsService 코드를 아래와 같이 변경해서 해결했다.
private UserDetails createUserDetails(Member member) {
BasicLoginMember basicLoginMember = basicLoginMemberRepository.findByMember(member)
.orElseThrow(() -> new UsernameNotFoundException("해당하는 회원을 찾을 수 없습니다."));
return User.builder()
.username(member.getEmail())
.password(basicLoginMember.getEncPassword()) // 이 부분 변경 !!
.build();
}
알게된 것
authenticationManagerBuilder의 getObject().authenticate 메서드는 내부적으로 UserDetailsService의 loadUserByUsername 메서드를 호출해 사용자를 찾는다는 것을 알게되었다.
'✍️ 개발 기록' 카테고리의 다른 글
| [👻 잼잼, 어디가] 비동기로 성능 개선하기 (asyncio + aiohttp) (0) | 2024.08.05 |
|---|---|
| Access Token과 Refresh Token을 어떻게 전달하고 클라이언트는 어디에 저장할까? (0) | 2024.06.21 |
| JPA Auditing으로 생성일/수정일 자동 갱신 (+date format이 동작하지 않는 문제 해결) (0) | 2024.06.14 |
| redis 분산락으로 동시성 제어하기 (0) | 2024.05.21 |
| synchronized 키워드를 사용하여 임계 영역 지정하기 (1) | 2024.05.17 |