티스토리 뷰

반응형

메모리 사용자(InMemoryUserDetailsManager)DB 기반 사용자(UserDetailsService) 를 순서대로 구현해볼게요.

  1. (예제는 Spring Security 6.x / Spring Boot 3.x 기준)

 

InMemoryUserDetailsManager (빠르게 동작 확인)

구성요소

  • PasswordEncoder : 비밀번호 암호화(BCrypt)
  • InMemoryUserDetailsManager : 메모리에 사용자 저장
  • SecurityFilterChain : 폼 로그인 켜두고 URL 권한 설정
     

📂 SecurityConfig.java

package com.example.security;


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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;


@Configuration
public class SecurityConfig {


    // ✅ 비밀번호 암호화기 Bean 등록
    @Bean
    public PasswordEncoder passwordEncoder() {
        // BCryptPasswordEncoder: 해시 기반 암호화, 복호화 불가능 (보안에 안전)
        return new BCryptPasswordEncoder();
    }


    // ✅ 메모리 기반 사용자 저장소 Bean 등록
    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder encoder) {
        // User.withUsername(): username, password, roles 지정 가능
        // roles("USER") → 내부적으로 "ROLE_USER"로 매핑됨
        UserDetails user = User.withUsername("user")
                .password(encoder.encode("1234")) // 비밀번호 암호화 필수
                .roles("USER") // ROLE_USER
                .build();


        UserDetails admin = User.withUsername("admin")
                .password(encoder.encode("1234")) // 비밀번호 암호화 필수
                .roles("ADMIN") // ROLE_ADMIN
                .build();


        // InMemoryUserDetailsManager: 메모리에 사용자 저장
        return new InMemoryUserDetailsManager(user, admin);
    }


    // ✅ SecurityFilterChain Bean 등록
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // URL별 접근 권한 설정
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/public/**", "/css/**", "/js/**", "/images/**").permitAll() // 인증 없이 허용
                .requestMatchers("/admin/**").hasRole("ADMIN") // ROLE_ADMIN 필요
                .anyRequest().authenticated() // 나머지는 인증 필요
            )
            // 폼 로그인 설정
            .formLogin(form -> form
                .loginPage("/login") // 커스텀 로그인 페이지 지정
                .permitAll() // 로그인 페이지는 인증 없이 접근 가능
            )
            // 로그아웃 설정
            .logout(logout -> logout.permitAll())
            // 개발 편의를 위해 CSRF 비활성화 (실무에서는 활성 유지)
            .csrf(csrf -> csrf.disable());


        // 최종 SecurityFilterChain 반환
        return http.build();
    }
}

이 상태로 애플리케이션 실행 후:

  • /login에서 user/1234, admin/1234 로 로그인 테스트 가능
  • /admin/**는 admin만 접근 가능

 

DB 기반 사용자 인증 (실무 기본)

구성요소

  • UserDetails 구현체: 내 엔티티 스프링 보안 사용자로 변환
  • UserDetailsService 구현체: username으로 사용자 조회
  • PasswordEncoder: BCrypt
  • (선택) DaoAuthenticationProvider 등록

 

1) 테이블 스키마(예시, JPA 기준)

DDL 예시

CREATE TABLE users (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(100) NOT NULL,
    role VARCHAR(30) NOT NULL,      -- 예: ROLE_USER, ROLE_ADMIN
    enabled BOOLEAN NOT NULL DEFAULT TRUE
);

❗️

  • role을 DB에는 "ROLE_USER" 형태로 저장하면 코드에서 매핑이 명확합니다.
  • 이미 "USER"만 저장한다면, 매핑 시 "ROLE_" prefix를 붙여주세요.
     

2) 엔티티 & 레포지토리

📂 UserAccount.java

package com.example.security.user; // 사용자 엔티티 패키지


import jakarta.persistence.*; // JPA 어노테이션
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;


@Entity // JPA 엔티티 클래스
@Table(name = "users") // 매핑할 테이블명 지정
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserAccount {


    @Id // PK 지정
    @GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT
    private Long id;


    @Column(nullable=false, unique=true, length=50) // NOT NULL, UNIQUE 제약조건
    private String username;


    @Column(nullable=false, length=100) // NOT NULL, 최대 길이 100
    private String password;


    @Column(nullable=false, length=30) // ROLE_USER / ROLE_ADMIN
    private String role;


    @Column(nullable=false) // 계정 활성 여부
    private boolean enabled = true;
}

 

📂 UserAccountRepository.java

package com.example.security.user;


import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;


// JpaRepository 상속 → CRUD 메서드 자동 제공
public interface UserAccountRepository extends JpaRepository<UserAccount, Long> {


    // username으로 사용자 조회
    Optional<UserAccount> findByUsername(String username);
}

 

3) UserDetails 구현체

📂 CustomUserDetails.java

package com.example.security.auth;


import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;


import com.example.security.user.UserAccount;


// UserDetails 구현 → Spring Security에서 사용할 사용자 객체
public class CustomUserDetails implements UserDetails {


    private final UserAccount account; // DB 사용자 정보


    public CustomUserDetails(UserAccount account) {
        this.account = account;
    }


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // DB role 값 그대로 사용 (예: ROLE_USER)
        return List.of(new SimpleGrantedAuthority(account.getRole()));
    }


    @Override
    public String getPassword() { return account.getPassword(); }


    @Override
    public String getUsername() { return account.getUsername(); }


    @Override
    public boolean isAccountNonExpired() { return true; } // 계정 만료 여부


    @Override
    public boolean isAccountNonLocked() { return true; } // 계정 잠금 여부


    @Override
    public boolean isCredentialsNonExpired() { return true; } // 비밀번호 만료 여부


    @Override
    public boolean isEnabled() { return account.isEnabled(); } // 계정 활성화 여부
}

 

4) UserDetailsService 구현

📂 CustomUserDetailsService.java

package com.example.security.auth;


import com.example.security.user.UserAccount;
import com.example.security.user.UserAccountRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;


@Service // 서비스 계층 빈 등록
@RequiredArgsConstructor // final 필드 자동 생성자 주입
public class CustomUserDetailsService implements UserDetailsService {


    private final UserAccountRepository userAccountRepository; // DB 접근 레포지토리


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // username으로 사용자 조회, 없으면 예외 발생
        UserAccount account = userAccountRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + username));
        // UserDetails 구현체 반환
        return new CustomUserDetails(account);
    }
}

 

5) 시큐리티 설정 (DB 인증 사용)

📂 SecurityConfig.java

package com.example.security;


import com.example.security.auth.CustomUserDetailsService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;


@Configuration // Spring 설정 클래스
@RequiredArgsConstructor // final 필드 자동 생성자
public class SecurityConfig {


    private final CustomUserDetailsService customUserDetailsService;


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


    // ✅ DB 기반 인증 제공자 등록
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider(PasswordEncoder encoder) {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(customUserDetailsService); // DB 조회 서비스
        provider.setPasswordEncoder(encoder); // 비밀번호 인코더
        return provider;
    }


    // ✅ SecurityFilterChain
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http,
                                                   DaoAuthenticationProvider authProvider) throws Exception {
        http
            .authenticationProvider(authProvider) // DB 인증 제공자 적용
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(PathRequest.toH2Console()).permitAll()    // H2 콘솔은 로그인 없이 허용
                .requestMatchers("/", "/public/**", "/css/**", "/js/**", "/images/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN") // ROLE_ADMIN 필요
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login") // 커스텀 로그인 페이지
                .defaultSuccessUrl("/", true) // 로그인 성공 후 이동할 페이지
                .permitAll()
            )
            .logout(logout -> logout.permitAll())
            .csrf(csrf -> csrf.disable()); // 개발 단계에서만 CSRF 비활성화
// ✅ H2 콘솔은 프레임을 사용 → 동일 출처만 허용
            .headers(headers -> headers
                .frameOptions(frame -> frame.sameOrigin())
            );


        return http.build();
    }
}

참고: DaoAuthenticationProvider를 직접 등록하지 않아도 UserDetailsService + PasswordEncoder 빈이 있으면 스프링이
자동 구성합니다.

다만 명시 등록은 확장(잠금정책, 에러메시지 커스터마이징 등)에 유리합니다.

 

6) 초기 사용자 데이터(비밀번호는 반드시 BCrypt)

테스트용 INSERT (스프링 환경에서 인코딩)

// 임시 실행 코드 또는 CommandLineRunner에서
var encoder = new BCryptPasswordEncoder();
String raw = "1234";
String encoded = encoder.encode(raw);
// INSERT INTO users(username, password, role, enabled) VALUES ('admin', '{encoded}', 'ROLE_ADMIN', true);

🎯 정리 & 팁

  • roles vs authorities
  • PasswordEncoder 필수
  • 예외 메시지/핸들러
  • 테스트

 

1) 의존성 추가

Maven

<!-- JPA -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 개발용 내장 DB (택1) -->
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <scope>runtime</scope>
</dependency>
<!-- 실DB 드라이버: 사용하는 DB에 맞춰 하나만 추가 -->
<!-- MySQL -->
<dependency>
  <groupId>com.mysql</groupId>
  <artifactId>mysql-connector-j</artifactId>
  <scope>runtime</scope>
</dependency>
<!-- MariaDB -->
<!--
<dependency>
  <groupId>org.mariadb.jdbc</groupId>
  <artifactId>mariadb-java-client</artifactId>
  <scope>runtime</scope>
</dependency>
-->
<!-- PostgreSQL -->
<!--
<dependency>
  <groupId>org.postgresql</groupId>
  <artifactId>postgresql</artifactId>
  <scope>runtime</scope>
</dependency>
-->

 

Gradle (Groovy)

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'                 // 개발용
runtimeOnly 'com.mysql:mysql-connector-j'       // 실DB(예: MySQL)
// 필요 시 교체:
// runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
// runtimeOnly 'org.postgresql:postgresql'

 

Gradle (Kotlin DSL)

implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("com.h2database:h2")                 // dev
runtimeOnly("com.mysql:mysql-connector-j")       // prod
// 필요 시 교체:
// runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
// runtimeOnly("org.postgresql:postgresql")
Spring Boot BOM이 버전 관리를 해주니 별도 버전 명시는 보통 필요 없습니다.

 

2) 기본 설정 예시

(A) 개발용 H2

src/main/resources/application.yml

spring:
  datasource:
    url: jdbc:h2:mem:testdb;MODE=MySQL;DATABASE_TO_LOWER=TRUE
    # jdbc:h2:mem:testdb → 메모리 DB로 testdb라는 이름 사용
    # MODE=MySQL → H2 SQL 문법을 MySQL 스타일로 호환
    # DATABASE_TO_LOWER=TRUE → 대소문자 구분 없이 소문자로 테이블명/컬럼명 처리
    driver-class-name: org.h2.Driver   # H2 전용 드라이버
    username: sa                       # 기본 계정
    password:                          # 비밀번호 없음
  jpa:
    hibernate:
      ddl-auto: update                  # 엔티티 변경 시 DB 스키마 자동 반영 (dev 전용)
    properties:
      hibernate:
        format_sql: true                # SQL을 보기 좋게 포맷팅
    open-in-view: false                 # OSIV(Open Session In View) 비활성화 (권장)
  h2:
    console:
      enabled: true                     # H2 웹 콘솔 활성화
      path: /h2-console                  # 웹 콘솔 접근 경로 (http://localhost:8080/h2-console)


logging:
  level:
    org.hibernate.SQL: debug            # 실행되는 SQL 로그 출력
    org.hibernate.orm.jdbc.bind: trace  # SQL 파라미터 바인딩 값까지 출력

 

(B) 실DB (예: MySQL)

src/main/resources/application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/appdb?serverTimezone=Asia/Seoul&characterEncoding=utf8
    # jdbc:mysql:// → MySQL 프로토콜
    # localhost:3306 → DB 서버 주소와 포트
    # appdb → 데이터베이스 이름
    # serverTimezone=Asia/Seoul → 시간대 설정
    # characterEncoding=utf8 → 문자 인코딩
    username: appuser                   # DB 사용자명
    password: secret                    # DB 비밀번호
  jpa:
    hibernate:
      ddl-auto: validate                 # 엔티티와 DB 스키마가 일치하는지 검증만 (운영 권장)
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect # MySQL 8 전용 방언(Dialect)
        format_sql: true                # SQL 포맷팅
    open-in-view: false                  # OSIV 비활성화 (트랜잭션 범위 명확하게)


logging:
  level:
    org.hibernate.SQL: debug            # 실행되는 SQL 로그 출력
    org.hibernate.orm.jdbc.bind: trace  # SQL 파라미터 로그 출력

 

각 속성별 설명 요약

속성 설명
spring.datasource.url DB 접속 주소 (DB 종류, 서버 위치, 포트, DB명, 옵션 포함)
spring.datasource.driver-class-name JDBC 드라이버 클래스명
spring.datasource.username DB 접속 계정명
spring.datasource.password DB 접속 비밀번호
spring.jpa.hibernate.ddl-auto 엔티티와 DB 스키마 동기화 전략 (create, update, validate, none)
spring.jpa.properties.hibernate.dialect 사용하는 DB 방언(Dialect)
spring.jpa.properties.hibernate.format_sql SQL 로그 포맷팅 여부
spring.jpa.open-in-view OSIV 활성화 여부 (false 권장)
spring.h2.console.enabled H2 웹 콘솔 사용 여부
spring.h2.console.path H2 웹 콘솔 경로
logging.level.org.hibernate.SQL 실행되는 SQL 로그 레벨
logging.level.org.hibernate.orm.jdbc.bind SQL 바인딩 파라미터 로그 레벨

 

3) JPA 코드 주의점 (Spring Boot 3 / Jakarta)

  • import jakarta.persistence.*; (javax 아님)
  • 엔티티 예:
import jakarta.persistence.*;

@Entity
@Table(name = "users")
public class UserAccount {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  
@Column(nullable=false, unique=true, length=50)
  private String username;
  
@Column(nullable=false, length=100)
  private String password;
  
@Column(nullable=false, length=30)
  private String role;
  
@Column(nullable=false)
  private boolean enabled = true;
// getters/setters ...

}

 

이제 앞서 드린 DB 인증(UserDetailsService) 코드가 정상 동작할 준비가 되었습니다.

반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/08   »
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
글 보관함