어제 오늘 내일

[Spring Security] 3편 - 내 DB 정보로 로그인하기 (UserDetailsService) 본문

IT/SpringBoot

[Spring Security] 3편 - 내 DB 정보로 로그인하기 (UserDetailsService)

hi.anna 2026. 2. 17. 07:40

지난 시간까지 시큐리티를 설치하고 비밀번호 암호화 설정을 마쳤습니다.

하지만 아직 로그인을 할 수 없었죠?

시큐리티가 우리 DB에 어떤 회원이 있는지 모르기 때문입니다.

이번 시간에는 시큐리티와 내 DB를 연결해 주는 핵심 인터페이스인 UserDetailsService를 구현해 보겠습니다.


Step 1. SecurityConfig 수정 (로그인 폼 활성화)

가장 먼저 지난 시간에 작성했던 설정 파일(SecurityConfig)을 수정해야 합니다.
Spring Boot 3.x부터는 명시적으로 설정을 안 하면 로그인 화면 자체가 안 뜨고 403 에러가 발생합니다.

  • 위치: src/main/java/com/example/board/config/SecurityConfig.java
package com.example.board.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf((csrf) -> csrf.disable()) // 개발 편의상 csrf 끔
            .authorizeHttpRequests((auth) -> auth
                .requestMatchers("/", "/login", "/join", "/test").permitAll() // 누구나 접속 가능
                .anyRequest().authenticated() // 나머지는 로그인해야 함
            )
            // ▼▼▼ [중요] 로그인 폼 기능 활성화 ▼▼▼
            .formLogin((form) -> form
                .defaultSuccessUrl("/", true) // 로그인 성공 시 메인으로 이동
                .permitAll()
            )
            .logout((logout) -> logout
                .logoutSuccessUrl("/")
                .permitAll()
            );

        return http.build();
    }

    // 비밀번호 암호화 빈 등록
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

💡 설정 코드 상세 분석 (Q&A)

Q1. .formLogin(...)은 왜 쓰나요?

  • HTML Form 태그 기반의 로그인 인증 방식을 활성화합니다.
  • 이 설정이 있으면 Spring Security는 기본적으로 /login 경로에서 제공하는 기본 로그인 페이지를 띄워주거나, 사용자가 지정한 커스텀 로그인 페이지를 처리할 준비를 합니다. (Username/Password 인증 방식)

Q2. .defaultSuccessUrl("/", true)에서 true는 무슨 뜻인가요?

  • 로그인 성공 후 이동할 페이지를 결정합니다.
  • 파라미터 의미:
    • 첫 번째 인자 "/": 로그인에 성공하면 메인 페이지(root)로 이동하겠다는 뜻입니다.
    • 두 번째 인자 true : alwaysUse 옵션입니다.
      • true일 경우: 사용자가 처음에 어떤 페이지에 접근하려 했든 상관없이, 로그인 성공 후 무조건 /로 강제 이동시킵니다.
      • false일 경우 (기본값): 사용자가 원래 /my-page에 접근하려다 로그인 페이지로 튕겼다면, 로그인 성공 후 다시 /my-page로 보내줍니다(SavedRequest).

Q3. .permitAll()을 왜 또 붙이나요?

  • 로그인 페이지(및 로그인 실패 페이지)에 대한 접근 권한을 모든 사용자에게 허용합니다.
  • 만약 이 설정이 없다면, "로그인 페이지"를 보기 위해 "로그인"을 해야 하는 모순(무한 리다이렉트 루프)에 빠질 수 있습니다. 인증되지 않은 사용자도 로그인 폼은 볼 수 있어야 하므로 필수적인 설정입니다.

 


Step 2. 회원(Member) Entity 만들기

회원 정보를 담을 테이블을 정의합니다.

  • 위치: src/main/java/com/example/board/domain/entity/Member.java
package com.example.board.domain.entity;

