<로그인/로그아웃 기능 구현>
1) UserDetailsService 인터페이스
- 데이터베이스 사용자 정보를 불러오는 인터페이스
- 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 객체를 반환
}