이제 인증 메일과 비밀번호 암호화 기능을 추가 해보려고 한다.
사실 요즘엔 핸드폰 인증을 더 많이 쓰는 것 같긴한데...더 간단하고 스프링에서 기본으로 제공하는 API가 있고 돈이 안들어가는 이메일 인증 방법을 택했다.
이메일 인증은 회원가입 프로세스에서 중요한 단계 중 하나이다.
이메일 인증을 통해 회원 신원을 확인할 수 있으며 불법적인 가입이나 다른 사용자의 계정 사용을 방지해서 보안을 강화시킬 수 있는 것이 장점이다. 또한, 더 나아가면 중요한 서비스 안내나 소식 전달도 이메일을 통해 할 수 있을 것!
1️⃣ 링크 참고하여 이메일 설정 ( 2단계 인증 활성화 ) ⇒ https://myaccount.google.com/u/0/security?hl=k
Google 계정
myaccount.google.com
- 여기서 설정해준 구글 계정이 이메일을 보내는 발신 계정이 된다.
- Google 계정 관리 → 보안 탭을 클릭해 직접 이동해도 된다.
1. 주소로 들어가면 2단계 인증을 클릭해서 인증을 한다.
2. 1번을 진행하면 앱 비밀번호 라는 부분이 추가되어 있다.
3. 앱 비밀번호를 생성할 앱 및 기기를 선택한다.
4. 앱 비밀번호를 생성하면 16자리의 비밀번호가 생성된다. -> gmail 비밀번호 대신 사용하게 될 비밀번호이므로 기억해두기
2️⃣ pom.xml에 메일 라이브러리 추가
먼저 JavaMail API와 Spring Framework의 Context Support를 사용하여 메일 전송과 관련된 라이브러리를 추가해줬다.
<!-- mail library -->
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.7</version>
</dependency>
- JavaMail API는 Java 언어를 사용하여 이메일 전송을 위한 표준 API를 제공한다.
- 메일 전송, 수신 및 관련된 작업을 처리하는 데 사용된다.
<!-- mail 서포트 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${org.springframework-version}</version>
</dependency>
- spirng-context-support는 Spring에서 제공하는 컨텍스트 지원과 관련된 라이브러리로, 이메일 전송과 같은 작업을 더 쉽게 구성하고 관리할 수 있도록 도와준다.
라이브러리를 추가 한 후 꼭 업데이트를 해주자! ( update project )
3️⃣ email-context.xml 또는 MailConfig.java 추가
src/main/java > com.config > MailConfig.java
package com.config;
import java.util.Properties; // 이메일 송신에 필요한 속성들을 설정
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import com.info.Info;
@Configuration // 스프링의 설정 클래스임
public class MailConfig {
private final Info info = new Info();
@Bean // 빈 등록
public JavaMailSender javaMailSender() {
Properties mailProperties = new Properties();
mailProperties.put("mail.transport.protocol", "smtp"); // 메일 전송에 사용할 프로토콜
mailProperties.put("mail.smtp.auth", "true"); // SMTP 서버에 사용자 인증 활성화
mailProperties.put("mail.smtp.starttls.enable", "true"); // STARTTLS 암호화
mailProperties.put("mail.smtp.debug", "true"); // 디버그 모드를 활성화
mailProperties.put("mail.smtp.ssl.trust", "smtp.gmail.com"); // SSL/TLS 연결에서 신뢰할 수 있는 호스트를 지정
mailProperties.put("mail.smtp.ssl.protocols", "TLSv1.2"); // 사용할 SSL/TLS 프로토콜
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setJavaMailProperties(mailProperties); // 앞서 설정한 메일 속성들을 JavaMailSenderImpl에 적용
mailSender.setHost("smtp.gmail.com"); // 메일을 보낼 SMTP 서버의 호스트를 설정
mailSender.setPort(587); // SMTP 서버의 포트 번호를 설정
mailSender.setUsername(info.getMailId()); // 이메일을 보내는 데 사용될 계정의 사용자 이름을 설정
mailSender.setPassword(info.getMailPassword()); // 이메일 계정의 암호를 설정
mailSender.setDefaultEncoding("utf-8"); // 이메일의 기본 인코딩을 설정
return mailSender;
}
}
여기에 아까 발급받은 앱 비밀번호를 넣어준다. (google계정 비밀번호 아님 주의)
4️⃣ 만약 email-context.xml 으로 했다면 email-context.xml을 읽을 수 있도록 web.xml에 코드 추가
>> 나는 java 파일로 했으므로 pass !
5️⃣ MaiHandler.java, TempKey.java 추가
파일 위치
MailHandler.java
package com.mail;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import javax.activation.DataSource;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.UnsupportedEncodingException;
public class MailHandler {
private JavaMailSender mailSender;
private MimeMessage message;
private MimeMessageHelper messageHelper;
public MailHandler(JavaMailSender mailSender) throws MessagingException {
this.mailSender = mailSender;
message = this.mailSender.createMimeMessage();
messageHelper = new MimeMessageHelper(message, true, "UTF-8");
}
public void setSubject(String subject) throws MessagingException {
messageHelper.setSubject(subject);
}
public void setText(String htmlContent) throws MessagingException {
messageHelper.setText(htmlContent, true);
}
public void setFrom(String email, String name) throws UnsupportedEncodingException, MessagingException {
messageHelper.setFrom(email, name);
}
public void setTo(String email) throws MessagingException {
messageHelper.setTo(email);
}
public void addInline(String contentId, DataSource dataSource) throws MessagingException {
messageHelper.addInline(contentId, dataSource);
}
public void send() {
mailSender.send(message);
}
}
- 메일 전송 라이브러리의 setter이다.
- 메일 제목, 내용, 발송자, 수신자, 보내기로 이루어져 있다.
TempKey.java
package com.mail;
import java.util.Random;
public class TempKey{
private boolean lowerCheck;
private int size;
public String getKey(int size, boolean lowerCheck) {
this.size = size;
this.lowerCheck = lowerCheck;
return init();
}
private String init() {
Random ran = new Random();
StringBuffer sb = new StringBuffer();
int num = 0;
do {
num = ran.nextInt(75) + 48;
if ((num >= 48 && num <= 57) || (num >= 65 && num <= 90) || (num >= 97 && num <= 122)) {
sb.append((char) num);
} else {
continue;
}
} while (sb.length() < size);
if (lowerCheck) {
return sb.toString().toLowerCase();
}
return sb.toString();
}
}
- 인증번호를 보낼 때 사용할 클래스이다. 이 클래스를 호출할 때는 몇 자리 수로 할 건지 사이즈를 파라미터로 보내면 된다.
- 실제 이메일을 보내는 코드에 위 코드를 넣는 경우도 있지만, 각 클래스나 메서드를 각자의 역할에 맞게 코드를 분리했고, 추후 비밀번호 찾기를 할 때도 위 클래스를 사용할 것 이기 때문에 따로 분리를 해뒀다.
여기까지 기본 설정과 꼭 필요한 파일들을 생성해줬다.
이제 로직을 짜고 이메일을 보내보자.
1️⃣ 테이블 설계하고 DTO 작성
package com.dto;
import org.apache.ibatis.type.Alias;
@Alias("MemberDTO")
public class MemberDTO {
private String userID;
private String passwd;
private String name;
private String email;
private String post;
private String addr1;
private String addr2;
private String phone;
private int mail_auth; // (추가) 이메일 인증 여부를 나타내는 값
private String mail_key; // (추가) 이메일 인증에 사용되는 키
- mail_auth ⇒ 기본값 0을 넣어 놓고, 이메일 인증을 했을 경우 값을 1로 변경시켜 로그인 되게 함
- mail_key ⇒ TempKey.java에서 생성한 난수를 저장하는데 사용
2️⃣ memberMapper.xml에 3개의 쿼리 추가
회원가입 시 이메일 인증을 위한 랜덤번호 저장
<!-- 이메일 인증을 위한 랜덤번호 저장 -->
<update id="updateMailKey" parameterType="MemberDTO">
update member set mail_key=#{mail_key} where email=#{email} and userID=#{userID}
</update>
메일 인증을 하면 mail_auth 컬럼을 기본값 0에서 1로 바꿔 로그인을 허용
<!-- 메일 인증 후 mail_auto 0 -> 1 변경 -->
<update id="updateMailAuth" parameterType="MemberDTO">
update member set mail_auth=1 where userID=#{userID} and email=#{email}
</update>
이메일 인증을 안 했으면 0을 반환, 로그인 시 인증했나 안 했나 체크
<!-- 로그인 시 인증 유무 체크 -->
<select id="emailAuthFail" parameterType="string" resultType="int">
select count(*) from member where userID=#{userID} and mail_auth=1
</select>
이메일 당 가입된 아이디 개수 확인 ( 필수 xx 부가적인 기능으로 추가 )
<!-- 이메일당 가입된 아이디 개수 -->
<select id="idPerEmailCount" parameterType="string" resultType="int">
select count(*)
from member
where email = #{email}
</select>
3️⃣ MemberDao, MemberService, MemberServiceImpl에 코드 추가
MemberDAO.java
package com.dao;
import java.util.HashMap;
import java.util.List;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.dto.MemberDTO;
@Repository
public class MemberDAO {
@Autowired // SqlSessionTemplate 주입
SqlSessionTemplate session;
// 이메일 인증을 위한 랜덤번호 저장
public int updateMailKey(MemberDTO dto) throws Exception {
return session.update("MemberMapper.updateMailKey", dto);
}
// 메일 인증 후 mail_auto 0 -> 1 변경
public int updateMailAuth(MemberDTO dto) throws Exception {
return session.update("MemberMapper.updateMailAuth", dto);
}
// 로그인 시 인증 유무 체크 => 인증 실패 횟수를 반환
public int emailAuthFail(String userID) throws Exception {
return session.selectOne("MemberMapper.emailAuthFail", userID);
}
// 이메일당 가입된 아이디 개수 => 특정 이메일에 가입된 아이디의 개수를 반환
public int idPerEmailCount(String email) {
return session.selectOne("MemberMapper.idPerEmailCount", email);
}
}
MemberService.java
package com.service;
import java.util.HashMap;
import java.util.List;
import com.dto.MemberDTO;
public interface MemberService {
// 회원가입
public void register(MemberDTO dto) throws Exception;
// 로그인
public MemberDTO login(HashMap<String, String> map);
public MemberDTO idCheck(String userID);
// 이메일 인증
public int updateMailKey(MemberDTO dto) throws Exception;
public int updateMailAuth(MemberDTO dto) throws Exception;
public int emailAuthFail(String userID) throws Exception;
// 이메일당 가입된 아이디 개수
public int idPerEmailCount(String email);
}
MemberServiceImpl.java
@Service
public class MemberServiceImpl implements MemberService {
@Autowired
MemberDAO dao;
@Autowired
JavaMailSender mailSender;
// 다른 메서드들...
@Transactional
@Override
public void register(MemberDTO dto) throws Exception {
// 랜덤 문자열 생성 -> mail_key 컬럼에 넣기
String mail_key = new TempKey().getKey(30, false); // 랜덤 키 길이 30
dto.setMail_key(mail_key);
// 회원가입
dao.register(dto);
// 회원가입 완료 후 인증 이메일 발송
MailHandler sendMail = new MailHandler(mailSender);
sendMail.setSubject("[여담 인증메일 입니다.]"); // 메일 제목
sendMail.setText( // 메일 내용
"<h1>여담 메일 인증</h1>" +
"<br>안녕하세요 " + dto.getUserID() + "님. 여담 가입을 환영합니다!" +
"<br>아래 <b>[이메일 인증 확인]</b>을 눌러주세요." +
"<br><h3>해당 링크의 유효시간은 3분입니다.</h3>" +
"<br><a href='http://localhost:8091/app/registerEmail?userID=" + dto.getUserID() + "&email=" + dto.getEmail() +
"&mail_key=" + mail_key +
"' target='_black'>이메일 인증 확인</a>");
sendMail.setFrom("pjtravelplan@gmail.com", "여담"); // 보내는 사람
sendMail.setTo(dto.getEmail()); // 받는 사람
sendMail.send(); // 메일 보내기
}
// 이메일당 가입된 아이디 개수
@Override
public int idPerEmailCount(String email) {
int num = dao.idPerEmailCount(email);
if(num >= 3) {
return 0;
}
return 1;
}
// 나머지 메서드들...
// 이메일 인증을 위한 랜덤번호 저장 - redis로 대체
@Override
public int updateMailKey(MemberDTO dto) throws Exception {
return dao.updateMailKey(dto);
return 0;
}
// 메일 인증 후 mail_auto 0 -> 1 변경
@Override
public int updateMailAuth(MemberDTO dto) throws Exception {
RedisTemplate<String, Object> redisTemplate = redis.getRedisTemplate();
// 인증 키 확인
String mail_key = (String) redisTemplate.opsForValue().get(dto.getUserID());
if(mail_key == null) { // 인증키 timeout
return 0;
}
if(!mail_key.equals(dto.getMail_key())) { // DB에 저장된 인증키와 url에 전달된 인증키가 다름
return 0;
}
redisTemplate.delete(dto.getUserID()); // 인증 후 키값 초기화
return dao.updateMailAuth(dto);
}
// 로그인 시 인증 유무 체크
@Override
public int emailAuthFail(String userID) throws Exception {
return dao.emailAuthFail(userID);
}
}
- 의존성 추가
- sendMail.setFrom(”보내는사람@이메일”, “쑤”) ⇒ 보내는 사람 이메일에는 위 1번에서 설정해주었던 이메일 주소를 넣어주면 된다.
4️⃣ registerController.java 컨트롤러 추가
package com.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.dto.MemberDTO;
import com.service.MemberService;
@Controller
public class registerController {
@Autowired
MemberService service;
// 신규 회원 입력화면
@GetMapping("/memberUI")
public String memberUI() {
return "registerForm";
}
// 신규 회원 등록
@PostMapping("/register")
public String register(MemberDTO dto) throws Exception {
// 이메일당 가입 계정 개수 확인
if(service.idPerEmailCount(dto.getEmail()) == 0) {
// 세개 이상이면 회원가입 방지
return "member/registerFail_email";
}
service.register(dto);
return "member/emailAuthInfo";
}
// 회원가입시 이메일 인증
@GetMapping("/registerEmail")
public String emailConfirm(MemberDTO dto) throws Exception {
int num = service.updateMailAuth(dto); // 인증키 확인해서 일치하면 해당 회원의 'mail_auth'값을 업데이트
if(num == 0) { // 인증키가 맞지 않음(만료되었거나, 일치하지 않음)
return "member/checkKeyFail";
}
return "member/emailAuthSuccess";
}
}
5️⃣ JSP 작성
emailAuthInfo.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!-- jquery CDN -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>여담: 이메일 인증 실패</title>
<!-- alert 커스텀 -->
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.9.0/dist/sweetalert2.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.9.0/dist/sweetalert2.all.min.js"></script>
<script>
$(document).ready(function(){
Swal.fire({
icon: 'success',
title: '이메일 인증 후 \n로그인 가능합니다.' }).then(function(){
location.href='/app/main';
});
});
</script>
</head>
<body>
</body>
</html>
emailAuthFail.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!-- jquery CDN -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>여담: 이메일 인증 실패</title>
<!-- alert 커스텀 -->
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.9.0/dist/sweetalert2.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.9.0/dist/sweetalert2.all.min.js"></script>
<script>
$(document).ready(function(){
Swal.fire({
icon: 'error',
title: '이메일 인증 후 재시도해주세요.' }).then(function(){
location.href='/app/loginForm';
});
});
</script>
</head>
<body>
</body>
</html>
emailAuthSuccess.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!-- jquery CDN -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>여담: 이메일 인증</title>
<!-- <script>
alert("이메일 인증이 완료되었습니다!");
location.href="/app/loginForm"
</script> -->
<!-- alert 커스텀 -->
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.9.0/dist/sweetalert2.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.9.0/dist/sweetalert2.all.min.js"></script>
<script>
$(document).ready(function(){
Swal.fire({
icon: 'success',
title: '이메일 인증이 완료되었습니다!' }).then(function(){
location.href='/app/loginForm';
});
});
</script>
</head>
<body>
</body>
</html>
'Project 여담 > 6주차' 카테고리의 다른 글
11/10 : object storage (0) | 2023.11.10 |
---|---|
11/09 : 비밀번호 암호화 (0) | 2023.11.09 |
11/07 : 중간발표 / Alert 디자인 추가 (0) | 2023.11.07 |