- loadUserByUsername() 메서드를 가지고 있으며 UserDetails 반환
@Service
@Transactional
@RequiredArgsConstructor
public class MemberService implements UserDetailsService {
private final MemberRepository memberRepository;
// 의존성 주입, 회원 정보에 대한 CRUD 연산을 데이터베이스에 위임
public Member saveMember(Member member) { // 새 회원 저장 메서드
validateDuplicateMember(member); // 먼저 중복 검사 실행
return memberRepository.save(member);
}
private void validateDuplicateMember(Member member) {
Member findMember = memberRepository.findByEmail(member.getEmail());
if (findMember != null) {
throw new IllegalStateException("이미 가입된 회원입니다.");
}
}
@Override // UserDetailsService 인터페이스에서 선언된 loadUserByUsername 메서드 오버라이드
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException{
// 이메일을 기반으로 회원 정보 조회
Member member = memberRepository.findByEmail(email);
// 사용자 존재 여부 검사
if(member == null){
throw new UsernameNotFoundException(email);
}
// UserDetails 생성 및 반환
return User.builder()
.username(member.getEmail())
.password(member.getPassword())
.roles(member.getRole().toString())
.build();
}
}
2) SecurityConfig 시큐리티 설정
- 기존 Spring Security 중 deprecated 된 것이 많음, 람다식으로 사용 권장
@Configuration // 클래스가 Spring IoC 컨테이너에 의해 Bean 정의의 소스로 사용됨을 나타냄
@EnableWebSecurity // Spring Security 설정을 활성화
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // CSRF(크로스 사이트 요청 위조) 보호 기능을 비활성화
.authorizeRequests(auth -> auth // 요청에 대한 접근 제어 설정
.requestMatchers("/members/login", "/members/new").permitAll() // 회원 가입 페이지는 인증되지 않은 사용자도 접근 가능
.anyRequest().authenticated() // 그 외의 모든 요청은 인증된 사용자만 접근 가능
)
.formLogin(form -> form // 폼 로그인 방식 설정
.loginPage("/members/login") // 사용자 정의 로그인 페이지의 URL을 지정
.defaultSuccessUrl("/") // 로그인 성공 시 리다이렉트될 기본 URL
.usernameParameter("email") // 로그인 폼에서 사용자 이름 대신 이메일 사용
.failureUrl("/members/login/error") // 로그인 실패 시 리다이렉트될 URL
)
.logout(logout -> logout // 로그아웃 설정 정의
.logoutRequestMatcher(new AntPathRequestMatcher("/members/logout")) // 로그아웃 URL
.logoutSuccessUrl("/") // 로그아웃 성공 시 리다이렉트될 URL
);
return http.build(); // HttpSecurity 구성을 빌드하여 SecurityFilterChain 객체를 반환
}
@Bean
// 비밀번호를 안전하게 저장하고 검증하는 메커니즘을 제공하는 인터페이스
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // BCrypt 해싱 함수를 사용하여 비밀번호를 인코딩
}
}
<페이지 권한 설정>
1) 상품 등록 페이지에 접근하는 컨트롤러 ItemController 구현
package com.ecproject.onlinestore.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ItemController {
@GetMapping("admin/item/new") //관리자 접근만 가능
public String itemForm() {
return "/item/itemForm";
}
}
2) CustomAuthenticationEntryPoint 클래스:AuthenticationEntryPoint 인터페이스 구현
-Spring Security에서 인증되지 않은 사용자가 보호된 리소스에 접근하려고 할 때 실행되는 로직 정의
- commence 메서드: 사용자에게 HTTP 401 Unauthorized 에러와 "Unauthorized"라는 메시지를 전달하도록 구현
package com.ecproject.onlinestore.config;
// import문 생략
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException,
ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
3) SecurityConfig 보완
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // CSRF(크로스 사이트 요청 위조) 보호 기능을 비활성화
.authorizeRequests(auth -> auth // 요청에 대한 접근 제어 설정
.requestMatchers("/members/login", "/members/new")
.permitAll() // 회원 가입 페이지는 인증되지 않은 사용자도 접근 가능
.requestMatchers("/admin/**")
.hasRole("ADMIN") // 관리자 페이지는 'ROLE_ADMIN' 권한을 가진 사용자만 접근 가능
.anyRequest().authenticated() // 그 외의 모든 요청은 인증된 사용자만 접근 가능
)
.exceptionHandling(e -> e
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
) // 인증되지 않은 사용자가 보호된 리소스에 접근을 시도할 때
// 'CustomAuthenticationEntryPoint' 내의 'commence' 메서드가 실행됨
.formLogin(form -> form // 폼 로그인 방식 설정
.loginPage("/members/login") // 사용자 정의 로그인 페이지의 URL을 지정
.defaultSuccessUrl("/") // 로그인 성공 시 리다이렉트될 기본 URL
.usernameParameter("email") // 로그인 폼에서 사용자 이름 대신 이메일 사용
.failureUrl("/members/login/error") // 로그인 실패 시 리다이렉트될 URL
)
.logout(logout -> logout // 로그아웃 설정 정의
.logoutRequestMatcher(new AntPathRequestMatcher("/members/logout")) // 로그아웃 URL
.logoutSuccessUrl("/") // 로그아웃 성공 시 리다이렉트될 URL
);
return http.build(); // HttpSecurity 구성을 빌드하여 SecurityFilterChain 객체를 반환
}
- Spring Security의 기본 설정을 커스터마이징하여 애플리케이션의 보안 요구 사항에 맞게 조정
- 'WebSecurityConfigurerAdapter' 클래스를 상속받아 구현하는 방식은 더이상 쓰이지 않음
- 'SecurityFilterChain' 사용이 권장됨
package com.ecproject.onlinestore.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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration // 클래스가 Spring IoC 컨테이너에 의해 Bean 정의의 소스로 사용됨을 나타냄
@EnableWebSecurity // Spring Security 설정을 활성화
public class SecurityConfig {
@Bean
// HttpSecurity 객체를 사용하여 HTTP 요청에 대한 보안 구성을 정의
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(authz -> authz
.anyRequest().authenticated()) // 모든 요청에 대해 인증을 요구함
.formLogin(withDefaults()); // 기본 로그인 폼을 제공함
return http.build();
}
@Bean
// 비밀번호를 안전하게 저장하고 검증하는 메커니즘을 제공하는 인터페이스
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // BCrypt 해싱 함수를 사용하여 비밀번호를 인코딩
}
}
3) MemberRole enum 생성
- 각각의 멤버가 일반 유저인지 관리자인지 구분
package com.ecproject.onlinestore.entity;
public enum MemberRole {
USER, ADMIN // 각각의 멤버가 일반 유저인지 관리자인지 구분
}
4) DTO 생성
- DTO(Data Transfer Object): 계층 간 데이터 전송을 위해 사용되는 객체
- 클라이언트와 서버 간, 또는 서비스 계층과 데이터 액세스 계층 간의 데이터 교환을 위해 사용
- @Enumerated(EnumType.STRING):열거형 값을 문자열로 데이터베이스에 저장, 열거형의 순서가 변경되더라도 데이터베이스에 영향 X
- createMember 메서드: MemberFormDto 객체와 비밀번호를 암호화하기 위한 PasswordEncoder를 받아 새로운 Member 인스턴스를 생성하고 반환
package com.ecproject.onlinestore.entity;
import com.ecproject.onlinestore.dto.MemberFormDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.crypto.password.PasswordEncoder;
@Entity
@Getter
@Setter
@ToString
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(unique = true) // 회원은 이메일을 통해 유일하게 구분
private String email;
private String password;
private String address;
@Enumerated(EnumType.STRING)
private Role role;
public static Member createMember(MemberFormDto memberFormDto,
PasswordEncoder passwordEncoder) {
Member member = new Member();
member.setName(memberFormDto.getName());
member.setEmail(memberFormDto.getEmail());
member.setAddress(memberFormDto.getAddress());
String password = passwordEncoder.encode(memberFormDto.getPassword());
member.setPassword(password);
member.setRole(Role.ADMIN);
return member;
}
}
6) MemberRepository 생성
package com.ecproject.onlinestore.repository;
import com.ecproject.onlinestore.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByEmail(String email); //회원 가입시 중복된 회원이 있는지 이메일로 검사
}
7) MemberService 생성
package com.ecproject.onlinestore.service;
import com.ecproject.onlinestore.entity.Member;
import com.ecproject.onlinestore.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
// 의존성 주입, 회원 정보에 대한 CRUD 연산을 데이터베이스에 위임
public Member saveMember(Member member) { // 새 회원 저장 메서드
validateDuplicateMember(member); // 먼저 중복 검사 실행
return memberRepository.save(member);
}
private void validateDuplicateMember(Member member) {
Member findMember = memberRepository.findByEmail(member.getEmail());
if (findMember != null) {
throw new IllegalStateException("이미 가입된 회원입니다.");
}
}
}
8) MemberController 생성
- 회원가입이 성공하면 메인 페이지로 리다이렉트
package com.ecproject.onlinestore.controller;
import com.ecproject.onlinestore.dto.MemberFormDto;
import com.ecproject.onlinestore.entity.Member;
import com.ecproject.onlinestore.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@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(MemberFormDto memberFormDto){ // 회원 가입 양식 제출 메서드
Member member = Member.createMember(memberFormDto, passwordEncoder);
memberService.saveMember(member);
return "redirect:/";
}
}
9) MainController 생성
- 애플리케이션의 홈페이지 또는 메인 페이지를 사용자에게 보여주는 역할을 수행
- 사용자가 웹 브라우저에서 루트 URL을 방문하면, "main" 뷰가 렌더링되어 사용자에게 응답으로 전송
package com.ecproject.onlinestore.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class MainController {
@GetMapping("/")
public String main() {
return "main";
}
}
10) MemberFormDto 유효성 검증 어노테이션 추가
package com.ecproject.onlinestore.dto;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
@Getter
@Setter
public class MemberFormDto {
@NotBlank(message = "이름은 필수 입력 값입니다.")
private String name;
@NotBlank(message = "이메일은 필수 입력 값입니다.")
@Email(message = "이메일 형식으로 입력해주세요.")
private String email;
@NotBlank(message = "비밀번호는 필수 입력 값입니다.")
@Length(min=4, max=10, message = "비밀번호는 4자 이상 10자 이하로 입력해주세요.")
private String password;
@NotEmpty(message = "주소는 필수 입력값입니다.")
private String address;
}
11) MemberController 기능 추가
- 회원 가입이 성공하면 메인 페이지로 리다이렉트
- 실패하면 다시 회원 가입 페이지로 돌아가 실패 이유 출력
- @Valid: MemberFormDto 객체에 설정된 검증 조건을 자동으로 검사
- BindingResult: 검증 과정에서 발생한 에러 정보를 담음
@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()){
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:/";
// 성공적으로 회원 가입이 완료되면 루트 경로(/)로 리다이렉트
}
}
@Configuration // 클래스가 Spring IoC 컨테이너에 의해 Bean 정의의 소스로 사용됨을 나타냄
@EnableWebSecurity // Spring Security 설정을 활성화
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화
.authorizeRequests(authz -> authz
.requestMatchers("/members/new").permitAll() // '/members/new' 경로에 인증되지 않은 접근 허용
.anyRequest().authenticated() // 그 외 모든 요청에 대해 인증 요구
)
.formLogin(withDefaults()); // 기본 로그인 폼 사용
return http.build();
}
@Bean
// 비밀번호를 안전하게 저장하고 검증하는 메커니즘을 제공하는 인터페이스
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // BCrypt 해싱 함수를 사용하여 비밀번호를 인코딩
}
}
- Controller: 요청을 처리에 매핑하고 결과를 뷰에 넘겨주는 제어를 함. 주요 처리는 Controller 안에서 실행하지 않고 '도메인 레이어'의 Service를 호출.
- Form: 화면의 폼을 표현. 입력한 값을 Controller에 넘겨주며, Controller에서 화면에 결과를 출력할 때도 사용. 도메인 레이어가 애플리케이션 레이어에 의존하지 않도록 Form에서 도메인 객체로 변환하거나 도메인 객체에서 Form으로 변환하는 것을 애플리케이션 레이어에서 수행.
- View: 화면 표시를 담당.
2) 도메인 레이어
- 도메인 객체에 대해 애플리케이션의 서비스 처리를 실행.
- Entity: 서비스 처리를 실행할 때 필요한 자원.
- Service: 애플리케이션의 서비스 처리를 담당.
- Repository: 인터페이스. 데이터베이스의 데이터 조작 내용만 정의(구현 내용은 작성하지 않음).
3) 인스라스트럭처 레이어
- 도메인 객체에 대해 CRUD 조작을 해서 데이터의 영속화를 담당.
- Repositorylmpl: 도메인 레이어에서 정의한 Repository의 구현 클래스.
- O/R Mapper: O(Object; 객체)와 R(Relational; 관계형 데이터베이스) 간의 데이터를 매핑
3. 데이터베이스/프로젝트 생성
1) dependencies
- Spring Boot DevTools: 스프링 부트 개발 툴. 자동 재실행 등 개발에 편리한 기능 포함.
- Lombok: 어노테이션을 부여하는 것으로 getter와 setter 등을 코드로 작성하지 않아도 자동으로 구현해줌.
- Spring Data JDBC: 스프링 데이터에서 제공하는 OR Mapper.
- Validation: 유효성 검사 기능인 'Bean Validation'과 'Hibernate Validator'를 사용할 수 있게 함.