개발일지

Team project - 쇼핑몰 구현(1)

준서이 2024. 4. 26. 15:41

 

이번 포스트는

회원 가입, 수정, 삭제, 조회, 로그인, 아이디 찾기, 비밀번호 찾기 등에 관한 내용을 작성할 것이다.

 

 

spring security 를 사용하여 로그인을 구현하고 각 페이지에 권한을 주어 관리자, 회원에 맞게 접근 할 수 있게 하였다.

 

 

 

build

나는 springsecurity6 버전을 사용하였다. (springboot 3.2.3버전)

implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
testImplementation 'org.springframework.security:spring-security-test'

 

 

MemberDTO

/*
작성자 : 정아름
작성일 : 24.02.19
작성내용 : 회원 구현
확인사항 : 테스트 해야함
 */

package com.example.basic.DTO;

import com.example.basic.Constant.RoleType;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.*;

import java.time.LocalDateTime;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MemberDTO {
    //회원 번호
    private Integer memberId;

    //회원 이메일
    @NotBlank (message = "이메일은 필수입니다.")
    @Email
    private String memberEmail;

    //회원 비밀번호
    @NotBlank (message = "비밀번호는 필수입니다.")
    @Pattern(regexp="(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}",
            message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 8자 ~ 20자의 비밀번호여야 합니다.")
    private String memberPassword;

    //회원 이름
    @NotBlank (message = "이름은 필수입니다.")
    @Size(min = 2, max = 10, message = "이름은 2자 이상 10자 이하여야 합니다.")
    private String memberName;

    //회원 전화번호
    private String memberPhone;

    //회원 주소
    @NotBlank (message = "주소는 필수입니다.")
    private String memberAddress;

    //회원 분류
    private RoleType roleType;

    //등록일
    private LocalDateTime regDate;

    //수정일
    private LocalDateTime modDate;
}

 

 

@Email
이메일 형식 유효성 검사(구멍이 많다고 한다. Pattern으로도 변경했는데 또한, 확실한 방법은 아니라고 한다.)
@NotBlank
@NotBlank는 null 과 "" 과 " " 모두 허용하지 않는다.
@Size
필드 크기가 min 과 max 사이여야 값을 저장할 수 있도록 유효성을 검사해준다.

 

 

 

SecurityConfig

package com.example.basic.Config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;


@Configuration
@EnableWebSecurity
public class SecurityConfig {

    //비밀번호 보안 작업
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //authenticate 인증 매니저 생성(인증과정 처리)
    @Bean
    AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        //권한범위(인증후 응답)
        http.authorizeHttpRequests((auth)->{
            //모든 이용자가 접근가능
            auth.requestMatchers("/").permitAll();
            auth.requestMatchers("/salad").permitAll();
            auth.requestMatchers("/mypage").hasRole("USER");
            auth.requestMatchers("/member/insert").permitAll();
            auth.requestMatchers("/member/list").hasRole("ADMIN");
            auth.requestMatchers("/member/detail").hasAnyRole("USER", "ADMIN");
            auth.requestMatchers("/member/insert").permitAll();
            auth.requestMatchers("/member/update").hasAnyRole("USER", "ADMIN");
            auth.requestMatchers("/member/delete").hasAnyRole("USER", "ADMIN");
            //auth.requestMatchers("/cart/**").permitAll();
            auth.requestMatchers("/cart/list").permitAll();
            auth.requestMatchers("/cart/insert/**").permitAll();
            auth.requestMatchers("/cart/update").hasAnyRole("USER", "ADMIN");
            auth.requestMatchers("/cart/delete").hasAnyRole("USER", "ADMIN");
            auth.requestMatchers("/order/insert/**").hasAnyRole("USER", "ADMIN");
            auth.requestMatchers("/order/list").hasAnyRole("USER", "ADMIN");
            auth.requestMatchers("/order/detail").hasAnyRole("USER", "ADMIN");
            auth.requestMatchers("/order/delete").hasAnyRole("USER", "ADMIN");
            auth.requestMatchers("/board/**").permitAll();
            auth.requestMatchers("/board/list").permitAll();
            auth.requestMatchers("/board/detail").permitAll();
            auth.requestMatchers("/board/insert").hasAnyRole("USER", "ADMIN");
            auth.requestMatchers("/board/update").hasAnyRole("USER", "ADMIN");
            auth.requestMatchers("/board/delete").hasAnyRole("USER", "ADMIN");
            auth.requestMatchers("/boardcmt/insert").permitAll();
            auth.requestMatchers("/boardcmt/update").permitAll();
            auth.requestMatchers("/boardcmt/delete").permitAll();
            auth.requestMatchers("/info/**").permitAll();
            auth.requestMatchers("/notice/list").permitAll();
            auth.requestMatchers("/notice/detail").permitAll();
            auth.requestMatchers("/notice/insert").hasRole("ADMIN");
            auth.requestMatchers("/notice/update").hasRole("ADMIN");
            auth.requestMatchers("/notice/delete").hasRole("ADMIN");
            auth.requestMatchers("/salad/list").permitAll();
            auth.requestMatchers("/product/list").permitAll();
            auth.requestMatchers("/product/detail").permitAll();
            auth.requestMatchers("/product/insert").permitAll();
            auth.requestMatchers("/product/update").hasRole("ADMIN");
            auth.requestMatchers("/product/delete").hasRole("ADMIN");
            auth.requestMatchers("/product/admin").hasRole("ADMIN");
            auth.requestMatchers("/review/list").permitAll();
            auth.requestMatchers("/review/detail").permitAll();
            auth.requestMatchers("/review/insert").hasAnyRole("USER", "ADMIN");
            auth.requestMatchers("/review/update").hasAnyRole("USER", "ADMIN");
            auth.requestMatchers("/review/delete").hasAnyRole("USER", "ADMIN");
            auth.requestMatchers("/reviewcmt/insert").permitAll();
            auth.requestMatchers("/reviewcmt/update").permitAll();
            auth.requestMatchers("/reviewcmt/delete").permitAll();
            auth.requestMatchers("/login","/logout","/find/email","/find/password").permitAll();
            auth.requestMatchers("/css/**", "/js/**", "/image/**", "/images/**").permitAll();
        });

        //로그인설정(기본 /login)
        //로그인 페이지는 모든 사용자가 접근 가능하고, 로그인 성공시 index 페이지로 이동
        http.formLogin(login-> login
                .loginPage("/login")
                .loginProcessingUrl("/login")
                .usernameParameter("memberEmail")
                .passwordParameter("memberPassword")
                //로그인 성공 시 index.html 로 이동
                .defaultSuccessUrl("/", true)
                //로그인 실패
                .failureUrl("/login?error=true")
                .permitAll()
        );

        //로그아웃설정(기본 /logout)
        http.logout(logout->logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/")
                //로그아웃 시 사용자 세션 삭제
                .invalidateHttpSession(true)
        );

        //사용자 인증처리 컴포넌트 서비스 등록
        //http.userDetailsService(loginService);
        //csrf 사용X
        http.csrf(AbstractHttpConfigurer::disable);

        return http.build();
    }
}

 

 

