12/3 security

AI·2025년 12월 3일

개발 방식 및 테스트

repository -> service -> controller 순으로 개발
service가 비즈니스 로직을 가지고 있기에 가장 시간이 많이 걸림.
한 개씩 개발하고 테스트 반복 / 1,2,3 다 개발하고 테스트 / 모든 테스트를 만들고 1,2,3 한 개씩 개발

LoginTest

package com.mycom.myapp.webapp.springboottest;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import com.mycom.myapp.auth.controller.LoginController;
import com.mycom.myapp.auth.service.LoginService;
import com.mycom.myapp.user.dto.UserResultDto;
import com.mycom.myapp.user.entity.User;
import com.mycom.myapp.user.repository.UserRepository;

import jakarta.servlet.http.HttpSession;

@SpringBootTest
public class LoginTest {
	// 1. repository test
	@Autowired
	private UserRepository userRepository;
	
	@Test
	public void testLoginRepository() {
		Optional<User> optionalUser = userRepository.findByEmail("user3@mycom.com");
		assertTrue(optionalUser.isPresent());
	}
	
	// 2. service test
	@Autowired
	private LoginService loginService;
	
	@Test
	public void testLoginService() {
		UserResultDto userResultDto = loginService.login("user3@mycom.com","password3");
		assertNotNull(userResultDto.getUserDto());
	}
	
	// 3. controller test
	@Autowired
	private LoginController loginController;
	
	@Autowired
	private HttpSession session;
	
	@Test
	public void testLoginController() {
		UserResultDto userResultDto = loginController.login("user3@mycom.com","password3", session);
		// userResultDto의 result가 success 확인
		// session의 userDto 확인
		assertEquals("success", userResultDto.getResult());
		assertNotNull(session.getAttribute("userDto"));
	}
}

RegisterTest

package com.mycom.myapp.webapp.springboottest;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import com.mycom.myapp.auth.controller.LoginController;
import com.mycom.myapp.auth.service.LoginService;
import com.mycom.myapp.user.controller.UserController;
import com.mycom.myapp.user.dto.UserResultDto;
import com.mycom.myapp.user.entity.User;
import com.mycom.myapp.user.repository.UserRepository;
import com.mycom.myapp.user.service.UserService;

import jakarta.servlet.http.HttpSession;
import jakarta.transaction.Transactional;

@SpringBootTest
public class RegisterTest {
	// 1. repository test
	@Autowired
	private UserRepository userRepository;
	
	// 테스트 과정에서 생긴 변화(insert, update, delete)를 모두 롤백
	@Test
	@Transactional
	// 최초 홍길동 insert, @Transactional 추가 후 이길동 insert하면 테스트는 성공하고 데이터는 다시 롤백
	// service에 @Transactional은 어떻게 영향을 받는가? @Test의 @Transactional 진행에 포함, 독자적으로 동작 x
	// 추가 options 등을 통해서 다양한 설정이 있지만 복잡하고 도움이 되지 않아 추천 x
	public void testLoginRepository() {
		User user = new User();
		user.setName("이길동");
		user.setEmail("hong@gildon.com");
		user.setPassword("1234");
		userRepository.save(user);
		User savedUser = userRepository.save(user); // db에 insert되고 영속화된 savedUser
		assertNotNull(savedUser);
	}
	
	// 2. service test
	@Autowired
	private UserService userService;
	
	@Test
	@Transactional
	public void testLoginService() {
		User user = new User();
		user.setName("이길동");
		user.setEmail("hong@gildon.com");
		user.setPassword("1234");
		UserResultDto userResultDto = userService.insertUser(user);
		assertEquals("success", userResultDto.getResult());
	}
	
	// 3. controller test
	@Autowired
	private UserController userController;
	
	@Autowired
	private HttpSession session;
	
	@Test
	@Transactional
	public void testLoginController() {
		User user = new User();
		user.setName("이길동");
		user.setEmail("hong@gildon.com");
		user.setPassword("1234");
		
		UserResultDto userResultDto = userController.insertUser(user);
		assertEquals("success", userResultDto.getResult());
		assertNotNull(session.getAttribute("userDto"));
	}
}

