개발환경
- 운영체제 : Mac OS
- 통합개발환경(IDE) : IntelliJ Ultimate
- JDK version : JDK 22
- Spring Boot version : 3.3.2
- Database : MySQL
- Build Tool : Maven
- Packaging : Jar
Dependencies
- Spring Web
- Spring Data JPA
- MySQL Driver
- H2 Database
- Thymeleaf
- Lombok
- Spring Security
- Spring Boot DevTools
- querydsl
- thymeleaf-layout-dialect
- validation
- thymeleaf-extras-springsecurity6
스프링 시큐리티 설정
- config 패키지에 securityconfig 클래스 생성
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(configurer ->
configurer
.requestMatchers("/h2-console/**").permitAll() // H2 콘솔에 대한 접근 허용
.anyRequest().authenticated() // 나머지 요청에 대해 인증 필요
);
// use HTTP Basic authentication
http.httpBasic(Customizer.withDefaults());
// disable Cross Site Request Forgery(CSRF)
http.csrf(csrf -> csrf.disable());
return http.build();
}
// H2 콘솔 관련 설정
@Bean
@ConditionalOnProperty(name = "spring.h2.console.enabled", havingValue = "true")
public WebSecurityCustomizer configureH2ConsoleEnable() {
return web -> web.ignoring()
.requestMatchers(PathRequest.toH2Console());
}
- @Configuration : Spring의 구성 클래스를 나타내며, Bean 정의를 포함하는 클래스를 의미
- @EnableWebSecurity : Spring Security를 활성화. Spring Security의 설정을 정의하고 적용
💡 BCryptPasswordEncoder
비밀번호를 암호화하고 검증하는 데 사용되는 PasswordEncoder 구현체. BCrypt 알고리즘을 사용하여 비밀번호를 암호화한다. BCrypt는 비밀번호 해시를 생성하는 데 안전한 방법으로 널리 사용
SecurityFilterChain : HTTP 요청에 대한 보안 필터 체인을 정의. HttpSecurity 객체를 사용하여 필터 체인을 설정.
HttpSecurity : HTTP 요청에 대한 보안 설정을 구성하는 데 사용됨
authorizeHttpRequests(configurer -> ...) : HTTP 요청에 대한 접근 권한을 설정
requestMatchers(...) : URL 패턴에 따라 요청을 필터링.
permitAll() : 해당 URL 패턴에 대해 인증 없이 접근을 허용.
anyRequest().authenticated() : 위에서 정의된 패턴을 제외한 모든 요청은 인증된 사용자만 접근할 수 있도록 설정.
httpBasic(Customizer.withDefaults()) : HTTP Basic Authentication을 활성화. 이 방법은 간단한 인증 방법으로 사용자 이름과 비밀번호를 HTTP 헤더에 포함시켜 인증을 수행.
csrf.disable() : Cross Site Request Forgery (CSRF) 보호를 비활성화. CSRF는 웹 애플리케이션에서 악의적인 요청을 방지하기 위한 보안 메커니즘. 일반적으로 비활성화하지 않는 것이 좋지만, API 또는 비상 상황에서는 비활성화할 수 있다.
📍 내가 노션에 정리한 내용!
https://www.notion.so/5-Spring-Boot-Security-b95a14467d6f4a87937a66b8d217c2d2?pvs=4
https://www.notion.so/8-Spring-MVC-Security-3fc33d56e52e4d809dac4b7fbdc0f025?pvs=4
.requestMatchers("/**").permitAll() - 일단 스프링 시큐리티 적용안하고 바로 화면 보이도록 설정...
엔티티, DTO 코드 설정
1. 일반 유저 / 관리자 역할 코드
- com.doraflower 패키지 아래 'constant' 패키지 생성 - Role.java 생성
- enum(열거형)
https://velog.io/@mooh2jj/Java-Enum%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0
public enum Role {
USER, ADMIN
}
2. MemberFormDTO 클래스 코드 - 회원가입 화면으로부터 넘어오는 정보를 담는다.
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MemberFormDTO {
@NotBlank(message = "Name is required")
private String name;
@NotBlank(message = "Email is required")
@Pattern(regexp = "^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}$",
message = "Please enter a valid email address.")
private String email;
@NotBlank(message = "Password is required")
@Length(min=8, max=16, message = "Enter a password between 8 and 16 characters, including numbers.")
private String password;
@NotBlank(message = "Address is required")
private String address;
}
2024.07.25 - [Spring & Spring Boot] - @Lombok 및 Entity 관련 어노테이션
- 회원가입 폼에서 받을 내용 - 이름 | 이메일 주소 (로그인 시 확인) | 비밀번호 | 주소
- message는 유효성 검사시 에러가 발생하면 출력될 내용!
3. Member 클래스 코드 - 회원 정보를 저장하는 엔티티 (DB에서 관리)
@Entity
@Table(name = "member")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@Id
@GeneratedValue (strategy = GenerationType.IDENTITY)
@Column(name="member_id")
private Long memberId;
@Column(nullable = false)
private String name;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String address;
@Enumerated(EnumType.STRING)
private Role role;
public static Member createMember(MemberFormDTO memberFormDTO,
PasswordEncoder passwordEncoder) {
String password = passwordEncoder.encode(memberFormDTO.getPassword());
Member member = Member.builder()
.name(memberFormDTO.getName())
.email(memberFormDTO.getEmail())
.password(password)
.address(memberFormDTO.getAddress())
.role(Role.USER)
.build();
return member;
}
}
- 데이터베이스 테이블 이름은 'member'로 지정
- 이메일을 통해 회원 구분을 위해 'unique'설정
- Role은 string으로 저장하도록 설정
- Member 엔티티 생성하는 메소드 설정 - 파라미터로 dto와 passwordencoder를 받음
- 패스워드는 db에 암호화 돼서 저장됨
- role은 'USER'로 설정해줌 -> ADMIN 먼저 만들고 바꿔줘도 됨
Repository 코드 설정 - DAO 역할
- repository 패키지 아래 MemberRepository 인터페이스 생성
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByEmail(String email);
}
- 회원가입 시 중복된 회원이 있는지 검사하기 위해 이메일로 회원을 검사할 수 있도록 쿼리 메소드 작성
Service 코드 설정
- service 패키지 아래 MemberService 인터페이스, MemberServiceImpl 클래스 생성
특성 | 서비스 계층이 필요한 경우 | 서비스 계층이 필요없는 경우 |
비즈니스 로직의 복잡성 | 복잡한 비즈니스 로직이 있는 경우 | 비즈니스 로직이 거의 없거나 단순한 경우 |
유지보수 | 비즈니스 로직을 중앙화하여 유지 보수 용이 | 간단한 애플리케이션에서 유지 보수가 덜 필요 |
테스트 용이성 | 독립적인 테스트가 필요한 경우 | 테스트가 크게 중요하지 않은 경우 |
트랙잭션 관리 | 트랜잭션 관리를 필요로 하는 경우 | 트랜잭션 관리가 필요 없는 경우 |
계층화된 아키텍처 | MVC 패턴 등 계층화된 아키텍처를 선호하는 경우 | 단일 클래스 또는 구조화된 아키텍처가 불필요 |
public interface MemberService {
Member saveMember(Member theMember);
}
@Service
@Log4j2
@RequiredArgsConstructor
@Transactional
public class MemberServiceImpl implements MemberService, UserDetailsService {
private final MemberRepository memberRepository;
@Override
public Member saveMember(Member theMember) {
Member tempMember = memberRepository.findByEmail(theMember.getEmail());
if (tempMember != null) {
throw new IllegalStateException("Already Registered Member!");
}
return memberRepository.save(theMember);
}
}
특성 | 단일 클래스 | 인터페이스 + 구현체 |
구조 | 단순하지만 커질 수 있음 | 명확하게 구조화됨 |
유지보수 | 어려울 수 있음 | 용이함 |
테스트 | 어려울 수 있음 | 용이함 (모킹 가능) |
확장성 | 낮음 | 높음 |
의존성 주입 | 어려울 수 있음 | 쉬움 |
- @Autowired는 빈에 생성자가 1개이고 생성자의 파라미터 타입이 빈으로 등록 가능하면 사용 안해도 의존성 주입 가능..
- 이미 가입된 회원은 IllegalStateException 예외 발생
회원 가입 기능 테스트
- 마우스 오른쪽 버튼 - Go To - Test 자동생성 기능 사용
@SpringBootTest
@Transactional
@Log4j2
@RequiredArgsConstructor
@TestPropertySource(locations = "classpath:application-test.properties")
class MemberServiceImplTest {
@Autowired
MemberService memberService;
@Autowired
PasswordEncoder passwordEncoder;
@Test
@DisplayName("회원가입 테스트")
public void saveMemberTest() {
MemberFormDTO theMemberDTO = MemberFormDTO.builder()
.email("test@email.com")
.name("Dora")
.address("Daegu")
.password("1234")
.build();
// 비밀번호 인코딩 및 회원 생성
Member member = Member.createMember(theMemberDTO, passwordEncoder);
// 회원 저장
Member savedMember = memberService.saveMember(member);
// 로그로 저장된 Member 확인
log.info("Saved Member: {}", savedMember);
assertNotNull(savedMember.getMemberId()); // ID가 null이 아닌지 확인
assertEquals("test@email.com", savedMember.getEmail());
}
@Test
@DisplayName("중복 회원 가입 테스트")
public void saveDuplicateMember() {
// 객체 생성
Member member1 = new Member(null, "Dora", "test@email.com", "1234", "Daegu", ADMIN);
Member member2 = new Member(null, "Dora", "test@email.com", "1234", "Daegu", USER);
// 객체 저장
memberService.saveMember(member1);
// 예외 처리
IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> {
memberService.saveMember(member2);
});
assertTrue(thrown.getMessage().contains("Already Registered Member!"));
}
}
- h2 db에 테스트 할 거기 때문에 기존 properties 외 새로 하나 더 만들어서 경로를 지정해줬다.
- Junit의 Assertions 클래스의 assertEquals 메소드를 이용해서 저장하려고 요청했던 값과 실제 저장된 데이터를 비교함.
- assertThrows 메소드 이용하면 예외 처리 테스트가 가능.
https://junit.org/junit5/docs/5.0.1/api/org/junit/jupiter/api/Assertions.html
https://velog.io/@roycewon/JUnit-Assert-Methods1
Controller 코드 구현
- controller 패키지 아래 MemberController 생성
@Controller
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
private final PasswordEncoder passwordEncoder;
@GetMapping("/new")
public String memberForm(Model model) {
model.addAttribute("memberFormDTO", new MemberFormDTO());
return "member/memberForm";
}
@PostMapping("/new")
public String memberForm(@Valid MemberFormDTO memberFormDTO,
BindingResult bindingResult,
Model model) {
// 유효성 오류시 실행할 메서드
if (bindingResult.hasErrors()) {
System.out.println("Binding results: " + bindingResult.toString());
return "member/memberForm";
}
// 이메일 중복 시 예외처리
try {
Member member = Member.createMember(memberFormDTO, passwordEncoder);
memberService.saveMember(member);
} catch (IllegalStateException e){
model.addAttribute("errorMessage", e.getMessage());
return "member/memberForm";
}
// 성공 시 리다이렉트
return "redirect:/";
}
}
- Model에 memberFormDTO라는 이름으로 새로운 MemberFormDTO 객체를 추가. 뷰에서 이 객체를 사용하여 폼을 렌더링할 수 있다.
- localhost:8080/members/new 접속하면 member 패키지 아래에 있는 memberForm.html 화면이 출력됨.
- @PostMapping은 form 을 제출했을 때 실행되는 코드~ 페이지 구현하고 작성해도 됨!
회원가입 페이지 구현
- templates 패키지 아래 member 패키지 생성 - memberForm.html 생성
- Thymeleaf 템플릿 사용해서 작성함.
- fragments 패키지 아래에 header/footer html 만들어서 layout01.html에 fragment로 설정하고 content영역만 따로 페이지 구현해서 연동되도록 만들었습니닷,
2024.05.09 - [Spring & Spring Boot] - [Spring Boot] Thymeleaf
내용 참고: 책 '스프링 부트 쇼핑몰 프로젝트 with JPA'
'Spring & Spring Boot' 카테고리의 다른 글
Spring Boot 쇼핑몰 프로젝트 | 로그인/로그아웃 화면 연동, 페이지 권한 설정 (0) | 2024.08.01 |
---|---|
Spring Boot 쇼핑몰 프로젝트 | 로그인 기능 구현 (0) | 2024.08.01 |
JpaRepository method, Query method (0) | 2024.07.31 |
spring boot 에서 h2 database 사용하기 (IntelliJ Ultimate) (0) | 2024.07.26 |
@Lombok 및 Entity 관련 어노테이션 (0) | 2024.07.25 |