antMatchers()
페이지에 접근할 수 있는 권한을 설정한다.
loginPage
로그인 페이지
loginProcessingUrl
구현한 로그인 페이지
defaultSuccessUrl
로그인 성공 시 제공할 페이지
failureUrl
로그인 실패 시 제공할 페이지
csrf().disable()
사이트 간 요청 위조(Cross-Site Request Forgery) 공격 방지 기능 키기

 

 

LoginService - 로그인, 회원 이메일 조회 기능

package com.example.basic.Service;

import com.example.basic.DTO.MemberDTO;
import com.example.basic.Entity.MemberEntity;
import com.example.basic.Repository.LoginRepository;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@Service
@RequiredArgsConstructor
public class LoginService implements UserDetailsService {
    private final LoginRepository loginRepository;
    private final ModelMapper modelMapper;
    private final PasswordEncoder passwordEncoder;

    //UsernameNotFoundException-사용자가 존재하지 않으면 예외발생(오류)
    @Override
    public UserDetails loadUserByUsername(String memberEmail) throws UsernameNotFoundException {
        //회원 이메일이 존재하는지 조회
        Optional<MemberEntity> memberEntity = loginRepository.findByMemberEmail(memberEmail);

        //조회한 데이터가 있으면
        if (memberEntity.isPresent()) {
            return User.withUsername(memberEntity.get().getMemberEmail())
                    //passwordEncoder 비밀번호 보안작업
                    .password(passwordEncoder.encode(memberEntity.get().getMemberPassword()))
                    .roles(memberEntity.get().getRoleType().name())
                    .build();
        } else { //조회된 데이터가 없으면
            throw new UsernameNotFoundException("존재하지 않는 회원입니다.");
        }
    }


