[Spring] Mail 보내기

2023. 12. 21. 20:44학부 강의/웹프로그래밍 (Spring)

0. 출처

 

아직 배우고 있는 중이라 부정확한 정보가 포함되어 있을 수 있습니다!
주의하세요!

 

올인원 스프링 프레임워크 참고.

 

올인원 스프링 프레임워크 : 네이버 도서

네이버 도서 상세정보를 제공합니다.

search.shopping.naver.com

 


1. 관리자 관련 기능

 

  • 관리자 계정 정보 수정
  • 새 비밀번호 생성 및 메일 발송
  • 신규 도서 등록 (+파일 업로드)
  • 도서 검색 및 상세 정보 출력
  • 도서 정보 수정과 삭제

 


2. 관리자 계정 정보 수정

 

가. 컨트롤러

@RequestMapping(value="/modifyAccountForm", method = RequestMethod.GET)
public String modifyAccountForm(HttpSession session) {
    System.out.println("[AdminMemberController] modifyAccountForm()");

    String nextPage = "admin/member/modify_account_form";

    AdminMemberVo loginedAdminMemberVo = (AdminMemberVo) session.getAttribute("loginedAdminMemberVo");
    if(loginedAdminMemberVo == null) nextPage = "redirect:/admin/member/loginForm";

    return nextPage;
}

/admin/member/modifyAccountForm 요청을 처리하는 메서드 modifyAccountForm() AdminMemberController에 추가.

 

관리자가 계정수정 링크를 누르면 정보를 수정할 수 있는 페이지로 이동한다.

 

이때 로그인하지 않으면 로그인부터 하도록 한다.

 

@RequestMapping(value="/modifyAccountConfirm", method = RequestMethod.POST)
public String modifyAccountConfirm(AdminMemberVo adminMemberVo, HttpSession session) {
    System.out.println("[AdminMemberController] modifyAccountConfirm()");

    String nextPage = "admin/member/modify_account_ok";

    int result = adminMemberService.modifyAccountConfirm(adminMemberVo);

    if (result > 0) {
        AdminMemberVo loginedAdminMemberVo = adminMemberService.getLoginedAdminMemberVo(adminMemberVo.getA_m_no());
        session.setAttribute("loginedAdminMemberVo", loginedAdminMemberVo);
        session.setMaxInactiveInterval(60 * 30);
    }else {
        nextPage = "admin/member/modify_account_ng";
    }

    return nextPage;
}

/admin/member/modifyAccountConfirm 요청을 처리하는 메서드를 AdminMemberController에 추가.

 

계정수정에 성공하면 세션에 저장된 loginedAdminMemberVo를 업데이트한다.

 


나. 서비스

public int modifyAccountConfirm(AdminMemberVo adminMemberVo) {
    System.out.println("[AdminMemberService] modifyAccountConfirm()");
    return adminMemberDao.updateAdminAccount(adminMemberVo);
}

public AdminMemberVo getLoginedAdminMemberVo(int a_m_no) {
    System.out.println("[AdminMemberService] getLoginedAdminMemberVo()");
    return adminMemberDao.selectAdmin(a_m_no);
}
  • getLoginedAdminMemberVo() : 세션 정보를 갱신하기 위해서 필요한 수정된 관리자 정보를 불러오기 위해서 필요함.

 


다. DAO

public int updateAdminAccount(AdminMemberVo adminMemberVo) {
    System.out.println("[AdminMemberDao] updateAdminAccount()");

    String sql = "UPDATE tbl_admin_member SET " + "a_m_name = ?, " + "a_m_gender = ?, " 
            + "a_m_part = ?, " + "a_m_position = ?, " 
            + "a_m_mail = ?, " + "a_m_phone = ?, " 
            + "a_m_mod_date = NOW() " + "WHERE a_m_no = ?";

    int result = -1;

    try {
        result = jdbcTemplate.update(sql, adminMemberVo.getA_m_name(), adminMemberVo.getA_m_gender(), 
                adminMemberVo.getA_m_part(), adminMemberVo.getA_m_position(), adminMemberVo.getA_m_mail(), 
                adminMemberVo.getA_m_phone(), adminMemberVo.getA_m_no());

    }catch (Exception e) {
        e.printStackTrace();
    }

    return result;
}

