SpringBoot 배달의민족-19 아이디, 비밀번호 찾기 (이메일 전송)

hanteng·2022년 7월 25일
0

SpringBoot 배달의민족

목록 보기
19/27

이번에는 아이디와 비밀번호 찾기 기능을 구현해보도록 하겠습니다

아이디찾기의 경우 사용자로부터 이메일주소를 입력받아 DB에서 이메일주소가 일치하는
아이디가 존재하는경우 해당 이메일로 아이디를 전송할것입니다

비밀번호 찾기의 경우 찾고자 하는 아이디를 입력받은후 이메일 또는 핸드폰으로
인증번호를 전송하여 일치하는경우 비밀번호를 변경하도록 구현할겁니다
이때 이메일 또는 핸드폰번호는 회원가입시 등록한 정보와 일치해야 합니다

핸드폰인증의 경우 저번과 마찬가지로 console창에 뜨도록 할것이므로 이메일인증을
위해 라이브러리를 추가해주도록 합시다

pom.xml 추가코드

		<!-- SMTP -->
		<dependency>
		  <groupId>org.springframework.boot</groupId>
		  <artifactId>spring-boot-starter-mail</artifactId>
		</dependency>

사용자한테 이메일로 인증번호를 전송하기 위해서는 발송용 이메일이 필요합니다
저는 gmail을 사용할것이며 이를 위해 gmail에 설정이 필요합니다
구글에 로그인한후 계정관리에 들어가서 보안탭을 클릭합니다


2단계 인증을 사용으로 바꾼후 앱비밀번호를 클릭합니다
앱 비밀번호를 생성할 앱 및 기기에서 기타를 선택한후 이름을 작성합니다

위와 같이 비밀번호가 생성되었다면 application.yml에 다음의 코드를 추가합니다
위 사진의 비밀번호를 넣으시면 안됩니다 자기 자신의 비밀번호를 입력하세요

application.yml 추가코드

  mail :
    host : smtp.gmail.com
    port : 587
    username : 지메일아이디
    password : 위에서 생성한 코드
    properties :
      mail :
        smtp :
          auth : true
          starttls :
            enable : true 

설정이 끝났다면 모든 레이어에 Find라는 새로운 이름의 클래스를 추가해줍니다

FindController.java 전체코드

@Controller
public class FindController {
	
	@Autowired
	UserService userService;
	
	// 아이디 찾기 페이지
	@GetMapping("/find/id")
	public String findId() {
		return "user/find/findId";
	}
	


아이디 찾기 버튼을 클릭시 해당화면으로 페이지가 이동됩니다
회원가입시 등록했던 이메일 주소를 입력한후 찾기를 누르면 Ajax통신을 통해
FindApi컨트롤러로 요청이 가게 됩니다

FindApiController.java 전체코드

@RestController
public class FindApiController {
	
	@Autowired
	FindService findService;
	@Autowired
	BCryptPasswordEncoder encodePwd;
	@Autowired
	AuthService authService;

	
	// 메일로 아이디 보내기
	@PostMapping("/api/find/sendUsernames")
	public ResponseEntity<Object> sendEmail(String email){
	    List<String> usernames =findService.findId(email);
	    if(usernames.size() != 0) {
	    	findService.sendUsernames(email, usernames);
	    	return new ResponseEntity<Object>(HttpStatus.OK);
	    }
	    
	    return new ResponseEntity<Object>(HttpStatus.BAD_REQUEST);
	}

Ajax통신을 통해 사용자가 작성한 이메일주소와 함께 요청이 오면
해당 이메일주소로 DB를 검색하게 됩니다. 이메일은 유니크값이 아니므로 같은 이메일을
가지는 여러 아이디가 존재할수 있기 때문에 List로 데이터를 받아야 합니다
이메일주소가 일치하는 아이디가 존재할경우 이메일전송 처리를 합니다

FindService.java 전체코드

@Service
public class FindService {
	