    //이메일 찾기
    public Map<String, String> findEmail(String memberPassword, String memberName) {
        //비밀번호와 이름이 존재하는지 조회
        Optional<MemberEntity> memberEntity = loginRepository.
                findByMemberPasswordAndMemberName(memberPassword, memberName);

        //조회한 데이터가 없으면
        if (memberEntity.isEmpty()) {
            //예외처리
            throw new UsernameNotFoundException("비밀번호 또는 이름이 일치하지 않습니다.");
        }

        Map<String, String> result = new HashMap<>();
        result.put("memberEmail", memberEntity.get().getMemberEmail());

        return result;
    }
}

 

 

DB에서 유저 정보를 가져오고, 그걸 UserDetails로 담아서 리턴한다.
User 클래스 (org.springframework.security.core.userdetails.User)의 빌더를 사용해서 username에 아이디, password에 비밀번호, roles에 권한(역할)을 넣어주면 UserDetails가 리턴된다.

 

 

 

MemberService - 회원가입, 수정, 삭제, 조회 기능

package com.example.basic.Service;

import com.example.basic.DTO.MemberDTO;
import com.example.basic.Entity.CartEntity;
import com.example.basic.Entity.MemberEntity;
import com.example.basic.Repository.CartRepository;
import com.example.basic.Repository.MemberRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
    private final CartRepository cartRepository;
    private final ModelMapper modelMapper;

    //삽입
    public void memberInsert(MemberDTO memberDTO) throws IllegalAccessException {
        //기존 사용하는 아이디를 조회
        Optional<MemberEntity> member = Optional.ofNullable(memberRepository
                .findByMemberEmail(memberDTO.getMemberEmail()));

        if (member.isEmpty()) {
            //읽어온 값이 없으면(기존 사용하는 아이디가 존재하지 않으면)
            MemberEntity memberEntity = modelMapper.map(memberDTO,
                    MemberEntity.class);
            //저장
            memberRepository.save(memberEntity);
        } else {
            throw new IllegalAccessException("이미 가입된 회원입니다.");
        }
    }

    //수정
    public void memberUpdate(MemberDTO memberDTO) {
        MemberEntity memberEntity = modelMapper.map(memberDTO,
                MemberEntity.class);

        memberRepository.save(memberEntity);
    }

    //삭제
    public void memberDelete(Integer memberId) {

        //회원의 장바구니 찾기
        CartEntity cart = cartRepository.findByMemberId(memberId);
        //장바구니 삭제
        cartRepository.deleteById(cart.getCartId());

        //회원 계정 삭제
        memberRepository.deleteById(memberId);
    }

    //전체 조회
    public Page<MemberDTO> memberList(Pageable pageable) {
        int cutPage = pageable.getPageNumber() - 1;
        int pageCnt = 10;

        Pageable page = PageRequest.of(cutPage, pageCnt,
                Sort.by(Sort.Direction.DESC, "memberId"));

        Page<MemberEntity> memberEntities = memberRepository.findAll(page);

        Page<MemberDTO> memberDTOS = memberEntities.map(data ->
                modelMapper.map(data, MemberDTO.class));

        return memberDTOS;
    }

    //개별 조회
    public MemberDTO memberDetail(Integer memberId) {
        MemberEntity memberEntity = memberRepository.findById(memberId).orElseThrow();

        MemberDTO memberDTO = modelMapper.map(memberEntity, MemberDTO.class);

        return memberDTO;
    }

    //마이페이지 조회
    public MemberDTO detail(String memberEmail) {

        MemberEntity member = memberRepository.findByMemberEmail(memberEmail);
        MemberDTO memberDTO = modelMapper.map(member, MemberDTO.class);

        return memberDTO;
    }
}

 

 

