일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 |
- JSON
- 윤성우 열혈자료구조
- s
- buffer
- Selection Sorting
- Stack
- list 컬렉션
- datastructure
- R
- stream
- 이것이 자바다
- 메모리구조
- 알기쉬운 알고리즘
- 혼자 공부하는 C언어
- Graph
- insertion sort
- 이스케이프 문자
- coding test
- C 언어 코딩 도장
- Serialization
- Algorithm
- C programming
- 윤성우의 열혈 자료구조
- Today
- Total
Engineering Note
[Error Handling] Spring Security 로그인 테스트에서 userParameter 설정 이슈 해결 본문
[Error Handling] Spring Security 로그인 테스트에서 userParameter 설정 이슈 해결
Software Engineer Kim 2025. 9. 12. 20:31문제 상황
Spring Security를 사용한 로그인 기능 테스트 중 인증이 실패하는 문제가 발생했습니다. 테스트 실행 시 loadUserByUsername 메소드에 null 값이 전달되어 회원을 찾을 수 없다는 오류가 발생했다.
환경 설정
HTML 로그인 폼
<form role="form" method="post" action="/members/login">
<div class="form-group">
<label th:for="email">이메일주소</label>
<input type="email" name="email" class="form-control" placeholder="이메일을 입력해주세요">
</div>
<div class="form-group">
<label th:for="password">비밀번호</label>
<input type="password" name="password" id="password" class="form-control" placeholder="비밀번호 입력">
</div>
<button class="btn btn-primary">로그인</button>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form>
Security 설정
.formLogin(form -> form
.loginPage("/members/login")
.defaultSuccessUrl("/")
.usernameParameter("email") // 중요: form의 name="email"과 매핑
.failureUrl("/members/login/error")
)
발생한 문제
실패한 테스트 코드
@Test
@DisplayName("로그인 성공 테스트")
public void loginSuccessTest() throws Exception{
String email = "test@email.com";
String password = "12345678";
this.createMember(email, password);
mockMvc.perform(formLogin()
.user(email)
.password(password)
.loginProcessingUrl("/members/login"))
.andExpect(authenticated())
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/"));
}
발생한 오류
- loadUserByUsername 메소드에 빈 문자열 전달
- "회원 데이터 없음" 로그 출력
- 인증 실패로 /members/login/error로 리다이렉트
원인 분석
Spring Security의 파라미터 처리 방식
- 일반 Spring MVC 요청
- form의 name 속성이 Controller의 DTO 필드와 자동 매핑
- <input name="email"> → memberDto.email
- Spring Security 로그인 요청
- Security Filter가 요청을 가로채서 직접 처리
- DTO를 거치지 않고 바로 loadUserByUsername으로 전달
- 기본적으로 name="username" 파라미터를 찾음
문제의 핵심
- Security Config: .usernameParameter("email") 설정
- HTML Form: name="email" 사용
- 테스트 코드: userParameter 설정 누락
테스트에서 formLogin()을 사용할 때 별도 설정 없이 기본값(username)을 찾으려 했기 때문에, 실제 전달된 email 파라미터를 인식하지 못했다.
해결 방법
수정된 테스트 코드
@Test
@DisplayName("로그인 성공 테스트")
public void loginSuccessTest() throws Exception{
String email = "test@email.com";
String password = "12345678";
this.createMember(email, password);
mockMvc.perform(formLogin()
.userParameter("email") // 추가: Security Config와 동일하게 설정
.user(email)
.password(password)
.loginProcessingUrl("/members/login"))
.andExpect(authenticated())
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/"));
}
핵심 포인트
1. 파라미터 매핑 이해하기
HTML Form name="email"
↓
Security Config .usernameParameter("email")
↓
Test Code .userParameter("email")
↓
loadUserByUsername(String email)
2. 설정의 일관성
- Security 설정: .usernameParameter("email")
- HTML Form: name="email"
- 테스트 코드: .userParameter("email")
모든 설정이 동일한 파라미터 이름을 사용해야 합니다.
3. 메소드 파라미터 이름의 자유도
// 파라미터 이름은 자유롭게 설정 가능
UserDetails loadUserByUsername(String email) // OK
UserDetails loadUserByUsername(String username) // OK
UserDetails loadUserByUsername(String userId) // OK
메소드의 파라미터 이름은 실제 동작에 영향을 주지 않으며, 가독성을 위해 의미에 맞게 명명하면 된다.
학습한 내용
- Spring Security의 요청 처리 방식: Security Filter가 로그인 요청을 가로채서 처리
- 파라미터 설정의 중요성: Security Config, HTML Form, Test Code 간의 일관성 필요
- 테스트 환경 설정: 실제 환경과 테스트 환경의 설정을 동일하게 맞춰야 함
이 경험을 통해 Spring Security의 내부 동작 방식과 테스트 설정의 중요성을 깊이 이해할 수 있었다.
그리고 중간 중간 로그를 설정한 과정이 문제를 해결하는 과정에서 많은 도움이 되었다. 이번 경우는 Security를 적용하기 위해 'UserDetailService'의 메서드를 오버라이딩 한 아래 코드에서 아래의 2개의 로그를 통해 버그의 원인을 찾을 수 있었다.
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
log.info("로그인 시도 =====> email: {}", email);
Member member = memberRepository.findByEmail(email);
if(member == null){
log.info("===== 회원 데이터 없음 =====");
throw new UsernameNotFoundException(email);
}
log.info("===== 로그인 회원 조회 성공 =====");
return User.builder()
.username(member.getEmail())
.password(member.getPassword())
.roles(member.getRole().toString())
.build();
}
추가로 Spring Security에서 중요한 개념을 정리하면 UserDetailService와 UserDetail가 있다.
UserDetailsService 인터페이스
- 데이터베이스에서 회원 정보를 가져오는 역할을 담당
- loadUserByUsername() 메서드가 존재하며, 회원 정보를 조회하여 사용자의 정보와 권한을 갖는 UserDetails 인터페이스를 반환
Spring Security에서는 UserDetailsService를 구현하고 있는 클래스를 통해 로그인 기능을 구현하다고 생각하면 된다.
UserDetail
- Spring Security에서 회원 정보를 담기 위해 사용하는 인터페이스
- 이 인터페이스를 직접 구현하거나 Spring Security에서 제공하는 User 클래스를 사용.
- User 클래스는 UserDetails 인터페이스를 구현하고 있는 클래스
- UserDetail는 DB에서 조회한 회원정보를 저장하는 DTO 역할
그리고 추가로 Spring Security를 테스트 하기 위해서는 Security Test 라이브러리를 추가로 의존성을 적용해야 한다.
maven Security Test 의존성
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
maven Security 의존성
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
loadUserByUser()메서드로 전달 된 후의 과정
loadUserByUsername이 반환한 UserDetails는 Spring Security 내부에서 AuthenticationManager와 AuthenticationProvider가 활용한다.
흐름
- 사용자가 로그인 폼 제출
- POST /members/login
- email + password 전송
- UsernamePasswordAuthenticationFilter가 요청 가로채기
- 입력받은 email과 password를 추출
- AuthenticationManager가 인증 시도
- 내부적으로 DaoAuthenticationProvider를 사용
- 여기서 UserDetailsService.loadUserByUsername(email) 호출
- DB에서 회원 정보 조회 → UserDetails 반환
- Password 검증
- 사용자가 입력한 평문 비밀번호와 UserDetails.getPassword()(암호화된 비밀번호)를 비교
- PasswordEncoder.matches(rawPassword, encodedPassword) 사용
- 인증 성공 / 실패
- 성공 → Authentication 객체 생성 후 SecurityContext에 저장
- 실패 → 로그인 실패 처리
즉, loadUserByUsername → 회원 정보 조회 후 Spring Security가 사용할 객체 반환 → Password 검증 → Authentication 객체 생성 → SecurityContext 저장
요약
- loadUserByUsername → DB 조회 + UserDetails 반환
- 반환된 UserDetails → AuthenticationProvider가 비밀번호 검증
- 검증 통과 → SecurityContextHolder에 Authentication 저장 → Spring Security 권한 관리 시작
'Error Handling' 카테고리의 다른 글
[Error Handling] Spring Security 로그아웃 버그 해결 (0) | 2025.09.14 |
---|---|
[Error Handling] DB 연결 설정 테스트 문제해결, ActiveProfile로 테스트 환경 관리 (0) | 2025.09.13 |
[Error Handling] EC2에서 Spring Boot 빌드 오류 해결 (0) | 2025.09.05 |
[Error Handling] 에러 메시지의 구조, 그리고 ‘Caused by’ (1) | 2025.07.10 |
[Error Handling] 도커 포트 포워딩 오류 해결 (0) | 2025.07.01 |