	@Autowired
	FindMapper findMapper;
	@Autowired
	JavaMailSender mailSender;

	
	public List<String> findId(String email) {
	    return findMapper.findId(email);
	}
	
	public void sendUsernames(String email, List<String> usernames) {
		SimpleMailMessage simpleMailMessage = new  SimpleMailMessage();
		simpleMailMessage.setTo(email);
		simpleMailMessage.setSubject("아이디 찾기");
		
		StringBuffer sb = new StringBuffer();
		sb.append("가입하신 아이디는");
		sb.append(System.lineSeparator());
		
		for(int i=0;i<usernames.size()-1;i++) {
			sb.append(usernames.get(i));
			sb.append(System.lineSeparator());
		}
		sb.append(usernames.get(usernames.size()-1)).append("입니다");
		
		simpleMailMessage.setText(sb.toString());
		
		
		new Thread(new Runnable() {
			public void run() {
				mailSender.send(simpleMailMessage);
			}
		}).start();
	}
	
	
}

SimpleMailMessage는 이메일을 쉽게 작성가능하도록 도와주는 스프링프레임워크가 제공하는
도구중 하나입니다. setTo는 받는사람의 이메일주소 , setSubject는 이메일제목을 뜻합니다
setText는 메일 내용으로 StringBuffer를 통해 텍스트와 유저아이디를 합친후 넣어줍니다
mailSender.send(simpleMailMessage)는 이메일을 발송하는 부분인데 몇초가량의 시간이
걸리므로 그 시간동안 다른 작업을 처리할수 있도록 새로운 쓰레드로 감싸주도록 합니다

FindMapper.java 전체코드

	//유저Id 찾기
	public List<String> findId(String email);

FindMapper.xml 전체코드

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.han.delivery.dao.FindMapper">
   
	<select id="findId" resultType="String">
	    SELECT	USERNAME
	    FROM	DL_USER
	    WHERE	EMAIL = #{email }
	</select>

</mapper>

아이디 찾기는 완료하였으니 이번에는 비밀번호 찾기를 구현하도록 하겠습니다
비밀번호의 경우 아이디보다 더 복잡합니다

아이디 찾기의 경우 이메일입력 -> 일치하는 이메일이 존재하는지 확인 -> 아이디 이메일전송
-> 아이디확인으로 간단하지만

비밀번호 찾기의 경우 아이디입력 -> 일치하는 아이디가 존재하는지 확인
-> 이메일or핸드폰번호 입력 -> 해당 아이디의 정보와 이메일or핸드폰번호 일치확인
-> 이메일or핸드폰번호로 인증번호 발송 -> 인증번호 확인 -> 비밀번호 변경
으로 처리해야할 부분이 많습니다 일단 비밀번호 찾기 화면을 보여주기 위한 코드를 추가합니다

FindController.java 추가코드

	// 비밀번호 찾기 페이지
	@GetMapping("/find/password")
	public String findPassword() {
	    return "user/find/findPassword";
	}

사용자가 아이디를 입력하고 다음버튼을 클릭시 DB에 해당 아이디가 존재하는지
확인해야 합니다. 또한 존재할경우 인증을 위한 페이지로 이동하여야 합니다

FindApiController.java 추가코드

	// 아이디가 존재하는지 확인
	@GetMapping("/api/find/overlapCheck")
	public ResponseEntity<String> overlapCheck(String username,  HttpSession session){

		System.out.println(username);
	    if(authService.usernameChk(username) != 0) {
		    Map<String, Object> authStatus = new HashMap<>();
		    authStatus.put("username", username);
		    authStatus.put("status", false);
		    
		    session.setMaxInactiveInterval(300);
		    session.setAttribute("authStatus", authStatus);
		    
	    	return ResponseEntity.ok().body(username);
	    }
	    
	    return ResponseEntity.badRequest().body("아이디가 존재하지 않습니다");
	}

처음 회원가입을 구현할때 아이디가 중복되는지 확인하기 위해 구현했던
usernameChk메서드를 사용합니다 아이디가 존재하면 1, 아니면 0이 리턴되고
존재할경우 세션에 username(유저아이디)를 저장하는데 그 이유는 아이디확인이후
아이디가 존재할경우에만 인증을 위한 페이지로 넘어가는데 해당주소인
/find/password/auth?username="admin"를 주소창에 입력하여 아이디확인을
하지 않고 바로 접근하는 경우를 막기 위해 세션에 아이디를 저장합니다
이제 컨트롤러에 인증번호 페이지를 추가하도록 합니다

FindController.java 추가코드