@RequiredArgsConstructor
생성자 자동 생성 및 final 변수를 의존관계를 자동으로 설정해 준다.

 

 

 

LoginController

/*
    설명 : login 페이지 영역
    입력값 : /login
    출력값 : login/form
    작성일 : 24.03.05
    작성자 : 정아름
    수정사항 : 아이디 찾기 & 비밀번호 찾기 - 이메일 전송 방식으로 수정.
              
 */

package com.example.basic.Controller;

import com.example.basic.DTO.MemberDTO;
import com.example.basic.Service.LoginService;
import com.example.basic.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.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@Controller
@RequiredArgsConstructor
public class LoginController {
    private final LoginService loginService;

    //로그인 페이지
    @GetMapping("/login")
    public String loginForm() {

        return "login/form";
    }

    //아이디 찾기
    @GetMapping("/find/email")
    public String email() {

        return "login/email";
    }

    @PostMapping("/find/email")
    public String emailProc(Model model, String memberName, String memberPassword,
                            BindingResult bindingResult) {

        if (bindingResult.hasErrors()) {
            return "redirect:/find/email";
        }
        //회원 비밀번호, 이름 조회
        MemberDTO memberDTO = (MemberDTO) loginService.findEmail(memberPassword, memberName);
        if (memberDTO == null) {
            bindingResult.reject("Not found", "비밀번호 또는 이름이 일치하지 않습니다.");
            //이메일 찾기 페이지로 이동
            return "redirect:/find/email";
        }
        model.addAttribute("data", memberDTO);
        //조회하는 값이 있으면 이메일 확인 페이지로 이동
        return "login/resultemail";
    }
}

 

 

 

MemberController

/*
    설명 : 회원관리의 목록, 수정, 삭제, 조회로 이동하는 페이지 영역
    입력값 : /member/list, /member/insert, /member/update, /member/delete, /member/detail
    출력값 : member/list, member/insert, member/update, member/detail
    작성일 : 24.02.22
    작성자 : 정아름
    수정사항 : 회원관리의 전체 목록 페이지는 page 처리 하기로 함
 */

package com.example.basic.Controller;

import com.example.basic.Constant.RoleType;
import com.example.basic.DTO.BoardDTO;
import com.example.basic.DTO.MemberDTO;
import com.example.basic.DTO.ReviewDTO;
import com.example.basic.Service.*;
import com.example.basic.Util.PaginationUtil;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;
import java.util.Map;

@Controller
@RequiredArgsConstructor
public class MemberController {
    //주입
    private final MemberService memberService;

    private final BoardService boardService;

    private final ReviewService reviewService;

    //전체 조회
    @GetMapping("/member/list")
    public String listForm(@PageableDefault(page = 1) Pageable pageable,
                           Model model) {
        //페이지처리
        Page<MemberDTO> memberDTOS = memberService.memberList(pageable);

        Map<String, Integer> page = PaginationUtil.Pagination(memberDTOS);

        model.addAllAttributes(page);
        model.addAttribute("list", memberDTOS);

        return "member/list";
    }

    //삽입
    @GetMapping("/member/insert")
    public String insertForm() {

        return "member/insert";
    }

    @PostMapping("/member/insert")
    public String insertProc(@Valid MemberDTO memberDTO, BindingResult bindingResult) {
        //회원 검증
        if (bindingResult.hasErrors()) {
            //오류가 있으면 회원 가입페이지로 이동
            return "redirect:/member/insert";
        }

        try {
            memberDTO.setRoleType(RoleType.USER);

            memberService.memberInsert(memberDTO);
        } catch (Exception e) {
            return "redirect:/member/insert";
        }

        return "redirect:/login";
    }

    //수정
    @GetMapping("/member/update")
    public String updateForm(Integer id, Model model) {
        MemberDTO memberDTO = memberService.memberDetail(id);

        model.addAttribute("data", memberDTO);

        return "member/update";
    }

    @PostMapping("/member/update")
    public String updateProc(MemberDTO memberDTO) {
        memberService.memberUpdate(memberDTO);

        return "redirect:/";
    }

    //삭제
    @GetMapping("/member/delete")
    public String deleteProc(Integer id) {
        memberService.memberDelete(id);

        return "redirect:/";
    }

