[Spring] Mail 보내기

0. 출처

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

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


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;
}
Code language: JavaScript (javascript)

/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;
}
Code language: JavaScript (javascript)

/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);
}
Code language: PHP (php)
  • 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;
}
Code language: PHP (php)

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


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

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


가. JavaMailSenderImpl

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

JavaMailSenderImpl (Spring Framework 7.0.2 API)

<!--  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>
Code language: HTML, XML (xml)

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>
Code language: HTML, XML (xml)

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단계 인증을 지원하지 않습니다.미지원 환경에서는 먼저 비밀번호를 생성한 후 애플리케이션 화면에서 생성된 비밀번호를 입력하세요.​​1. 애플리케이션 비밀번호가 필요한 서비스- IMAP·POP3·SMTP 방식의 메일 애플리케이션- CalDav 방식의 캘린더- 라인 및 네이버 계정을 연동한 게임​​2. 애플리케이션 비밀번호 생성 방법2단계 인증을 지원하지 않는 애플리케이션에서 미리 만들어 둔 전용 비밀번호를 입력해 로그인할 수 있도록 관리 기능을 제공합니다.​1) 메뉴 위치: – 개인 회원: 네이버ID &gt; 보안설정 &gt; 기본보안설정 &gt; 2단계 인증 &gt; 관리 (바로가기) – 단체 회원: 네이버ID(단체정보) &gt; 보안설정 &gt; 2단계 인증 &gt; 관리하기 (바로가기)​2) 네이버 로그인 비밀번호 재확인 후 ‘애플리케이션 비밀번호 관리’ 기능 확인 ※ 단체 회원은 관리자 비밀번호로 입력해 주세요.​3) 사용하려는 애플리케이션 종류 선택 또는 직접 입력 후 생성하기 버튼 클릭 ※ 단체 회원은 하나의 아이디에 하나의 애플리케이션 비밀번호만 생성할 수 있습니다.​4) 생성되는 비밀번호를 확인 또는 복사하여 애플리케이션의 로그인 화면 비밀번호란에 입력 ​​3. 애플리케이션 비밀번호 사용 내역애플리케이션 비밀번호는 한 번 생성한 후 보안을 위해 다시 확인할 수 없습니다.​사용하지 않는 애플리케이션 비밀번호는 삭제하세요.2단계 인증을 해제하면 모든 애플리케이션 비밀번호도 함께 삭제됩니다.​설정해 둔 애플리케이션 비밀번호를 삭제하거나, 생성일 또는 최근 사용일은 아래 경로에서 확인 가능합니다.[네이버ID &gt; 보안설정 &gt; 기본보안설정 &gt; 2단계 인증 &gt; 관리]​​※ 2025년 6월 24일, POP3/IMAP/SMTP 및 외부메일 가져오기 시 비밀번호 정책이 변경되었습니다.​아래 링크를 통해 자세한 내용을 확인할 수 있습니다.메일 서비스 공지사항 바로가기POP3/IMAP/SMTP 및 외부메일 가져오기 FAQ 바로가기​

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

<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>
Code language: HTML, XML (xml)

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;
}
Code language: JavaScript (javascript)

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

<%@ 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>
Code language: HTML, XML (xml)

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;
}
Code language: JavaScript (javascript)

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;
}
Code language: JavaScript (javascript)
  • 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();
}

...
Code language: JavaScript (javascript)

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);
}
Code language: PHP (php)

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

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

JavaMailSenderImpl이 준비된 MimeMessagePreparator를 보낸다.

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

MimeMessageHelper (Spring Framework 7.0.2 API)


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;
}
Code language: JavaScript (javascript)

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

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;
}
Code language: JavaScript (javascript)

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

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


댓글 남기기