Spring Security

  1. 개념적으로 어렵다.
  2. Spring Security 6 는 이전 5 보다 변화가 많다.
    => 1년보다 더 오래된 블로그, 유튜브 컨텐츠는 보지 않는다.
  3. 학습하기 이전에 왜 이걸 사용해야 하는지 필요성을 느껴야 한다.
    당장 현재 DB 에 관리되는 패스워드는 암호화 X

인증(Authentication)과 인가(Authorization)의 표준화 -> 개발 실수 방지
강력한 보안 공격 방어 (CSRF, Session Fixation 등)
서블릿 필터(Servlet Filter) 단계에서 요청을 가로채서 검사
Spring Security 6은 상속을 버렸음


Spring Security

실습

SecurityConfig

package com.mycom.myapp.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.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {
	// #1
	// 모든 요청에 대해 허락
	// 가장 단순한 정책을 가진 SecurityFilterChain
//	@Bean
//	SecurityFilterChain fiterChain(HttpSecurity http) throws Exception{
//		return http
//				.authorizeHttpRequests(request -> request.anyRequest().permitAll())
//				.build();
//	}
	
	// #2
	// 모든 요청에 대해 인증 필요(authenticated())
	// 인증이 필요한 설정 -> 로그인 방식 지정
//	@Bean
//	SecurityFilterChain fiterChain(HttpSecurity http) throws Exception{
//		return http
//				.authorizeHttpRequests(request -> request.anyRequest().authenticated())
//				.formLogin(Customizer.withDefaults())
//				.build();
//	}
	
	// #3
	// 일부 요청에 대해 허락하고, 나머지 요청에 대해 인증 필요(authenticated())
	// 인증이 필요한 설정 -> 로그인 방식 지정
	@Bean
	SecurityFilterChain fiterChain(HttpSecurity http) throws Exception{
		return http
				.authorizeHttpRequests(request -> {
					request.requestMatchers("/","index.html").permitAll();
					request.anyRequest().authenticated();
				})
				.formLogin(Customizer.withDefaults())
				.build();
	}
}

CustomerController

package com.mycom.myapp.user.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/customer")
public class CustomerController {
	@GetMapping("/hello")
	public String hello() {
		return "Hello, Customer";
	}
}

AdminController

package com.mycom.myapp.user.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/admin")
public class AdminController {
	@GetMapping("/hello")
	public String hello() {
		return "Hello, Admin";
	}
}

SecurityConfig

package com.mycom.myapp.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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
    
    // PasswordEncoder DI
    // MyUserDetailsService 에서 UserDetails 객체를 생성하는 과정에서 필요.
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    // #1. 
    // UserDetailsService 적용에 맞는 formLogin
    // logout 추가
    // Role 적용 X
//  @Bean
//  SecurityFilterChain filterChain(HttpSecurity http ) throws Exception{
//      return http
//              .authorizeHttpRequests( request -> { 
//                  request.requestMatchers("/", "/index.html").permitAll(); // 일부 요청
//                  request.anyRequest().authenticated(); // 나머지 요청
//              })
//              .formLogin( 
//                  form -> form.defaultSuccessUrl("/")
//                  .permitAll()
//              )
//              .logout(logout -> logout.permitAll()) // logout
//              .build();
//  }   
    
    // #2. 
    // Role 적용 O
    // Authentication( 로그인 ) -> Authorization ( 동일하게 로그인했어도 어떤 Role 을 가졌느냐에 따라 다르게 권한 부여 ) 적용 
    // HttpSecyrity http 메소드 체인 적용. request -> {} {} 제거
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http ) throws Exception{
        return http
                .authorizeHttpRequests( request -> request
                    .requestMatchers("/", "/index.html").permitAll() // 일부 요청
                    .requestMatchers("/customer/**").hasAnyRole("CUSTOMER", "ADMIN") // Role 기반 접근
                    .requestMatchers("/admin/**").hasRole("ADMIN") // Role 기반 접근
                    .anyRequest().authenticated() // 나머지 요청
                )
                .formLogin( 
                    form -> form.defaultSuccessUrl("/")
                    .permitAll()
                )
                .logout(logout -> logout.permitAll()) // logout
                .build();
    }       
}