    //개별 조회
    @GetMapping("/member/detail")
    public String readProc(Integer id, Model model) {
        MemberDTO memberDTO = memberService.memberDetail(id);

        model.addAttribute("data", memberDTO);

        return "member/detail";
    }

    //마이페이지
    @GetMapping("/mypage")
    public String myPageForm(@AuthenticationPrincipal User user,
                             Model model) {

        //보안 인증 된 유저의 이메일로 회원 정보 찾기
        String memberEmail = user.getUsername();
        MemberDTO memberDTO = memberService.detail(memberEmail);

        List<BoardDTO> boardDTO = boardService.memberBoard(memberDTO.getMemberId());

        List<ReviewDTO> reviewDTO = reviewService.memberReview(memberDTO.getMemberId());

        model.addAttribute("board", boardDTO);
        model.addAttribute("review", reviewDTO);
        model.addAttribute("data", memberDTO);

        return "member/mypage";
    }
}

 

 

@RequiredArgsConstructor
생성자 자동 생성 및 final 변수를 의존관계를 자동으로 설정해 준다.
@GetMapping
@RequestMapping(Method=RequestMethod.GET)과 같다.
@PostMapping
@RequestMapping(Method=RequestMethod.POST)과 같다.
@AuthenticationPrincipal
세션 정보 UserDetails에 접근할 수 있는 정보

 

 

 

추가사항


회원 아이디나 비번을 찾을 때 바로 알려주는 로직으로 짰다가 security 까지 쓰는데 좀 그래서...

방법을 찾던 중, 

임시 비밀 번호를 생성하여  javamailsender로 메일 전송해주는 로직으로 변경하였다.

 

 

MailService

package com.example.basic.Service;

import com.example.basic.DTO.EmailDTO;
import com.example.basic.Entity.MemberEntity;
import com.example.basic.Repository.LoginRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.thymeleaf.spring6.SpringTemplateEngine;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@Service
@RequiredArgsConstructor
@Slf4j
public class MailService {
    private final LoginRepository loginRepository;
    private final JavaMailSender javaMailSender;

    //임시 비밀번호 생성
    public String createRandomPw() {
        String[] strings = new String[]{
                "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
                "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N",
                "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
                "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n",
                "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
        };

        //임시 비밀번호 변수
        String password = "";

        //랜덤으로 값을 6개를 뽑아 조합
        for (int i = 0; i < 6; i++) {
            int random = (int) (strings.length * Math.random());
            password += strings[random];
        }
        return password;
    }

    //html 메일 전송
    public void sendEmail(EmailDTO emailDTO) {
        //단순 문자 메일 보낼 수 있는 객체
        SimpleMailMessage message = new SimpleMailMessage();

        try {
            //메일 제목
            message.setSubject(emailDTO.getTitle());
            //수신자 메일 주소
            message.setText(emailDTO.getEmail());
            //메일 내용
            message.setText(emailDTO.getContent());

            javaMailSender.send(message);
            log.info("SUCCESS");

        } catch (Exception e) {
            log.info("FAIL");
            throw new RuntimeException(e);
        }
    }
    
    //비밀번호 찾기
    public Map<String, String> findPassword(String memberEmail, String memberName) {
        //이메일과 이름이 존재하는지 조회
        Optional<MemberEntity> memberEntity = loginRepository.
                findByMemberEmailAndMemberName(memberEmail, memberName);

        //조회한 데이터가 없으면
        if (memberEntity.isEmpty()) {
            //예외처리
            throw new UsernameNotFoundException("이메일 또는 이름이 일치하지 않습니다.");
        }

        Map<String, String> result = new HashMap<>();
        result.put("memberPassword", memberEntity.get().getMemberPassword());

        return result;
    }

    public void memberCheck(String memberEmail) {
        Optional<MemberEntity> member = loginRepository.findByMemberEmail(memberEmail);

        if (member.isEmpty() && !member.get().getMemberEmail().equals(memberEmail)) {
            throw new UsernameNotFoundException("존재하지 않는 회원입니다.");
        }
    }
}

 

 

 

MailController

package com.example.basic.Controller;

import com.example.basic.DTO.EmailDTO;
import com.example.basic.DTO.MemberDTO;
import com.example.basic.Service.MailService;
import com.example.basic.Service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;


