서버 상관없이 main메소드에서 보내는 것부터 해보자.
pom.xml
Spring에서 mail을 보내보자.
mail을 사용하기 위해선 다음과 같이 2가지 dependency가 필요하다.
<!-- email -->
<!-- https://mvnrepository.com/artifact/javax.mail/mail -->
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context-support -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${org.springframework-version}</version>
</dependency>
Javax.mail은 SMTP, POP3, IMAP을 통해 전자 메일을 주고 받을 수 있게 하는 자바 API이다.
piring-context-support에는 캐시,메일,스케줄링,UI 관련 기능들이 포함되어있다.
interface MailSender
스프링은 메일 발송 기능을 위한 MailSender 인터페이스를 제공하고 있으며 다음과 같이 정의되어있다.
public interface MailSender {
/**
* Send the given simple mail message.
* @param simpleMessage the message to send
* @throws MailParseException in case of failure when parsing the message
* @throws MailAuthenticationException in case of authentication failure
* @throws MailSendException in case of failure when sending the message
*/
void send(SimpleMailMessage simpleMessage) throws MailException;
/**
* Send the given array of simple mail messages in batch.
* @param simpleMessages the messages to send
* @throws MailParseException in case of failure when parsing a message
* @throws MailAuthenticationException in case of authentication failure
* @throws MailSendException in case of failure when sending a message
*/
void send(SimpleMailMessage... simpleMessages) throws MailException;
}
MailSender 인터페이스는 SimpleMailMessage를 전달받아 메일을 발송하는 기능을 정의하고 있다.
SimpleMailMessage는 메일 제목과 단순 텍스트 내용으로 구성된 메일을 발송할 때에 사용된다.
MailSender 인터페이스를 상속받은 JavaMailSender는
Java Mail API의 MimeMessage를 이용해서 메일을 발송하는 기능을 추가적으로 정의하고 있다.
스프링은 JavaMailSender인터페이스의 구현체로 JavaMailSenderImpl 클래스를 제공하고 있다.
우리는 이 클래스를 이용해 빈 설정을 하면 된다.
Context-mail.xml
이제 실제 메일을 보내는 역할을하는 빈등록을하자. context-main.xml에 만들어도 되지만
Spring Bean Configuration file을 새로 만들자.
context-mail.xml
(구글정책에 따라 프로퍼티 설정 방법이 바뀔 수 있다. 그때는 발생하는 에러를 잘 보고 구글에 검색해보자.)
(글 마지막에 정책에 따른 설정 작성해놨다. )
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- email 인증 관련 -->
<bean id="mailSender"
class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="smtp.gmail.com" />
<property name="port" value="587" />
<property name="username" value="본인구글계정아이디" /> <!--나는 hch930620@gmail.com -->
<property name="password" value="본인구글계정패스워드" />
<property name="javaMailProperties">
<props>
<prop key="mail.transport.protocol">smtp</prop>
<prop key="mail.smtp.auth">true</prop>
<prop key="mail.smtp.starttls.enable">true</prop>
<prop key="mail.debug">true</prop>
<prop key="mail.smtp.ssl.trust">smtp.gmail.com</prop>
<prop key="mail.smtp.ssl.protocols">TLSv1.2</prop>
</props>
</property>
</bean>
</beans>
이렇게 설정값을 직접 입력해도 상관없지만
email설정값 외에 DB설정, 파일경로설정 등 여러가지 설정값들을 한곳에서 관리하는게 좋다.
그 설정파일을 우리는 appconfig.properties라는 properties파일로 관리했다.
appconfig.properties에 이메일설정값을 추가해주자.
appconfig.properties
#mail information
mail.host=smtp.gmail.com
mail.port=587
mail.username=본인구글계정아이디
mail.password=본인구글계정패스워드
mail.javaMailProperties.protocol=smtp
mail.javaMailProperties.auth=true
mail.javaMailProperties.enable=true
mail.javaMailProperties.debug=true
mail.javaMailProperties.ssl.trust=smtp.gmail.com
mail.javaMailProperties.ssl.protocols=TLSv1.2
appconfig.properties를 이용해 context-email.xml을 작성하면 다음과 같다.
<bean id="mailSender"
class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="#{util['mail.host']}" /> <!-- { 중괄호 쓰는 습관 꼭. } -->
<property name="port" value="#{util['mail.port']}" />
<property name="username" value="#{util['mail.username']}" />
<property name="password" value="#{util['mail.password']}" />
<property name="javaMailProperties">
<props>
<prop key="mail.transport.protocol">#{util['mail.javaMailProperties.protocol']}</prop>
<prop key="mail.smtp.auth">#{util['mail.javaMailProperties.auth']}</prop>
<prop key="mail.smtp.starttls.enable">#{util['mail.javaMailProperties.enable']}</prop>
<prop key="mail.debug">#{util['mail.javaMailProperties.debug']}</prop>
<prop key="mail.smtp.ssl.trust">#{util['mail.javaMailProperties.ssl.trust']}</prop>
<prop key="mail.smtp.ssl.protocols">#{util['mail.javaMailProperties.ssl.protocols']}</prop>
</props>
</property>
</bean>
실제 메일을 보내는 Service를 작성하자.
MailSendService.java
package com.study.member.service;
import java.io.UnsupportedEncodingException;
import java.util.Random;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.stereotype.Service;
@Service
public class MailSendService {
@Autowired //context-mail에서 빈 등록했기 때문에 주입받을 수 있다. Spring에서 제공하는 MailSender.
private JavaMailSenderImpl mailSender;
private String getKey(int size) {
return "622354"; //6개 숫자 랜덤 만들어보세요
}
public String sendAuthMail(String mail) throws MessagingException{
String authKey = getKey(6);
MimeMessage mailMessage = mailSender.createMimeMessage();
String mailContent = "인증번호: "+authKey ; //보낼 메시지
mailMessage.setSubject("창희가보내는메일", "utf-8");
mailMessage.setText(mailContent, "utf-8");
mailMessage.addRecipient(Message.RecipientType.TO, new InternetAddress(mail));
mailSender.send(mailMessage);
return authKey;
}
}
mailSender.send(mailMessage)에 의해 실제로 파라미터로 입력받은 mail 주소로 이메일이 전달된다.
어찌됐든 이 MailSendService의 sendAuthMail()메소드가 실행되도록 하기만 하면된다.
먼저 step2.jsp의 이메일부분에 버튼을 만들자.
<tr class="form-group-sm">
<th>이메일</th>
<td><form:input path="memMail" cssClass="form-control input-sm" />
<form:errors path="memMail" />
<button onclick="return false;" type="button" id="mailAuth">mail인증하기</button>
</td>
</tr>
버튼클릭이벤트를 다음과 같이 만들자.
//mail인증하기 버튼 클릭
$("#mailAuth").on("click",function(e){
isMailAuthed=true;
$.ajax({
url : "<c:url value='mailAuth.wow' />"
,data : {"mail" : $("input[name='memMail']").val()}
,success: function(data){
alert(data);
},error : function(req,status,err){
console.log(req);
}
});//ajax
});//mailCheck
그리고 MemberJoinController에 mailAuth.wow를 처리하는 mailAuth메소드를 만들자.
@Inject
MailSendService mailSendService; //@Service를 붙였었다.
@RequestMapping("/join/mailAuth.wow")
@ResponseBody
public String mailAuth(String mail, HttpServletResponse resp) throws Exception {
String authKey = mailSendService.sendAuthMail(mail); //사용자가 입력한 메일주소로 메일을 보냄
return authKey;
}
여기까지했다면 "mail 인증하기"버튼을 눌렀을 때 mail이 갈 것이다.
gks93062@naver.com 으로 메일이 왔다. 보낸사람을 보면 hch930620@gmail.com이다.
(톰캣 서버가 직접 보내는게 아니라 java를 이용해
'구글한테 naver.com으로 메일좀 보내줘'라는 코드를 MailSendService의 sendAuthMail에 작성했다.)
메일자체를 보내는건 어려운 것이 없다.
이제 mail에 온 인증코드를 사용자가 입력해서 인증여부를 확인하는 작업을 하자.
먼저 DB에 인증키와 메일주소, 인증완료 여부를 저장할 테이블을 만들자.
create table mail_auth( mail varchar(40), auth_key varchar(40), is_auth number);
MailAuthVO를 만들자.
package com.study.member.vo;
public class MailAuthVO {
private String mail;
private String authKey;
private int isAuth;
public String getMail() {
return mail;
}
public void setMail(String mail) {
this.mail = mail;
}
public String getAuthKey() {
return authKey;
}
public void setAuthKey(String authKey) {
this.authKey = authKey;
}
public int getIsAuth() {
return isAuth;
}
public void setIsAuth(int isAuth) {
this.isAuth = isAuth;
}
}
로직을 대충 설명하면 다음과 같다. 이 때 step2.jsp는 부모창, mailWindow.jsp는 자식창이다.
0. DB와 상관없이 step2.jsp로 온다면(첫요청이든,validation 통과 실패든) id 중복체크 확인과,
mail인증하기 버튼을 누르도록 javascript에서 변수를 선언한다. isIdChecked, isMailAuthed.
1. step2.jsp에서 mail인증하기버튼을 누른다. ajax를 호출한다.
=> @ReuqestMapping("/join/mailAuth.wow")에서
mail을 보내고 DB에 mail주소와 authKey를 insert한다.
2. 1이 성공했다면 mailWindow.jsp(자식창)을 연다.
3. 자식창에서 사용자는 이메일을 확인해서 authKey를 입력한다.
ajax를 호출해 DB에 있는 authKey랑 비교해서
맞으면 DB에서 is_auth=1로 변경한다. 그 후 자식창 close()
4. 사용자가 step2.jsp에서 이제 나머지 필드 입력 후 다음버튼을 누르면
id중복체크 및 mail인증여부를 확인한다. 변수에 의해 id체크도했고,
mail인증하기 버튼도 눌렀었다면 ajax로 DB에 is_auth=1인지 확인해서
1이면 mail인증된걸로 보고 다음 화면으로 넘어간다.
IMemberDao, member.xml에 다음과 같은 메소드를 추가하자.
IMemberDao.java
public int insertMailAuth(@Param("mail")String mail,@Param("authKey")String authKey);
public MailAuthVO getMailAuth(String mail); //여기저기서 쓰임. 인증되었는지 확인하는데에도 쓰임
public int updateMailAuth(@Param("mail")String mail,@Param("authKey")String authKey); //인증키 재발급+같은메일로다시가입할때
//부모창 mail인증하기 버튼
public int completeAuth(String mail); //인증완, 자식창 확인버튼
member.xml
<insert id="insertMailAuth" >
INSERT INTO mail_auth (mail,auth_key,is_auth)
values (#{mail},#{authKey},0)
</insert>
<select id="getMailAuth" parameterType="String" resultType="com.study.member.vo.MailAuthVO">
SELECT mail, auth_key, is_auth
FROM mail_auth
WHERE mail=#{mail}
</select>
<update id="updateMailAuth" parameterType="String">
UPDATE mail_auth
SET auth_key=#{authKey}
,is_auth=0
WHERE mail=#{mail}
</update>
<update id="completeAuth" parameterType="String">
UPDATE mail_auth SET is_auth=1
WHERE mail=#{mail}
</update>
IMemberService, MemberServiceImpl에 다음과 같은 메소드를 추가하자.
IMemberService.java
public void registMailAuth(String mail, String authKey) throws BizNotEffectedException;//부모창 mail 확인하기, 확인하기버튼 누르는 순간 인증키랑 mail 저장
public void authKeyCompare(MailAuthVO mailAuth) throws BizNotEffectedException ,BizPasswordNotMatchedException;
//자식창 확인버튼, 사용자가 메일에서 인증번호 입력하면 DB에 있는 내용이랑 비교. 같으면 is_auth=1로 바꿔서 인증
public int isMailAuthed(String mail) ; // 부모창 다음버튼, is_auth=1이면 1리턴해서 인증되었다는걸 알림
MemberServiceImpl.java
@Override
public void registMailAuth(String mail, String authKey) throws BizNotEffectedException {
MailAuthVO mailAuth= memberDao.getMailAuth(mail);
if(mailAuth==null)memberDao.insertMailAuth(mail, authKey);
else {
memberDao.updateMailAuth(mail, authKey);
}
}
@Override
public void authKeyCompare(MailAuthVO mailAuth) throws BizNotEffectedException, BizPasswordNotMatchedException {
MailAuthVO vo=memberDao.getMailAuth(mailAuth.getMail()); //왠만하면 null안나옴. 자식창에서 확인바로누르면.
//vo는 DB에 있는 거, mailAuth는 자식창에서 입력한 값
if(!vo.getAuthKey().equals(mailAuth.getAuthKey())) {
throw new BizPasswordNotMatchedException();
//자식창에서 입력한 authKey가 다르면
}
if(vo.getAuthKey().equals(mailAuth.getAuthKey())) { //인증번호 제대로 입력했으면
int cnt=memberDao.completeAuth(mailAuth.getMail());
if(cnt==0) throw new BizNotEffectedException();
}
}
@Override
public int isMailAuthed(String mail) {
MailAuthVO mailAuth=memberDao.getMailAuth(mail);
if(mailAuth==null) return 0;
return mailAuth.getIsAuth();
}
컨트롤러.
MemberJoinController.java
@Inject
MailSendService mailSendService;
@RequestMapping("/join/mailAuth.wow")
@ResponseBody
public String mailAuth(String mail, HttpServletResponse resp) throws Exception {
String authKey = mailSendService.sendAuthMail(mail); //사용자가 입력한 메일주소로 메일을 보냄
memberService.registMailAuth(mail, authKey); //메일보냄과 동시에 DB저장
return authKey;
}
// 자식창 생성
@RequestMapping("/join/mailWindow.wow")
public String mailWindow(String mail, HttpServletResponse resp) throws Exception {
return "join/mailWindow"; //step2.jsp화면에서 mail인증하기 버튼 누르면 자식창생김. 거기서 사용자는 authKey입력
}
//자식창에서 authKey맞는지확인
@RequestMapping(value="/join/authKeyCompare.wow", produces = "text/plain;charset=utf-8")
@ResponseBody
public String authKeyCompare(MailAuthVO mailAuth) {
try{
memberService.authKeyCompare(mailAuth); //authKey는 사용자가 입력한 값
return "authKeySame";
} catch (Exception e) {
return "somethingWrong";
}
}
//다음버튼눌렀을 때 isAuth=1인지 확인
@RequestMapping(value="/join/isMailAuthed.wow" ,produces = "text/plain;charset=utf-8")
@ResponseBody
public String isMailAuthed(String mail) {
int isAuth= memberService.isMailAuthed(mail);
if(isAuth==1) return "메일인증완";
else return "인증안됨";
}
step2.jsp id중복체크와 같이 보면 완성된 script는 다음과 같다.
<script type="text/javascript">
$(document).ready(function(){
var isIdChecked=false;
var isMailAuthed=false; //DB상관없이 무조건 mail인증하기버튼 누르도록 하기위해..
$("#idCheck").on("click", function(e){
e.preventDefault(); //form태그안의 있는 버튼이라 그냥 submit되는거 방지
$.ajax({
url : "<c:url value='idCheck.wow' />"
,data : {"id" : $("input[name='memId']").val()}
,success :function(data){
if(data=="사용가능") isIdChecked=true;
alert(data);
},error : function(req,status,err){
console.log(req);
}
}); //ajax
});// idCheck
$("input[name='memId']").on("change", function(e){
isIdChecked=false;
});//memId값 바뀔 때마다
//form태그내의 버튼 클릭 (일부러 idCheck, email인증 버튼은 type을 button으로 했음. 어찌됐든 다음버튼 클릭 이벤트가 되도록 jquery 잘 쓰기)
$("button[type=submit]").click(function(e) {
e.preventDefault();
if(isIdChecked){
if(isMailAuthed){ // 인증확인 버튼은 무조건 눌러야 된다. 맨 처음에.. vaildation에 의해 돌아왔어도 버튼은 누르도록하자.
//ajax로 email 인증됐나 확인
$.ajax({
url :"<c:url value='isMailAuthed.wow' />"
,type : "POST"
,data : {"mail" : $("input[name=memMail]").val()}
,success: function(data){
//자식창에서 인증번호 잘 눌러서 DB에 is_auth=1로 되어있는지 확인
if(data=="메일인증완")$("form").submit();
else alert("mail인증해주세요");
},error : function(req,st,err){
}
});//ajax
}else{
alert("메일인증해주세요")
}
}else {
alert("id 중복체크를 해주세요");
}
}); //다음버튼
//mail인증하기 버튼 클릭
$("#mailAuth").on("click",function(e){
isMailAuthed=true; //다음버튼 누를 수 있게.
$.ajax({
url : "<c:url value='mailAuth.wow' />"
,data : {"mail" : $("input[name='memMail']").val()}
,success: function(data){ //1.메일보내고 DB에 넣었으면 자식창 띄우기
var mailWindow=window.open("<c:url value='mailWindow.wow' />","메일인증","_blank, width=500px, height=200px,left=500px,top=200px");
},error : function(req,status,err){
console.log(req);
}
});//ajax
});//mailCheck
}); //ready
</script>
그리고 WEB-INF/views/join 폴더에 mailWindow.jsp를 만들어주자.
mailWindow.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%
request.setCharacterEncoding("UTF-8");
%>
<!DOCTYPE html>
<html>
<head>
<%@include file="/WEB-INF/inc/header.jsp"%>
<title></title>
</head>
<body>
<div class="container">
<div class="row col-md-8 col-md-offset-2">
<div class="page-header">
<h3>이메일인증</h3>
</div>
<table class="table">
<tr>
<td>
<input type="text" name="authKey" class="form-control input-sm" placeholder="인증번호를 입력하세요">
<button onclick="return false;" type="button" id="authKeyCompare">확인</button>
</td>
</tr>
</table>
</div>
</div>
</body>
<script type="text/javascript">
$(document).ready(function(){
$("#authKeyCompare").on("click",function(e){
e.preventDefault(); //걍 심심해서씀. form태그 없어서 사실 필요없음
var mail=opener.document.getElementsByName("memMail")[0].value;
var authKey=$("input[name='authKey']").val();
$.ajax({
url : "<c:url value='authKeyCompare.wow' />"
,type : "POST"
,data : {"mail" : mail, "authKey" :authKey }
,success : function(data){
if(data=="authKeySame"){
window.close();
} else{
alert("인증번호달라");
}
}, error :function(req,status,err){
console.log(req);
}
});//ajax
}); //확인버튼클릭
}); //ready
</script>
</html>
구글정책에 따른 설정 변경
2021.06
사실 구글이 보안상의 이유로 톰캣에서의 요청을 허락하지 않아 콘솔에 다음과 같은 에러가 난다.
AuthenticationFailedException: Username and Password not accepted
이는 구글계정의 보안을 낮추면 된다. 모든 보안을 낮추는것이 아니라 '보안수준이 낮은 앱이
구글에 접근할 때의 허용하겠다'는 것이다.
이 마저도 불안하다면 그냥 실습용 구글 계정을 하나 만들어서 보안을 낮춰주자.
https://myaccount.google.com/lesssecureapps 에서 허용해주면된다.
또 이 허용해주는 '활동'이 본인이 한 활동인지 구글메일이 날라오는 경우도 있다.
이 때 본인 활동이 맞다고 체크해주자.
이렇게 보안설정을 다 해주면 위에처럼 사용자가 입력한 메일주소로 메일이 날라온다.
-----------------------------------------------------------------------------------------------------------------------------------
2022/07
※구글정책으로 실제 구글메일 비밀번호가 아닌 앱 비밀번호를 password부분에 적도록 변경되었다.
https://myaccount.google.com/security 구글 계정 보안에 가서
2단계 인증을 활성화 한 후
앱 비밀번호를 생성하자. 생성된 앱 비밀번호를 JavaMailSender 빈에 password 속성에 입력하면 된다.
혹시 앱 비밀번호가 안 나오는 경우는 아래와 같이 진행해보자