티스토리 뷰
반응형
메모리 사용자(InMemoryUserDetailsManager) 와 DB 기반 사용자(UserDetailsService) 를 순서대로 구현해볼게요.
- (예제는 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) 코드가 정상 동작할 준비가 되었습니다.
반응형
'Spring Security' 카테고리의 다른 글
[Spring Security] 스프링 시큐리티 적용하기 (0) | 2025.08.13 |
---|---|
[Spring Security] 스프링 시큐리티 기본 개념 익히기 (1) | 2025.08.13 |
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- Network
- Session
- 스프링
- html css
- 서블릿
- react
- CSS
- 미들웨어
- Spring Security
- Binding
- 서브넷팅
- el
- nodejs
- 스프링 시큐리티
- 네트워크
- 제이쿼리
- Redux
- Spring
- 리액트
- a 태그
- javaserverpage
- 세션
- Servlet
- Java Server Page
- httpServletRequest
- CSS 속성
- script element
- HTML
- JSP
- 내장객체
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함