public AdminMemberVo selectAdmin(int a_m_no) {
    System.out.println("[AdminMemberDao] selectAdmin()");

    String sql = "SELECT * FROM tbl_admin_member " + "WHERE a_m_no = ?";

    List<AdminMemberVo> adminMemberVos = new ArrayList<AdminMemberVo>();

    try {
        adminMemberVos = jdbcTemplate.query(sql, new RowMapper<AdminMemberVo>() {
            @Override
            public AdminMemberVo mapRow(ResultSet rs, int rowNum) throws SQLException{
                AdminMemberVo adminMemberVo = new AdminMemberVo();

                adminMemberVo.setA_m_no(rs.getInt("a_m_no"));
                adminMemberVo.setA_m_approval(rs.getInt("a_m_approval"));
                adminMemberVo.setA_m_id(rs.getString("a_m_id"));
                adminMemberVo.setA_m_pw(rs.getString("a_m_pw"));
                adminMemberVo.setA_m_name(rs.getString("a_m_name"));
                adminMemberVo.setA_m_gender(rs.getString("a_m_gender"));
                adminMemberVo.setA_m_part(rs.getString("a_m_part"));
                adminMemberVo.setA_m_position(rs.getString("a_m_position"));
                adminMemberVo.setA_m_mail(rs.getString("a_m_mail"));
                adminMemberVo.setA_m_phone(rs.getString("a_m_phone"));
                adminMemberVo.setA_m_reg_date(rs.getString("a_m_reg_date"));
                adminMemberVo.setA_m_mod_date(rs.getString("a_m_mod_date"));

                return adminMemberVo;
            }
        }, a_m_no);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return adminMemberVos.size() > 0 ? adminMemberVos.get(0) : null;
}

계정 정보를 변경하면 데이터베이스의 데이터가 변경된다.

 

 


3. 관리자 새 비밀번호 생성

 

관리자 계정의 비밀번호를 잊어버렸을 때 메일로 새로운 비밀번호를 발급받는 기능을 구현해 보자.

 

 

 


가. JavaMailSenderImpl

 

  • JavaMailSenderImpl : 스프링에서 메일을 보내기 위해서 사용됨.
 

JavaMailSenderImpl (Spring Framework 6.1.2 API)

Obtain and connect a Transport from the underlying JavaMail Session, passing in the specified host, port, username, and password.

docs.spring.io

 

<!--  SEND MAIL -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>${org.springframework-version}</version>
</dependency>

<dependency>
    <groupId>com.sun.mail</groupId>
    <artifactId>javax.mail</artifactId>
    <version>1.6.2</version>
</dependency>

pom.xmldependency를 추가한다.

 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">

    <!-- JavaMailSenderImpl 객체 -->
    <bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
        <property name="host" value="smtp.[메일 서비스 제공자].com" />
        <property name="port" value="587" />
        <property name="username" value="[사용할 메일]@[주소를 입력]" />
        <property name="password" value="[발급받은 애플리케이션 비밀번호]" />
        <property name="javaMailProperties">
            <props>
                <prop key="mail.smtp.auth">true</prop>
                <prop key="mail.smtp.starttls.enable">true</prop>
            </props>
        </property>
    </bean>
</beans>

BookRentalPjt/src/main/webapp/WEB-INF/spring/mail-context.xml를 추가한다.

 

  • <property name="port" value="587" /> : 587 포트 사용.

  • <property name="host" value="smtp.[메일 서비스 제공자].com" />
    : smtp 프로토콜을 사용한다는 것을 알 수 있다.
    : 메일 서비스 제공자(ex. google, naver, …)

  • <property name="password" value="[발급받은 애플리케이션 비밀번호]" />
    : 애플리케이션 비밀번호를 발급받아서 입력해야 한다.

 

 

애플리케이션 비밀번호 사용 방법 (아웃룩 등 2단계 인증 미지원 환경에서 로그인) : 회원정보 고

아웃룩, 휴대전화 기본 메일앱, 캘린더앱 등 일부 애플리케이션은 2단계 인증을 지원하지 않습니다.​미지원 환경에서는 먼저 비밀번호를 생성한 후 애플리케이션 화면에서 생성된 비밀번호를

help.naver.com

 

앱 비밀번호로 로그인 - Google 계정 고객센터

도움말: 앱 비밀번호는 권장되지 않으며 대부분의 경우 필요하지 않습니다. 계정을 안전하게 보호하려면 'Google 계정으로 로그인'을 사용하여 앱을 Google 계정에 연결하세요. 앱 비밀번호란 보안

support.google.com

 

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
        /WEB-INF/spring/root-context.xml
        /WEB-INF/spring/jdbc-context.xml
        /WEB-INF/spring/security-context.xml
        /WEB-INF/spring/mail-context.xml
    </param-value>
</context-param>

web.xml 수정.

 


나. 새 비밀번호 생성

1) 컨트롤러

