Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 | 31 |
Tags
- 오블완
- mysql 표 출력
- 맥
- 인프런
- VI
- 분할정복 방법
- mycli
- 인프런워밍업클럽
- 욕심쟁이 방법
- Pager
- CS스터디
- table status
- 네트워킹데이
- zsh theme
- 순차탐색
- mysql 표
- cs
- 스터디2기
- MySQL
- 동적 프로그래밍 방법
- 터미널
- spring boot
- 오일러 경로
- 이진탐색
- oh-my-zsh
- 데이크스트라
- 알고리즘
- Less
- 티스토리챌린지
- zsh
Archives
- Today
- Total
Develop
[Spring Boot] 게시판 - 14. Spring Security 로그인 구현 본문
안녕하세요 ~ .ᐟ
오늘은 Spring Security를 활용한 기본 로그인 기능을 구현했습니다.
JWT는 내일 추가할 예정이고, 오늘은 Spring Security의 핵심 아키텍처를 이해하고 로그인 플로우를 보여드리겠습니다.
목차
- Spring Security 란?
- Spring Security 의 아키텍처 이해
- UserDetailsService 구현
- 로그인 API 만들기
- 테스트 결과
1. Spring Security 란?
- Spring 생태계의 인증/인가 프레임워크
- 쉽게말하면 아래 두 가지를 처리해주는 도구
- "이 사람 누구야?" (인증, Authentication)
- "이 사람 이거 할 수 있어?" (인가, Authorization)
- 장점
- 검증된 보안 메커니즘을 쉽게 사용할 수 있음
- CSRF, XSS 등 보안 공격 자동 방어
- 세션/JWT 등 다양한 인증방식 지원
- 권한 관리 (`@PreAuthorize`) 편리
2. Spring Security 아키텍처 이해
- 전체 흐름
사용자가 /api/auth/login 호출
↓
AuthService.login() 실행
↓
AuthenticationManager.authenticate() 호출
↓
[내부 동작]
1) CustomUserDetailsService.loadUserByUsername(email) 호출
2) DB에서 User 조회
3) CustomUserDetails로 변환
4) PasswordEncoder로 비밀번호 비교
↓
성공 → Authentication 객체 반환
실패 → BadCredentialsException 던짐
- 핵심 용어 정리
1) Authentication (인증 객체)
- "이 사람 누구야?"에 대한 정보
- 로그인 성공하면 이 객체에 사용자 정보가 담김
2) Principal (주체)
- Authentication 안에 들어있는 "현재 사용자"
- 우리는 CustomUserDetails를 Principal로 사용
3) SecurityContext
- 현재 요청의 인증 정보를 담는 저장소
- 우리는 STATELESS 설정이라 저장 안함
4) AuthenticationManager
- 인증의 핵심 처리기
- "비밀번호 맞아?" 확인하는 역할
5) UserDetailsService
- "이 username(email)으로 사용자 찾아줘"
- DB 조회 담당
3. UserDetailsService 구현
CustomUserDetailsService
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + email));
return new CustomUserDetails(user);
}
}
왜 메서드명이 loadUserByUsername인데 email 을 받나요?
- Spring Security Interface 규칙
- 실제로는 email을 받지만, 메서드명은 그대로 써야 합니다.
CustomUserDetails
@Getter
@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {
private final User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// ROLE_USER, ROLE_ADMIN 형태로 변환
return Collections.singleton(
new SimpleGrantedAuthority("ROLE_" + user.getRole().name())
);
}
@Override
public String getUsername() {
return user.getEmail(); // email을 username으로 사용
}
// 기타 오버라이드 : 계정 상태 관련 (지금은 다 true)
}
getAuthorities()의 역할
- 이 사용자가 어떤 권한을 가졌는지 리턴
- ROLE_USER, ROLE_ADMIN 형태로 변환
- 나중에 @PreAuthorize("hasRole('ADMIN')") 같은거 쓸 때 여기서 확인
4. 로그인 API 만들기
AuthService
@Service
@RequiredArgsConstructor
public class AuthService {
private final AuthenticationManager authenticationManager;
public LoginResponse login(LoginRequest request) {
// 1. 인증 요청
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(),
request.getPassword()
)
);
// 2. 인증 성공 시 UserDetails 꺼내기
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
// 3. 응답 생성
return LoginResponse.of(
userDetails.getUserId(),
userDetails.getUsername(),
userDetails.getUser().getName(),
userDetails.getUser().getRole().name()
);
}
}
AuthenticationManager 내부 동작
1. authenticate() 호출
↓
2. CustomUserDetailsService.loadUserByUsername() 호출
↓
3. 리턴받은 UserDetails의 password와 입력 password 비교
↓
4. 일치 → Authentication 리턴
불일치 → BadCredentialsException 던짐
AuthController
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/login")
public ResponseEntity<ApiResponse<LoginResponse>> login(
@Valid @RequestBody LoginRequest request) {
LoginResponse response = authService.login(request);
return ResponseEntity.ok(
ApiResponse.success(response, "로그인에 성공했습니다.")
);
}
}
GlobalExceptionHandler에 예외 처리 추가
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ApiResponse<Void>> handleBadCredentialsException(
BadCredentialsException e) {
return buildErrorResponse(HttpStatus.UNAUTHORIZED,
"이메일 또는 비밀번호가 올바르지 않습니다.");
}
@ExceptionHandler(UsernameNotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleUsernameNotFoundException(
UsernameNotFoundException e) {
return buildErrorResponse(HttpStatus.UNAUTHORIZED,
"사용자를 찾을 수 없습니다.");
}
5. 테스트 결과 (로그인)
POST http://localhost:8080/api/auth/login
Content-Type: application/json
{
"email": "test@example.com",
"password": "Test1234!@"
}
- ✅ 로그인 성공
{
"success": true,
"message": "로그인에 성공했습니다.",
"data": {
"userId": 1,
"email": "test@example.com",
"name": "테스트",
"role": "USER"
}
}
- ❌ 비밀번호 틀림
{
"success": false,
"message": "이메일 또는 비밀번호가 올바르지 않습니다.",
"data": null
}
- ❌ 존재하지 않는 이메일
{
"success": false,
"message": "사용자를 찾을 수 없습니다.",
"data": null
}
느낀 점
- 팀프로젝트 할때도 느꼈지만, 인증된 상태에서 허용되는 URL Path를 잘 확인해야된다.
- 로그인 페이지, / 경로, 에러 페이지, API-Doc (스웨거) 경로는 열어두어야 한다.
- 로그인해야만 보이는 페이지들은 따로 권한을 줘야한다.
- 지금은 세션을 쓰고있지않기때문에 로그인 후 인증상태가 유지되지 않음 -> 내일 JWT 도입해서 Stateless 인증 구현 ㄱㄱ!

'백엔드 > 게시판 만들기' 카테고리의 다른 글
| [Spring Boot] 게시판 - 16. JWT 적용 + 스테이징 배포 트러블슈팅 기록 (1) | 2025.12.17 |
|---|---|
| [Spring Boot] 게시판 - 15. JWT 인증 구현하기 (Token 기반으로 전환) (0) | 2025.12.16 |
| [Spring Boot] 게시판 - 13. 코드 리팩토링과 배포 파이프라인 개선 (0) | 2025.12.11 |
| [Spring Boot] 게시판 - 12. JPA N+1 문제 해결하기 (EntityGraph vs Fetch Join) (0) | 2025.12.09 |
| [Spring Boot] 게시판 - 11. 동적 정렬과 필터링으로 사용자 경험 개선하기 (0) | 2025.12.09 |