MyUserDetailsService

package com.mycom.myapp.config;

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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;

@Service // Service Layer 중 한 개
@RequiredArgsConstructor
public class MyUserDetailsService implements UserDetailsService{
	// Spring Security 는 사용자의 패스워드를 암호화.
	// Spring Security 에 DI 요청
	// DI 되는 BCryptPasswordEncoder
	// SecurityConfig 에서 DI 설정
	private final PasswordEncoder passwordEncoder;
	
	// 기본 인증 대신 사용자별 인증을 구현
	// parameter username 을 통해서 DB Access 통해 username 에 해당하는 User 의 기본 정보를 UserDetails 객체로 만들어서 리턴한다.
	// DB Access X, 대신 dskim/1234 를 username, password 사용
	
	// #1. Role 적용 전
//	@Override
//	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//		
//		if("dskim".equals(username)) {
//			return User.builder() // User 는 Spring Security 의 User
//					.username("dskim")
//					.password(passwordEncoder.encode("1234"))
//					.build();
//		} else {
//			throw new UsernameNotFoundException("User Not Found");
//		}
//	}

	// #2. Role 적용 후
	//     사용자별 Role 은 DB Access 를 통해서 처리해야 함. 현재는 Role 도 하드코딩.
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		
		if("dskim".equals(username)) {
			return User.builder() // User 는 Spring Security 의 User
					.username("dskim")
					.password(passwordEncoder.encode("1234"))
//					.roles("CUSTOMER")
//					.roles("ADMIN")
					.roles("ADMIN", "CUSTOMER") // 현재 코드는 ADMIN 한 개만 있어도 CUSTOMER 에 접근가능한 상황. 많은 경우 1 사용자가 복수 개의 Role 을 일반적으로 가진다.
					.build();
		} else {
			throw new UsernameNotFoundException("User Not Found");
		}
	}
}

요약

개발 순서: 
Repository (DB 접근) → Service (비즈니스 로직) → Controller (요청 처리)
- LoginTest, RegisterTest 예제처럼 각 계층(Layer)을 하나씩 테스트
- @SpringBootTest: 실제 스프링 환경을 띄워서 통합 테스트를 수행
- @Transactional (테스트용): 테스트가 끝나면 DB 데이터를 자동으로 롤백하여,
							다음 테스트에 영향을 주지 않음

Spring Security 6 >

--SecurityConfig--
passwordEncoder 
- BCryptPasswordEncoder를 @Bean으로 등록하여 암호화에 사용
- SecurityConfig에서 정의하고 UserDetailsService에서 사용

@Bean
SecurityFilterChain fiterChain(HttpSecurity http) throws Exception{
	return http
			.authorizeHttpRequests(request -> {
				request.requestMatchers("/","index.html").permitAll();
				request.anyRequest().authenticated();
			})
			.formLogin(Customizer.withDefaults())
			.build();
}
- authorizeHttpRequests: URL별 접근 권한 설정
- requestMatchers(): 동일한 주소, HTTP 메서드 같이 설정 값만 통과
- permitAll(): 누구나 접근 가능
- authenticated(): 인증된 사람만 (로그인)
- formLogin(): 로그인 형태 제공
- Customizer.withDefaults(): Spring Security가 제공하는 기본 로그인 페이지와 설정
- logout(): 로그아웃 제공
- hasAnyRole(), hasRole(): role 여러개/한개 에 따른 권한 제공

-- MyUserDetailsService --
implements UserDetailsService: DB에 있는 회원 정보를 Spring Security에게 알려줌 
	=> UserDetails 객체 (암호화된 비밀번호)
- loadUserByUsername(): 로그인 시 입력한 ID(username)로 회원을 찾기
- User.builder(): 찾은 회원 정보를 스프링 시큐리티가 이해하는 UserDetails 객체로 변환하여 반환
- roles(): 역할 부여 - 합치기도 가능

0개의 댓글