@Controller
@RequiredArgsConstructor
public class MailController {
    private final MailService mailService;
    private final MemberService memberService;
    
    //비밀번호 찾기
    @GetMapping("/find/password")
    public String findPassword() {
        return "login/password";
    }

    @PostMapping("/find/password")
    public String sendPassword(String memberEmail, String memberName) {

        //회원 이메일, 이름 조회
        MemberDTO memberDTO = (MemberDTO) mailService.findPassword(memberEmail, memberName);

        //임시 비밀번호 생성
        String password = mailService.createRandomPw();
        //임시 비밀번호로 변경
        memberDTO.setMemberPassword(password);
        memberService.memberUpdate(memberDTO);

        //회원 정보가 있으면
        if (memberDTO != null) {
            String content = "login/sendmail";
            EmailDTO emailDTO = new EmailDTO();

            //이메일 전송
            emailDTO.setTitle("임시 비밀번호 발송(Test)");
            emailDTO.setEmail(memberDTO.getMemberEmail());
            emailDTO.setContent("임시 비밀번호는 '" + password +
                    "' 입니다. 로그인 후 반드시 비밀번호를 변경하세요!");
            //emailDTO.setContent(content);

            mailService.sendEmail(emailDTO);
        }

        //조회하는 값이 있으면 비밀번호 확인 페이지로 이동
        return "login/resultpassword";
    }
}

 

 

 

View - 회원가입, 로그인 페이지만 올림. 프론트 보단 백 위주로 작성했기에 기타 설명은 생략하겠다.

 

member/insert

<!--
파일명 : member/insert
작성자 : 정아름
작성일 : 24.02.21
수정사항 : 글씨체 적용
-->
<!DOCTYPE html>
<html lang="ko"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/main}"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>SalPick</title>
    <!-- bootstrap -->
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
    <!-- google fonts -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Nanum+Pen+Script&display=swap" rel="stylesheet">
    <!-- google icon -->
    <link rel="stylesheet"
          href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0"/>
    <style>
        body {
            font-family: Verdana, sans-serif;
            font-size: 15px;
        }

        h2 {
            font-family: "Nanum Pen Script", cursive;
            font-weight: 400;
            font-size: 50px;
            font-style: normal;
        }
    </style>
</head>
<body>
<div layout:fragment="content">
    <div class="row">
        <!-- 여백 -->
        <div class="col-sm-3"></div>
        <!-- 회원 가입 페이지 -->
        <div class="col-sm-6">
            <div class="container mt-5 mb-5">
                <h2>회원 가입</h2>
                <form th:action="@{/member/insert}" method="post">
                    <div class="mb-3 mt-3">
                        <label for="memberEmail" class="form-label">이메일 : </label>
                        <input type="email" class="form-control" name="memberEmail"
                               id="memberEmail">
                        <button onclick="validateEmail()">확인</button>
                        <div id="result"></div>
                    </div>
                    <div class="mb-3">
                        <label for="memberPassword" class="form-label">비밀번호 : </label>
                        <input type="password" class="form-control" name="memberPassword"
                               id="memberPassword">
                    </div>
                    <div class="mb-3">
                        <label for="memberName" class="form-label">회원명 : </label>
                        <input type="text" class="form-control" name="memberName"
                               id="memberName">
                    </div>
                    <div class="mb-3">
                        <label for="memberPhone" class="form-label">전화번호 : </label>
                        <input type="text" class="form-control" name="memberPhone"
                               id="memberPhone">
                    </div>
                    <div class="mb-3">
                        <label for="memberAddress" class="form-label">주소 : </label>
                        <input type="text" class="form-control" name="memberAddress"
                               id="memberAddress">
                    </div>
                    <button type="submit" class="btn btn-outline-warning float-end">
                        회원 가입
                    </button>
                </form>
            </div>
        </div>
        <!-- 여백 -->
        <div class="col-sm-3"></div>
    </div>
