이 내용은 스프링 부트 쇼핑몰 프로젝트 with JPA 책을 학습한 내용입니다.
1. UserDetailService
- 데이터베이스에서 회원정보를 가져오는 인터페이스
- loadUserByUsername() 메소드를 통해 회원 정보를 조회 → 사용자의 정보와 권한을 갖는 UserDetails 인터페이스를 반환
2. UserDetails
- 회원 정보를 담는 인터페이스
- 직접 구현하거나 스프링 시큐리티에서 제공하는 User 클래스 사용(구현체)
3. MemberService 로그인/로그아웃 구현
- MemberService.java
- UserDetailsService 인터페이스를 구현하고 loadUserByUsername() 메소드 오버라이딩
- Builder 패턴을 이용하여 UserDetail 인터페이스를 구현한 User 객체 생성 후 반환
package kr.spring.member.service; import kr.spring.member.entity.Member; import kr.spring.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; 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 org.springframework.transaction.annotation.Transactional; // DB로 값을 가져오거나 controller로 가기 전에 따로 처리해야 할 기능이 있다 @Service @Transactional @Log4j2 //final이 붙은 애들을 올려준다 이게 싫으면 @autowired 쓰면 됨 //일반적으로 controller나 service에서 쓰면 상관이 없는데 test에서는 이걸 쓰면 에러가 난다. @RequiredArgsConstructor public class MemberService implements UserDetailsService { private final MemberRepository memberRepository; public Member saveMember(Member member) { // 중복확인 validateDuplicate(member); // 중복확인 후 회원 저장 return memberRepository.save(member); } private void validateDuplicate(Member member) { //이메일로 찾았을 때 값이 있는지 없는지 판단 Member findMember = memberRepository.findByEmail(member.getEmail()); // DB에서 이메일이 검색이 되면 이미 등록되어있는 회원이라는 알림 표시 if(findMember != null) { throw new IllegalStateException("이미 등록된 사용자 입니다."); } } //implements해놨으니까 추상메소드 구현 @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { //DB에 있는 email을 찾아서 잡아온다 Member member = memberRepository.findByEmail(email); // Optional을 쓰는 경우 // findMember가 null이냐를 물어보는 게 아니라, findMember가 존재하는지 안 존재하는지 물어보는 것 // Optional<Member> findMember = memberRepository.findByEmail(email); -> repository에 Optional로 선언 되어있어야 함. // 검사식 // if(findMember.isPresent()){ // throw new UsernameNotFoundException(email); // } // -> if문 줄여서 쓰기 // Member member = memberRepository.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException("해당 사용자 없음 " + email); //멤버가 없는 경우 (기존에 사용자가 없는 경우) if(member == null) { //이메일 정보를 추가해서 exception을 날린다. 이거 근데 로그인이 잘못됐다는 예외가 발생한다 throw new UsernameNotFoundException("해당 사용자가 없습니다." + email); } log.info("=================> loadUserByUsername : " + member); return User.builder() .username(member.getEmail())//이메일, 아이디 등 어떤 로직으로 로그인을 하던 그 로그인을 할 필드를 넣어준다. .password(member.getPassword()) //암호화 되어서 들어가야 함. .roles(member.getRole().toString())//역할을 string으로 넣어줘야 한다. -> enum 안 됨 .build(); } }
4. SecurityConfig 인증 filter 추가
- SecurityConfig.java
- configure(HttpSecurity) 메소드를 통해 로그인 및 로그아웃 URL 지정
- http.formLogin() - http를 통해 들어오는 form 기반 request를 이용하여 Login을 처리
- form 태그에서 사용자의 ID 부분은 default 값으로 “username” 필드
package kr.spring.config; import org.springframework.beans.factory.annotation.Configurable; 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.config.annotation.web.configuration.WebSecurityConfiguration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; // 설정 파일로 쓸 수 있다고 알려주는 어노테이션 @Configuration // 로그인을 하기 위한 설정 @EnableWebSecurity public class SecurityConfig { // 메모리를 미리 올려놔야 하기 때문에 bean 붙이기 @Bean // http 요청에 대한 보안 설정. 페이지 권한, 로그인 페이지, 로그아웃 메소드 설정 예정 public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.formLogin() // 로그인과 관련된 주소 .loginPage("/member/login") // 로그인 주소 .defaultSuccessUrl("/") // 성공 시 이동할 주소 .usernameParameter("email") // user이름을 email로 사용할 것이기 때문에 field이름을 적어줘야 함 -> username이라 적은 경우엔 안적어도 됨 .failureUrl("/member/login/error") // 로그인 실패 시 이동할 페이지 .and() .logout() // 로그아웃과 관련된 정보 .logoutRequestMatcher(new AntPathRequestMatcher("/member/logout")) // 로그아웃을 누를 때 처리할 내용 .logoutSuccessUrl("/"); http.authorizeHttpRequests() // 인증 여부 확인 -> 스프링 3.0 이하 버전은 authorizeRequests()로 설정 // 스프링 3.0 이하 버전은 antMatchers(), mvcMatchers(), regexMatchers()으로 사용 .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll() // 페이지 이동할 경우 default로 인증이 걸리도록 되어있기 때문에 추가 .requestMatchers("/css/**", "/js/**").permitAll() // 모든 사람에게 css 적용 .requestMatchers("/", "/member/**", "/item/**", "/images/**").permitAll() // 아무나 페이지에 들어올 수 있고, member, item 밑에 있는 애들은 모두 permit 허용 .requestMatchers("/admin/**").hasRole("ADMIN") // admin인 애들만 admin에 접속 가능 .anyRequest().authenticated(); // 인증 받기 http.exceptionHandling() // 권한이 없는 경우 .authenticationEntryPoint(new CustomEntryPoint()); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder();// 단방향 암호화 객체 생성 } }
- AuthenticationManagerBuilder를 통해 AuthenticationManager를 생성하여 인증 처리 수행
- UserDetailsService 인터페이스를 구현하고 loadUserByUsername 메소드를 오버라이딩한 memberService 객체를 이용하여 User 객체를 얻어낸 뒤, 지정된 비밀번호 암호화 방식으로 비밀번호가 일치하는지 검증
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder();// 단방향 암호화 객체 생성 }
5. 로그인 페이지
<!-- 앞으로 우리가 사용할 기본 템플릿 --> <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layouts/layout1}"> <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layouts/layout1}"> <!-- 사용자 CSS 추가 --> <th:block layout:fragment="css"> <style> /* 에러가 나오면 빨간색으로 표시하겠다는 말임 */ .error { color: #bd2130; } </style> </th:block> <div layout:fragment="content"> <!-- post로 날아가기 때문에 이 정보들을 들고 controller로 날아갈 수 있는 것 --> <form role="form" method="post" action="/member/login"> <div class="form-group"> <!-- 우리가 username이 아닌 email로 로그인을 하기 때문에 이건 userparameter로 security에 적어줘야 한다 --> <label th:for="email">이메일주소</label> <input type="email" name="email" class="form-control" placeholder="이메일을 입력해주세요"> </div> <div class="form-group"> <!-- default는 password. 이거 바꾸면 이것도 security에 적어줘야 한다 --> <label th:for="password">비밀번호</label> <input type="password" name="password" id="password" class="form-control" placeholder="비밀번호 입력"> </div> <p th:if="${loginErrorMsg}" class="error" th:text="${loginErrorMsg}"></p> <button class="btn btn-primary">로그인</button> <button type="button" class="btn btn-primary" onClick="location.href='/member/new'">회원가입</button> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"> </form> </div> </html>
6. thymeleaf-extras-springsecurity6 의존성 추가
- 로그인한 상태에서는 로그아웃만 노출, 상품 등록 메뉴는 관리자로 로그인 했을 때만 노출되도록 인증 및 권한에 따라 설정 변경을 도와주는 라이브러리
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE'
7. header Navbar 부분 수정
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security"> <!-- 부트스트랩 이용함 --> <div th:fragment="header"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container-fluid"> <a class="navbar-brand" href="/">Woojin's Shop</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav me-auto mb-2 mb-lg-0"> <!-- 상품등록 --> <li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')"> <a class="nav-link active" aria-current="page" href="/admin/item/new" >상품등록</a></li> <!-- 상품관리 --> <li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')"> <a class="nav-link active" aria-current="page" href="/admin/items">상품관리</a></li> <!-- 장바구니 --> <li class="nav-item" sec:authorize="isAuthenticated()"> <a class="nav-link active" aria-current="page" href="/cart">장바구니</a></li> <!-- 구매이력 --> <li class="nav-item" sec:authorize="isAuthenticated()"> <a class="nav-link active" aria-current="page" href="/orders">구매이력</a></li> <!-- 게시판 --> <li class="nav-item" sec:authorize="isAuthenticated()"> <a class="nav-link active" aria-current="page" href="/board">게시판</a></li> <!-- 로그인 --> <li class="nav-item" sec:authorize="isAnonymous()"> <a class="nav-link active" aria-current="page" href="/member/login">로그인</a></li> <!-- 로그아웃 --> <li class="nav-item" sec:authorize="isAuthenticated()"> <a class="nav-link active" aria-current="page" href="/member/logout">로그아웃</a></li> </ul> <form class="d-flex" role="search"> <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search"> <button class="btn btn-outline-success" type="submit">Search</button> </form> </div> </div> </nav> </div> </html>
8. 인증 및 권한에 따른 화면
- Guest 상태

- 로그인 상태

- ADMIN 권한 화면

'Web & Android > 스프링 부트 쇼핑몰 프로젝트 with JPA' 카테고리의 다른 글
[스프링 부트 쇼핑몰 프로젝트 with JPA] 5. Entity 공통 속성 공통화(Auditing) (0) | 2023.10.15 |
---|---|
[스프링 부트 쇼핑몰 프로젝트 with JPA] 4. 페이지 권한 설정 (0) | 2023.10.15 |
[스프링 부트 쇼핑몰 프로젝트 with JPA] 2-3. 회원가입 검증 (0) | 2023.10.15 |
[스프링 부트 쇼핑몰 프로젝트 with JPA] 2-2. 회원가입 페이지 (0) | 2023.10.15 |
[스프링 부트 쇼핑몰 프로젝트 with JPA] 2-1. 회원가입 로직 (0) | 2023.10.15 |