@RequestMapping(value="/findPasswordForm", method = RequestMethod.GET)
public String findPasswordForm() {
    System.out.println("[AdminMemberController] findPasswordForm()");

    String nextPage = "admin/member/find_password_form";

    return nextPage;
}

비밀번호 찾기 페이지 이동.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">

<jsp:include page="../../include/title.jsp" />

<link href="<c:url value='/resources/css/admin/find_password_form.css' />" rel="stylesheet" type="text/css">

<jsp:include page="../include/find_password_js.jsp" />

</head>
<body>

    <jsp:include page="../../include/header.jsp" />

    <jsp:include page="../include/nav.jsp" />

    <section>

        <div id="section_wrap">

            <div class="word">

                <h3>FIND PASSWORD FORM</h3>
                <p>(We will send you a new password.)</p>

            </div>

            <div class="find_password_form">

                <form action="<c:url value='/admin/member/findPasswordConfirm' />" name="find_password_form" method="post">

                    <input type="text" name="a_m_id" placeholder="INPUT ADMIN ID."> <br>
                    <input type="text" name="a_m_name" placeholder="INPUT ADMIN NAME."> <br>
                    <input type="text" name="a_m_mail" placeholder="INPUT ADMIN MAIL."> <br>
                    <input type="button" value="find password" onclick="findPassword();"> 
                    <input type="reset" value="reset">

                </form>

            </div>

            <div class="create_account_login">

                <a href="<c:url value='/admin/member/createAccountForm' />">create account</a>
                <a href="<c:url value='/admin/member/loginForm' />">login</a>

            </div>

        </div>

    </section>

    <jsp:include page="../../include/footer.jsp" />

</body>
</html>

admin/member/find_password_form.jsp

(출판사에서 제공하는 실습 코드에서 jsp 파일을 가져온다.)

 

dmin/member/find_password_form.jsp

 

@RequestMapping(value="/findPasswordConfirm", method = RequestMethod.POST)
public String findPasswordConfirm(AdminMemberVo adminMemberVo) {
    System.out.println("[AdminMemberController] findPasswordConfirm()");

    String nextPage = "admin/member/find_password_ok";

    int result = adminMemberService.findPasswordConfirm(adminMemberVo);

    if(result <= 0)
        nextPage = "admin/member/find_password_ng";

    return nextPage;
}

findPasswordConfirm() 추가.

 


2) 서비스

public int findPasswordConfirm(AdminMemberVo adminMemberVo) {
    System.out.println("[AdminMemberService] findPasswordConfirm()");

    AdminMemberVo selectedAdminMemberVo = adminMemberDao.selectAdmin(adminMemberVo.getA_m_id(), adminMemberVo.getA_m_name(), adminMemberVo.getA_m_mail());

    int result = 0;

    if(selectedAdminMemberVo != null) {
        String newPassword = createNewPassword();
        result = adminMemberDao.updatePassword(adminMemberVo.getA_m_id(), newPassword);
        if(result > 0) sendNewPasswordByMail(adminMemberVo.getA_m_mail(), newPassword);
    }


    return result;
}
  • AdminMemberVo selectedAdminMemberVo = adminMemberDao.selectAdmin(adminMemberVo.getA_m_id(), adminMemberVo.getA_m_name(), adminMemberVo.getA_m_mail());
    : 전달받은 id, name, mail을 바탕으로 계정이 존재하는지 우선 확인한다.

  • String newPassword = createNewPassword(); : 새로운 비밀번호를 생성한다.

  • result = adminMemberDao.updatePassword(adminMemberVo.getA_m_id(), newPassword);
    : 비밀번호를 업데이트한다.

  • if(result > 0) sendNewPasswordByMail(adminMemberVo.getA_m_mail(), newPassword);
    : 정상적으로 비밀번호를 업데이트했다면 변경된 비밀번호를 메일로 전송한다.

 