import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username; // 로그인 아이디

    @Column(nullable = false)
    private String password; // 암호화된 비밀번호

    @Column(nullable = false)
    private String role; // 권한 (예: ROLE_USER)

    @Builder
    public Member(String username, String password, String role) {
        this.username = username;
        this.password = password;
        this.role = role;
    }
}

Step 3. Repository 만들기

아이디(username)로 회원을 찾는 기능이 필요합니다.

  • 위치: src/main/java/com/example/board/domain/repository/MemberRepository.java
package com.example.board.domain.repository;

import com.example.board.domain.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {
    // 아이디로 회원 정보 조회
    Optional<Member> findByUsername(String username);
}

Step 4. ★핵심★ UserDetailsService 구현하기

여기가 오늘 가장 중요한 부분입니다. 시큐리티가 "이 사람 알아?"라고 물어볼 때 DB를 조회해서 대답해 주는 서비스입니다.

  • 위치: src/main/java/com/example/board/service/MyUserDetailsService.java
package com.example.board.service;

import com.example.board.domain.entity.Member;
import com.example.board.domain.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
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.stereotype.Service;

@RequiredArgsConstructor
@Service
public class MyUserDetailsService implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. DB에서 username으로 회원 조회
        Member member = memberRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("없는 회원입니다."));

        // 2. 시큐리티가 이해할 수 있는 UserDetails 객체로 변환해서 반환
        return User.builder()
                .username(member.getUsername())
                .password(member.getPassword())
                .roles(member.getRole())
                .build();
    }
}

💡 코드 동작 원리

  1. 로그인 요청: 사용자가 로그인 버튼을 누르면 시큐리티가 loadUserByUsername을 호출합니다.
  2. DB 조회: memberRepository를 통해 해당 아이디가 진짜 있는지 확인합니다.
  3. 변환 및 반환: DB에 있는 정보(Entity)를 시큐리티 전용 객체(UserDetails)로 포장해서 넘겨줍니다.
  4. 비밀번호 검증: 우리가 코드를 짜지 않아도, 시큐리티가 알아서 DB의 암호화된 비번과 사용자가 입력한 비번을 대조합니다.

Step 5. 테스트용 회원 데이터 넣기

아직 회원가입 화면이 없으니, 서버가 켜질 때 admin 계정을 DB에 강제로 집어넣겠습니다.
비밀번호는 반드시 암호화(encode)해서 넣어야 로그인이 됩니다.

  • 위치: src/main/java/com/example/board/BoardApplication.java (메인 클래스)
// ... 기존 import 생략 ...
import com.example.board.domain.entity.Member;
import com.example.board.domain.repository.MemberRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableJpaAuditing
@SpringBootApplication
public class BoardApplication {

    public static void main(String[] args) {
        SpringApplication.run(BoardApplication.class, args);
    }

    // 서버 시작 시 실행되는 코드
    @Bean
    public CommandLineRunner initData(MemberRepository memberRepository, PasswordEncoder passwordEncoder) {
        return args -> {
            // admin 계정이 없으면 생성
            if (memberRepository.findByUsername("admin").isEmpty()) {
                memberRepository.save(Member.builder()
                        .username("admin")
                        .password(passwordEncoder.encode("1234")) // 암호화 필수!
                        .role("USER")
                        .build());
            }
        };
    }
}

✅ 최종 테스트

  1. 서버 실행(Run)
  2. http://localhost:8080/test/user 접속
  • 아까와 달리 기본 로그인 화면이 나타납니다. (디자인 없는 흰 화면)
  1. 로그인 시도
  • Username: admin
  • Password: 1234
  1. 결과 확인
  • 로그인이 성공하고 메인 페이지(/)로 이동하면 성공입니다!

마무리

이제 시큐리티와 DB가 완벽하게 연결되었습니다.

하지만 지금 보는 로그인 화면은 시큐리티가 제공하는 못생긴 기본 화면입니다.
다음 4편에서는 우리가 직접 디자인한 예쁜 로그인 화면(HTML)을 만들고 연결해 보겠습니다.

 

 

반응형
Comments