	// 인증번호 보내기 페이지
	@GetMapping("/find/password/auth")
	public String auth(String username, HttpSession session) {
	    Map<String, Object> authStatus = (Map<String, Object>) session.getAttribute("authStatus");
	    if(authStatus == null || !username.equals(authStatus.get("username"))) {
	        return "redirect:/find/password";
	    }
	    
	    return "user/find/auth";
	}

사용자가 이메일 또는 전화번호를 입력하고 인증번호받기 버튼을 클릭하면
DB에 저장된 찾고자 하는 아이디의 이메일 또는 전화번호와 일치하는지 확인해야 합니다

FindApiController.java 추가코드

	// username의 이메일이 맞는지 확인
	@GetMapping("/api/find/password/emailCheck")
	public ResponseEntity<Boolean> emailCheck(String username, String email){
	    boolean emailCheck = findService.emailCheck(username, email);
	    return new ResponseEntity<Boolean>(emailCheck, HttpStatus.OK);
	}
	 
	 
	// username의 전화번호가 맞는지 확인
	@GetMapping("/api/find/password/phoneCheck")
	public ResponseEntity<Boolean> phoneCheck(String username, String phone) {
	    boolean phoneCheck = findService.phoneCheck(username, phone);
	    return new ResponseEntity<Boolean>(phoneCheck,HttpStatus.OK);
	}

FindService.java 추가코드

	public boolean emailCheck(String username, String email) {
	    Map<String, Object> map = new HashMap<>();
	    map.put("username", username);
	    map.put("email", email);
	    String result = findMapper.emailCheck(map);
	    if("1".equals(result)) {
	        return true;
	    }
	    return false;
	}
	 
	 

	public boolean phoneCheck(String username, String phone) {
	    Map<String, Object> map = new HashMap<>();
	    map.put("username", username);
	    map.put("phone", phone);
	    System.out.println(map);
	    String result = findMapper.phoneCheck(map);
	    if("1".equals(result)) {
	        return true;
	    }
	    return false;
	}

FindMapper.java 추가코드

	//패스워드 찾기 이메일 일치 확인
	public String emailCheck(Map<String, Object> map);
	 
	//패스워드 찾기 폰번호 일치 확인
	public String phoneCheck(Map<String, Object> map);

FindMapper.xml 추가코드

	<select id="emailCheck" resultType="String">
	    SELECT	1 result 
	    FROM 	DUAL 
	    WHERE EXISTS(
	        SELECT	1 
	        FROM 	DL_USER 
	        WHERE 	USERNAME = #{username } 
	        AND 	EMAIL = #{email }
	    ) 
	</select>
	 
	 
	<select id="phoneCheck" resultType="String">
	    SELECT	1 result 
	    FROM 	DUAL 
	    WHERE EXISTS(
	        SELECT	1 
	        FROM 	DL_USER 
	        WHERE 	USERNAME = #{username } 
	        AND 	PHONE = #{phone }
	    )
	</select>

우리는 기존에 회원가입을 할때 중복된 아이디가 존재하는지 확인하기 위해 COUNT쿼리를
사용했었습니다. EXISTS는 COUNT쿼리와 비슷하지만 좀 더 빠른 성능을 가집니다
그 이유는 COUNT를 사용하여 중복체크를 하는 경우 처음부터 끝까지 모든 데이터를
조회해야합니다 하지만 EXISTS를 사용할 경우 데이터를 조회하다 해당 데이터가 존재할 경우
데이터를 끝까지 조회하지 않고 그 순간에 종료를 하므로 더 빠른 실행이 가능합니다

이제 이메일 또는 전화번호가 일치할경우 저번포스팅에서의 폰번호변경과 같이
타이머가 실행되고 인증번호가 생성되어 이메일로 전송되거나 console창에 표시됩니다
저번시간에 추가했었던 authNum메서드에서 주석처리했던 부분을 활성화해줍니다

사용자에게 이메일을 보낸후 세션에 시간과 인증번호를 저장합니다
이메일을 보내기 위한 코드를 추가해줍니다

FindService.java 추가코드