</div>
<th:block layout:fragment="script">
    <script type="text/javascript">
        function emailCheck(memberEmail) {
            var memberEmail_regex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-z]{2,4}$/i;
            if (!memberEmail_regex.test(memberEmail)) {
                return false;
            } else {
                return true;
            }
        }

        function validateEmail() {
            var emailInput = document.getElementById('memberEmail');
            var resultDiv = document.getElementById('result');

            var email = emailInput.value;

            if (emailCheck(email)) {
                alert(resultDiv.innerHTML = '유효한 이메일 주소입니다.');
            } else {
                alert(resultDiv.innerHTML = '유효하지 않은 이메일 주소입니다.');
            }
        }
    </script>
</th:block>
</body>
</html>

 

view에서도 역시 이메일 형식을 확인할 수 있게 pattern 방식으로 유효성 검사를 하였다.

 

 

 

login/form

<!--
    파일명 : login/form
    작성자 : 정아름
    작성일 : 24.02.22
    수정사항 : 글씨체 적용함
              폼 생성 완료. 배경 및 이미지, 아이콘 등 확인! - 완료. 더 추가할 거 있나 확인바람.
-->
<!DOCTYPE html>
<html lang="ko"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
      layout:decorate="~{layouts/main}">
<head>
    <meta charset="UTF-8">
    <title>SalPick</title>
    <!-- bootstrap -->
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
    <!-- google fonts -->
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Sofia">
    <!-- google icon -->
    <link rel="stylesheet"
          href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0"/>
    <style>
        .wrapper {
            display: grid;
            place-items: center;
            min-height: 100dvh;
            font-family: Ubuntu, sans-serif;
            font-size: 15px;
            font-style: normal;
            background-color: ivory;
        }

        h2 {
            font-size: 30px;
            font-style: oblique;
            font-family: Sofia, sans-serif;
            font-weight: bold;
            text-shadow: 3px 3px 3px #ababab;
        }
    </style>
</head>
<body>
<div layout:fragment="content">
    <div class="row wrapper">
        <div class="col-4"></div>
        <div class="col-4">
            <div class="container p-2 my-2 bg-success text-white text-center">
                <h2>login</h2>
            </div>
            <div class="container p-5 my-5 border border-2 bg-white">
                <form th:action="@{/login}" method="post">
                    <div th:if="${param.error}">
                        <div class="alert alert-danger">
                            사용자 ID 또는 비밀번호를 확인해 주세요.
                        </div>
                    </div>
                    <div class="mb-3">
                        <label for="memberEmail"> Email</label>
                        <input type="email" class="form-control" id="memberEmail" name="memberEmail">
                    </div>
                    <div class="mb-3">
                        <label for="memberPassword"> Password </label>
                        <input type="password" class="form-control" id="memberPassword" name="memberPassword">
                    </div>
                    <div class="form-check mb-3">
                        <label class="form-check-label">
                            <input class="form-check-input" type="checkbox" name="remember"> 자동로그인
                        </label>
                    </div>
                    <br>
                    <button type="submit" class="btn btn-outline-success float-end">
                        로그인
                    </button>
                    <br>
                </form>
            </div>
            <div class="container">
                <ul class="nav justify-content-center">
                    <li class="nav-item">
                        <a class="nav-link" th:href="@{/member/insert}">회원 가입</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" th:href="@{/find/email}">아이디 찾기</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" th:href="@{/find/password}">비밀번호 찾기</a>
                    </li>
                </ul>
            </div>
        </div>
        <div class="col-4"></div>
    </div>
</div>
</body>
</html>

 

 

 

 

화면 캡쳐

메인화면(로그인 전 화면)

 

 

로그인 페이지

 

 

 

메인화면(관리자로 로그인 했을 경우에만 관리자 페이지가 보일 수 있게 권한 설정을 하였다.)

 

 

메인화면(로그인 후에는 로그아웃 버튼만 클릭할 수 있고, 회원 가입 및 로그인 버튼은 숨긴다.)

 

 

게시판(공지사항, 고객센터, 구매후기)

 

 

우리 팀 소개와 쇼핑몰 사이트 이름 선정한 이유를 소개하는 페이지이다.

 

 

 

이메일 찾기 페이지

 

 

 

비밀번호 찾기 페이지

 

 

 

마이페이지(로그인 한 회원만 이용할 수 있게 인증 처리를 하였다. 인증이 완료 된 회원의 정보를 수정할 수 있고, 내가 쓴 글의 내역을 확인 할 수 있다.)