Spring Security 6 튜토리얼 (2)

Sorrynthx Kim·2023년 8월 18일
0

스프링 시큐리티 사용자 관리 (InMemoryUserDetailsManager, JdbcUserDetailsManager, LdapUserDetailsManager)

❗LdapUserDetailsManager는 회사에서 사용 안하기 때문에 튜토리얼 제외

InMemoryUserDetailsManager (withDefaultPasswordEncoder 방식)

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);
	}
	
	 
	
}

InMemoryUserDetailsManager (PasswordEncorder 방식)

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();
    }
	
}

User Management (사용자 관리 Interface)

🟢 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 데이터베이스를 통해 사용자 정보를 관리합니다.

UserDetails & Authentication 관계 (사용자 세부 정보와 인증)

🟢 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);
    }

}

🟢 구조

profile
https://github.com/sorrynthx/Spring-Boot/tree/main/springsecurity

1개의 댓글

comment-user-thumbnail
2023년 8월 18일

좋은 정보 얻어갑니다, 감사합니다.

답글 달기