	public void sendAuthNum(String email, String authNum) {
	    SimpleMailMessage simpleMailMessage = new  SimpleMailMessage();
	    simpleMailMessage.setTo(email);
	    simpleMailMessage.setSubject("비밀번호 찾기 인증번호");
	    
	    String text = "인증번호는 " + authNum + "입니다";
	    
	    simpleMailMessage.setText(text);
	    new Thread(new Runnable() {
	        public void run() {
	            mailSender.send(simpleMailMessage);
	        }
	    }).start();
	    
	}

이제 인증번호를 입력하고 확인버튼을 누르면 저번 포스팅에서 추가한
authNumCheck메서드를 통해 세션에 저장된 인증번호와 사용자가 입력한 인증번호가
일치하는지를 확인합니다 일치할경우 비밀번호 변경을 위한 페이지로 이동시킵니다

FindApiController.java 추가코드

	// 인증 완료 후
	@PostMapping("/api/find/auth/completion")
	public ResponseEntity<String> authCompletion(HttpSession session) {
	    Map<String, Object> authStatus = (Map<String, Object>) session.getAttribute("authStatus");
	    if(authStatus == null) {
	        return new ResponseEntity<String>("인증시간이 만료되었습니다", HttpStatus.BAD_REQUEST);
	    }
	    authStatus.put("status", true);
	    return new ResponseEntity<String>(HttpStatus.OK);
	}
	

세션의 status를 true로 바꿔주는이유는 비밀번호 변경 페이지는 get요청이므로
username을 주소에 포함하여 화면을 요청하는데 아까 말한것처럼 인증단계를 거치지않고
주소입력을 통해 접근하는 경우를 막아주기 위해서입니다.

FindController.java 추가코드

	// 비밀번호 변경 페이지
	@GetMapping("/find/modify/password")
	public String moldifyPassword(String username, HttpSession session) {
	    Map<String, Object> authStatus = (Map<String, Object>) session.getAttribute("authStatus");
	    
	    if(authStatus == null || !username.equals(authStatus.get("username"))) {
	        return "redirect:/find/password";
	    }
	    
	    // 페이지에 왔을때 인증이 안되있다면
	    if(!(boolean) authStatus.get("status")) {
	        return "redirect:/find/password";
	    }
	    return "user/find/modify";
	}
	

이제 비밀번호를 입력할 경우 DB에 패스워드를 업데이트 해줘야 합니다
비밀번호 변경의 경우 Find가 아닌 User컨트롤러에 추가하도록 하겠습니다

UserApiController.java 추가코드

	// 비밀번호 변경
	@PatchMapping("/api/user/password")
	public ResponseEntity<String> modifyPassword(String password, String username, HttpSession session) {
	    password = encodePwd.encode(password);
	    userService.modifyInfo(username, "password", password);
	    session.setMaxInactiveInterval(0);
	    session.setAttribute("authStatus", null);
	    return new ResponseEntity<String>("비밀번호를 변경했습니다",HttpStatus.OK);
	}

profile
이메일 : ehfvndcjstk@naver.com

0개의 댓글