[ 구현 배경 ]
비로그인 상태에서 비밀번호 재설정을 만들 때,
단순히 “이메일 입력 → 코드 발송”으로 끝내면
위험 포인트가 많다.
그래서 이번 목표는 아래 흐름을
안전하게 만드는 것이다.
- 사용자가 아이디 입력
- “아이디 확인” 클릭
→ 서버에서 계정 존재 여부 확인 + 이메일 자동 세팅 - “인증번호 발송” 클릭
→ 서버가 이메일 발송 + 인증코드/만료시간(3분) 세션 저장 - 인증 성공 후에만 새 비밀번호 입력 가능
1. 최종 구조(역할 분리)
이번 구현은 “페이지 이동(동기)”과
“데이터 처리(JSON, 비동기)”를 분리한다.
동기 페이지 진입
- FindPasswordPageAction
→ findpassword.jsp 렌더링
비동기(AJAX) 3종
- FindPasswordMemberLookupServlet
: 아이디 존재 확인 + 이메일 조회 - FindPasswordSendCodeServlet
: 인증코드 발급 + 이메일 발송 + 세션 저장 - FindPasswordVerifyCodeServlet
: 인증코드 검증(2편에서)
최종 반영(동기)
- FindPasswordResetAction
: 검증 완료 상태에서만 비밀번호 UPDATE
2. 세션에 저장할 인증 상태 값
서버가 “진짜 상태”를 들고 있어야 한다
비밀번호 재설정은 클라이언트 값을 신뢰하면 안 된다.
그러므로 최소 아래 5개는 서버 세션에 저장한다.
- findPasswordMemberId : 대상 회원 PK
- findPasswordEmail : 발송 이메일(참고용)
- findPasswordCode : 발급 인증코드
- findPasswordExpireAt : 만료 시각(ms)
- findPasswordVerified : 인증 성공 여부(Boolean)
3. (비동기 1) 아이디 확인 + 이메일 자동세팅
FindPasswordMemberLookupServlet
핵심 포인트는 2가지이다.
- 이메일을 사용자가 입력하게 두지 않고
서버가 DB에서 가져와 세팅한다. - 응답은 JSON으로 내려줘서
JSP가 UI를 제어하게 한다.
@WebServlet("/FindPasswordMemberLookup")
public class FindPasswordMemberLookupServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=UTF-8");
Gson gson = new Gson();
Map<String, Object> result = new HashMap<>();
String memberName = request.getParameter("memberName");
if (memberName == null || memberName.trim().isEmpty()) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
result.put("success", false);
result.put("message", "아이디를 입력해주세요.");
response.getWriter().print(gson.toJson(result));
return;
}
memberName = memberName.trim();
MemberDAO dao = new MemberDAO();
MemberDTO dto = new MemberDTO();
dto.setCondition("MEMBER_ID_EMAIL");
dto.setMemberName(memberName);
MemberDTO data = dao.selectOne(dto);
result.put("success", true);
if (data == null) {
result.put("exists", false);
result.put("message", "존재하지 않는 아이디입니다.");
} else {
result.put("exists", true);
result.put("memberId", data.getMemberId());
result.put("memberEmail", data.getMemberEmail());
}
response.getWriter().print(gson.toJson(result));
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
doPost(request, response);
}
}
4. (비동기 2) 인증코드 발급 + 이메일 발송 + 세션 저장
FindPasswordSendCodeServlet
핵심 포인트는 아래 보안 요소이다.
→ 클라이언트가 준 email을 믿지 않는다
memberName으로 다시 DB 조회해서
서버가 이메일을 확정한다.
@WebServlet("/FindPasswordSendCode")
public class FindPasswordSendCodeServlet extends HttpServlet {
private static final int EXPIRE_SECONDS = 180; // 3분
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=UTF-8");
Gson gson = new Gson();
Map<String, Object> result = new HashMap<>();
String memberName = request.getParameter("memberName");
if (memberName == null || memberName.trim().isEmpty()) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
result.put("success", false);
result.put("message", "아이디를 입력해주세요.");
response.getWriter().print(gson.toJson(result));
return;
}
memberName = memberName.trim();
// 1) 서버가 DB에서 이메일 조회(신뢰)
MemberDAO dao = new MemberDAO();
MemberDTO dto = new MemberDTO();
dto.setCondition("MEMBER_ID_EMAIL");
dto.setMemberName(memberName);
MemberDTO data = dao.selectOne(dto);
if (data == null) {
result.put("success", false);
result.put("message", "존재하지 않는 아이디입니다.");
response.getWriter().print(gson.toJson(result));
return;
}
String email = data.getMemberEmail();
if (email == null || email.trim().isEmpty()) {
result.put("success", false);
result.put("message", "해당 계정의 이메일 정보가 없습니다.");
response.getWriter().print(gson.toJson(result));
return;
}
// 2) 코드 생성
String code = String.valueOf(100000 + new Random().nextInt(900000));
// 3) 이메일 발송
try {
EmailService emailService = new EmailService();
emailService.sendPasswordResetCode(email, code);
} catch (Exception e) {
e.printStackTrace();
result.put("success", false);
result.put("message", "이메일 발송에 실패했습니다.");
response.getWriter().print(gson.toJson(result));
return;
}
// 4) 세션 저장(검증/최종변경에 사용)
HttpSession session = request.getSession(true);
session.setAttribute("findPasswordMemberId", data.getMemberId());
session.setAttribute("findPasswordEmail", email);
session.setAttribute("findPasswordCode", code);
session.setAttribute("findPasswordExpireAt", System.currentTimeMillis() + (EXPIRE_SECONDS * 1000L));
session.setAttribute("findPasswordVerified", false);
result.put("success", true);
result.put("expireSeconds", EXPIRE_SECONDS);
response.getWriter().print(gson.toJson(result));
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
doPost(request, response);
}
}
5. 프론트(JSP)에서는 “버튼만 서버에 연결”하면 된다
1편에서는 UI 전체를 길게 설명하지 않고, 핵심만 적는다.
- “아이디 확인” 버튼
→ /FindPasswordMemberLookup - “인증번호 발송” 버튼
→ /FindPasswordSendCode
(인증코드 확인/최종 변경은 2편에서)
'개주 훈련일지 > 🏋️ 전집중 호흡 훈련' 카테고리의 다른 글
| 카카오페이 결제 승인 응답 전달 방식 개선(PRG) (0) | 2026.01.06 |
|---|---|
| 이메일 인증 완료 후 비밀번호 변경까지 - Ajax 3단계 + 최종 ResetAction 적용기 (0) | 2025.12.31 |
| 동기 검색/이동+비동기 페이지네이션 적용기 (0) | 2025.12.30 |
| 검색/이동은 동기, 데이터 로딩은 비동기 > 역할 분리 설계 (0) | 2025.12.28 |
| 프로필 이미지 변경 최종 확정 구현: temp→final 이동 + 캐시 차감 + 롤백 설계 (0) | 2025.12.25 |