@Autowired
JavaMailSenderImpl javaMailSenderImpl;

private String createNewPassword() {
    System.out.println("[AdminMemberService] createNewPassword()");

    char[] chars = new char[] {
            '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'
    };

    StringBuffer stringBuffer = new StringBuffer();
    SecureRandom secureRandom = new SecureRandom();
    secureRandom.setSeed(new Date().getTime());

    int index = 0, length = chars.length;
    for(int i =0; i < 8; i++) {
        index = secureRandom.nextInt(length);

        if(index % 2 == 0)
            stringBuffer.append(String.valueOf(chars[index]).toUpperCase());
        else
            stringBuffer.append(String.valueOf(chars[index]).toLowerCase());
    }

    System.out.println("[AdminMemberService] NEW PASSWORD: " + stringBuffer.toString());

    return stringBuffer.toString();
}

...

SecureRandomRandom보다 더 복잡한 난수를 생성한다.

 

  • String이 아닌 굳이 StringBuffer을 사용한 이유
    • 효율성 : 자바에서 String 객체는 불변(immutable)이다. 즉, 문자열에 변경이 생길 때마다 새로운 String 객체가 생성되고 기존 객체는 가비지 컬렉터에 의해 제거된다. 이는 메모리 사용량이 많고 성능 저하를 일으킬 수 있다. 반면, StringBuffer는 가변(mutable)이어서 기존 객체에 문자열을 추가하거나 변경할 수 있다. 따라서 반복적인 문자열 조작이 필요한 경우 StringBuffer를 사용하는 것이 더 효율적이다. (출처 : ChatGPT)
    • 스레드 안전성 : StringBuffer는 스레드 안전(thread-safe)하다는 특징을 가지고 있다. 즉, 멀티스레드 환경에서 동시에 접근해도 데이터 무결성이 유지된다. 각 메서드가 synchronized 키워드로 동기화되어 있기 때문이다. 이는 여러 스레드가 동시에 해당 객체를 수정할 때 발생할 수 있는 문제를 방지한다. (출처 : ChatGPT)

  • secureRandom.setSeed(new Date().getTime());
    : 난수를 생성할 때 시간을 참고한다.
    : 하지만! 높은 보안성을 위해서 따로 시드를 설정하지 않는 것을 추천한다.
    : 특별한 경우가 아니라면 SecureRandom의 자동 시드 생성 메커니즘을 그대로 사용하는 것이 좋다.
    : SecureRandom 클래스는 기본적으로 시스템에서 제공하는 엔트로피 소스(예: 시스템 시간, 하드웨어 랜덤성 소스 등)를 사용하여 난수 생성의 기본 시드를 자동으로 생성한다. 이 방식은 매우 높은 엔트로피를 제공하므로, 생성된 난수의 예측이 어렵게 만들어 보안을 강화한다.

  • index = secureRandom.nextInt(length);
    : 무작위로 인덱스를 선택합니다.

  • if(index % 2 == 0) stringBuffer.append(String.valueOf(chars[index]).toUpperCase()); else stringBuffer.append(String.valueOf(chars[index]).toLowerCase());
    : 대소문자를 섞어서 비밀번호를 생성한다.

 

