❗LdapUserDetailsManager는 회사에서 사용 안하기 때문에 튜토리얼 제외
application.properties에 단일 사용자를 정의하는 대신, InMemoryUserDetailsManager를 이용하여 여러 사용자 정의해보기
🟣 코드
package com.example.springsecurity61.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class ProjectSecurityConfig {
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
(requests) -> requests.requestMatchers("/myAccount", "/myBalance", "/myLoans", "/myCards").authenticated()
.requestMatchers("/notices", "/contact").permitAll())
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public InMemoryUserDetailsManager userDetailsService() {
// Approach1 passwordEncoder 없이 테스트 (deprecated 뜸)
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("123123")
.authorities("admin")
.build();
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("123123")
.authorities("read")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
}
package com.example.springsecurity61.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class ProjectSecurityConfig {
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
(requests) -> requests.requestMatchers("/myAccount", "/myBalance", "/myLoans", "/myCards").authenticated()
.requestMatchers("/notices", "/contact").permitAll())
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public InMemoryUserDetailsManager userDetailsService() {
// Approach 2 NoOpPasswordEncoder Bean 사용
UserDetails admin = User.withUsername("admin")
.password("123132")
.authorities("admin")
.build();
UserDetails user = User.withUsername("user")
.password("123123")
.authorities("read")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
/**
* NoOpPasswordEncoder is not recommended for production usage.
* Use only for non-prod.
*
* @return PasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
🟢 UserDetailsService (인터페이스)
목적: 특정 사용자의 정보를 로드하는 핵심 인터페이스입니다.
메서드: loadUserByUsername(String username)
설명: 사용자 이름을 기반으로 사용자의 세부 정보를 로드합니다. 일반적으로 인증 과정에서 사용됩니다.
🟢 UserDetailsManager (인터페이스)
목적: UserDetailsService의 확장으로, 새 사용자를 생성하고 기존 사용자를 업데이트하는 기능을 제공합니다.
메서드:
createUser(UserDetails user): 새 사용자 생성
updateUser(UserDetails user): 기존 사용자 업데이트
deleteUser(String username): 사용자 삭제
changePassword(String oldPwd, String newPwd): 비밀번호 변경
userExists(String username): 사용자 존재 여부 확인
설명: 사용자 관리 기능을 위한 중요한 인터페이스로, 사용자 생성, 업데이트, 삭제 등의 작업을 수행할 수 있습니다.
🟢 InMemoryUserDetailsManager (클래스)
🟢 JdbcUserDetailsManager (클래스)
🟢 LdapUserDetailsManager (클래스)
목적: Spring Security 팀에서 제공하는 UserDetailsManager의 구현 예제 클래스들입니다.
설명:
InMemoryUserDetailsManager: 메모리 내에서 사용자 정보를 관리합니다. 주로 개발 및 테스트에 사용됩니다.
JdbcUserDetailsManager: 관계형 데이터베이스를 통해 사용자 정보를 관리합니다.
LdapUserDetailsManager: LDAP 데이터베이스를 통해 사용자 정보를 관리합니다.
🟢 Principal (인터페이스) ---> Authentication (인터페이스) ---> UsernamePasswordAuthenticationToken (클래스)
메서드:
getName(): 사용자 이름 반환
getPrincipal(): 인증 주체 반환
getAuthorities(): 사용자 권한 반환
getCredentials(): 사용자 자격 증명 반환
getDetails(): 인증 세부 정보 반환
isAuthenticated(): 인증 여부 확인
setAuthenticated(): 인증 상태 설정
eraseCredentials(): 자격 증명 지우기
설명: 인증은 사용자가 자신이 주장하는 사람인지 확인하는 과정입니다. Authentication 인터페이스는 인증 정보를 나타내며, UsernamePasswordAuthenticationToken 클래스는 사용자 이름과 비밀번호를 기반으로 한 인증을 나타냅니다.
사용처: Authentication은 인증이 성공적인지 여부를 판단하는 모든 시나리오에서 반환 타입입니다. 예를 들어, AuthenticationProvider 및 AuthenticationManager 내부에서 사용됩니다.
🟢 UserDetails (인터페이스) ---> User (클래스)
메서드:
getPassword(): 비밀번호 반환
getUsername(): 사용자 이름 반환
getAuthorities(): 사용자 권한 반환
isAccountNonExpired(): 계정 만료 여부 확인
isAccountNonLocked(): 계정 잠금 여부 확인
isEnabled(): 계정 활성화 여부 확인
isCredentialsNonExpired(): 자격 증명 만료 여부 확인
eraseCredentials(): 자격 증명 지우기
설명: UserDetails 인터페이스는 사용자의 세부 정보를 나타냅니다. 이 정보는 인증 과정에서 사용되며, User 클래스는 이를 구현한 예제입니다.
사용처: UserDetails는 저장 시스템에서 사용자 정보를 로드하는 모든 시나리오에서 반환 타입입니다. 예를 들어, UserDetailsService 및 UserDetailsManager 내부에서 사용됩니다.
결론
UserDetails와 Authentication은 Spring Security에서 사용자의 세부 정보와 인증 정보를 나타내는 중요한 구성 요소입니다. UserDetails는 사용자의 세부 정보를 관리하며, Authentication은 인증 과정을 담당합니다. 이 두 요소는 사용자 인증 및 권한 관리의 핵심 역할을 하며, 개발자는 이를 이해하고 적절히 활용해야 합니다.
🟣 코드
-- MYSQL DB를 이용하여 스프링 시큐리티 인증 테스트
-- test.users definition
CREATE TABLE `users` (
`id` int DEFAULT NULL,
`username` varchar(100) DEFAULT NULL,
`password` varchar(100) DEFAULT NULL,
`enabled` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO test.users (id, username, password, enabled) VALUES(1, 'happy', '12345', 1);
-- test.authorities definition
CREATE TABLE `authorities` (
`id` int DEFAULT NULL,
`username` varchar(100) DEFAULT NULL,
`authority` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO test.authorities (id, username, authority) VALUES(1, 'happy', 'read');
-- customer (JPA 설치 + 앱 실행 후)
INSERT INTO `customer` (`email`, `pwd`, `role`)
VALUES ('johndoe@example.com', '54321', 'admin');
# application.properties
## spring JDBC
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=#testdb123!
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
// spring jdbc
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'mysql:mysql-connector-java:8.0.26'
// spring ldap (optional)
implementation 'org.springframework.boot:spring-boot-starter-data-ldap'
// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class ProjectSecurityConfig {
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
(requests) -> requests.requestMatchers("/myAccount", "/myBalance", "/myLoans", "/myCards").authenticated()
.requestMatchers("/notices", "/contact").permitAll())
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
/**
* NoOpPasswordEncoder is not recommended for production usage.
* Use only for non-prod.
*
* @return PasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
public UserDetailsService userDetailsService(DataSource dataSource) {
return new JdbcUserDetailsManager(dataSource);
}
}
package com.example.springsecurity61.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import org.hibernate.annotations.GenericGenerator;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy= GenerationType.AUTO,generator="native")
@GenericGenerator(name = "native",strategy = "native")
private int id;
private String email;
private String pwd;
private String role;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}
// Repository
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.example.springsecurity61.model.Customer;
import java.util.List;
@Repository
public interface CustomerRepository extends CrudRepository<Customer,Long> {
List<Customer> findByEmail(String email);
}
// UserDetail 추가
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;
import com.example.springsecurity61.model.Customer;
import com.example.springsecurity61.repository.CustomerRepository;
import java.util.ArrayList;
import java.util.List;
@Service
public class EazyBankUserDetails implements UserDetailsService {
@Autowired
private CustomerRepository customerRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String userName, password;
List<GrantedAuthority> authorities;
List<Customer> customer = customerRepository.findByEmail(username);
if (customer.size() == 0) {
throw new UsernameNotFoundException("User details not found for the user : " + username);
} else{
userName = customer.get(0).getEmail();
password = customer.get(0).getPwd();
authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(customer.get(0).getRole()));
}
return new User(userName,password,authorities);
}
}
🟢 구조
좋은 정보 얻어갑니다, 감사합니다.