private void sendNewPasswordByMail(String toMailAddr, String newPassword) {
    System.out.println("[AdminMemberService] sendNewPasswordByMail()");

    final MimeMessagePreparator mimeMessagePreparator = new MimeMessagePreparator() {
        @Override
        public void prepare(MimeMessage mimeMessage) throws Exception{
            final MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true, "UTF-8");

            mimeMessageHelper.setTo(toMailAddr);
            mimeMessageHelper.setSubject("[한국도서관] 새 비밀번호 안내입니다.");
            mimeMessageHelper.setText("새 비밀번호 : " + newPassword, true);
        }
    };

    javaMailSenderImpl.send(mimeMessagePreparator);
}

변경된 새 비밀번호를 전달받은 이메일 주소로 보낸다.

 

MimeMessagePreparator.prepare() 동작 중에 MimeMessageHelper로 구체적인 MimeMessage를 작성한다.

 

JavaMailSenderImpl이 준비된 MimeMessagePreparator를 보낸다.

 

아래는 보다 다양한 MimeMessageHelper 기능이다.

 

MimeMessageHelper (Spring Framework 6.1.2 API)

setText Set the given text directly as content in non-multipart mode or as default body part in multipart mode. The "html" flag determines the content type to apply. NOTE: Invoke addInline(java.lang.String, jakarta.activation.DataSource) after setText; els

docs.spring.io

 


3) DAO

public AdminMemberVo selectAdmin(String a_m_id, String a_m_name, String a_m_mail) {
    System.out.println("[AdminMemberDao] selectAdmin()");
    String sql = "SELECT * FROM tbl_admin_member " + "WHERE a_m_id = ? AND a_m_name = ? AND a_m_mail = ?";

    List<AdminMemberVo> adminMemberVos = new ArrayList<AdminMemberVo>();

    try {
        adminMemberVos = jdbcTemplate.query(sql, new RowMapper<AdminMemberVo>() {
            @Override
            public AdminMemberVo mapRow(ResultSet rs, int rowNum) throws SQLException{
                AdminMemberVo adminMemberVo = new AdminMemberVo();

                adminMemberVo.setA_m_no(rs.getInt("a_m_no"));
                adminMemberVo.setA_m_approval(rs.getInt("a_m_approval"));
                adminMemberVo.setA_m_id(rs.getString("a_m_id"));
                adminMemberVo.setA_m_pw(rs.getString("a_m_pw"));
                adminMemberVo.setA_m_name(rs.getString("a_m_name"));
                adminMemberVo.setA_m_gender(rs.getString("a_m_gender"));
                adminMemberVo.setA_m_part(rs.getString("a_m_part"));
                adminMemberVo.setA_m_position(rs.getString("a_m_position"));
                adminMemberVo.setA_m_mail(rs.getString("a_m_mail"));
                adminMemberVo.setA_m_phone(rs.getString("a_m_phone"));
                adminMemberVo.setA_m_reg_date(rs.getString("a_m_reg_date"));
                adminMemberVo.setA_m_mod_date(rs.getString("a_m_mod_date"));

                return adminMemberVo;
            }
        }, a_m_id, a_m_name, a_m_mail);
    } catch (Exception e) {
        e.printStackTrace();
    }


    return adminMemberVos.size() > 0 ? adminMemberVos.get(0) : null;
}

관리자의 비밀번호를 재설정 요청에 대하여 올바른 접근인지 확인한다.

 

public int updatePassword(String a_m_id, String newPassword) {
    System.out.println("[AdminMemberDao] updatePassword()");

    String sql = "UPDATE tbl_admin_member SET " + "a_m_pw = ?, " + "a_m_date = NOW() " 
            + "WHERE a_m_id = ?";

    int result = -1;

    try {
        result = jdbcTemplate.update(sql, passwordEncoder.encode(newPassword), a_m_id);

    }catch (Exception e) {
        e.printStackTrace();
    }

    return result;
}

비밀번호를 업데이트한다.

 

당연히 암호화하여 저장한다.

 

 


 

'학부 강의 > 웹프로그래밍 (Spring)' 카테고리의 다른 글

[Spring] 도서 수정, 삭제  (0) 2024.01.01
[Spring] 도서 등록, 검색  (0) 2023.12.31
[Spring] 관리자 로그인  (2) 2023.12.20
[Spring] 데이터베이스  (0) 2023.11.09
[Spring] 전자 도서관 서비스 1  (0) 2023.11.08