본문 바로가기

Springboot

springboot email 인증

기존의 security가 적용된 코드에서 추가한 것이기 때문에 중간중간 security관련 코드는 이전포스팅을 참고해주세요.

 

0. 사전준비사항

https://gofnrk.tistory.com/82

 

Spring Boot 메일 발송 (gmail, naver, daum, nate)

스프링 부트 환경에서 메일을 발송해볼게요. 관리자 계정으로 서버의 상태를 전송한다던가 이메일 인증 메일을 보낸다던가할 때 지금 포스팅한 내용대로 적용하실 수 있을 것 같네요. 준비사항으로는 본인의 gmail..

gofnrk.tistory.com

해당 사이트를 참고하셔서 사용하실 이메일의 엑세스를 허용해주세요.

 

 

1. 이메일 인증 흐름

시작전 이메일 인증할 흐름을 설명해드리겠습니다.

 

1) 유저가 가입을 진행할시 인증키(랜덤값)을 유저테이블에 같이 저장합니다.

 

2) 로그인시 유저테이블에 랜덤값이 있을경우에는 가가입 상태로 판단하여 이메일 인증이 필요하다는 페이지를 출력 합니다.

가가입 화면

 

3) 이메일 인증 버튼을 눌렀을경우 smtp를 이용하여 가입시 입력한 이메일로 메일을 보내게됩니다.

메일 인증 버튼에는 유저이메일과 인증키가 경로에 담겨있습니다.

4) 메일인증 버튼을 눌렀을 경우 경로에 있는 유저이메일과 인증키로 DB에서 검색합니다.

DB에 존재할경우 인증키를 Y로 바꾸고 이메일 인증이 완료되었다고 판단합니다.

 

 

2. DB수정

CREATE TABLE `user` (
  `username` varchar(20) COLLATE utf8mb4_general_ci NOT NULL,
  `password` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `name` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `isAccountNonExpired` tinyint(1) DEFAULT NULL,
  `isAccountNonLocked` tinyint(1) DEFAULT NULL,
  `isCredentialsNonExpired` tinyint(1) DEFAULT NULL,
  `isEnabled` tinyint(1) DEFAULT NULL,
  `certified` VARCHAR(50) DEFAULT NULL, 
  PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

기존테이블에서 certified를 추가하여 생성하였습니다.

 

2. pom.xml

<!-- Mail -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

 

3. application.properties

spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=<googleid>
spring.mail.password=<googlepassword>
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true

저는 gmail을 선택했습니다.

googleid와 googlepassword에 여러분의 id와 password를 입력해주세요.

 

4. User

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

public class User implements UserDetails {
	
	private static final long serialVersionUID = 5177294317961485740L;

	private String username;
    private String password;
    private String name;
    private String certified;
    private boolean isAccountNonExpired;
    private boolean isAccountNonLocked;
    private boolean isCredentialsNonExpired;
    private boolean isEnabled;
    private Collection<? extends GrantedAuthority> authorities;
   
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
         return authorities;
    }

    @Override
    public String getPassword() {
         return password;
    }

    @Override
    public String getUsername() {
         return username;
    }

    @Override
    public boolean isAccountNonExpired() {
         return isAccountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
         return isAccountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
         return isCredentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
         return isEnabled;
    }

    public String getName() {
         return name;
    }

    public void setName(String name) {
         this.name = name;
    }

    public void setUsername(String username) {
         this.username = username;
    }

    public void setPassword(String password) {
         this.password = password;
    }
    
    public String getCertified() {
		return certified;
	}

	public void setCertified(String certified) {
		this.certified = certified;
	}

	public void setAccountNonExpired(boolean isAccountNonExpired) {
         this.isAccountNonExpired = isAccountNonExpired;
    }

    public void setAccountNonLocked(boolean isAccountNonLocked) {
         this.isAccountNonLocked = isAccountNonLocked;
    }

    public void setCredentialsNonExpired(boolean isCredentialsNonExpired) {
         this.isCredentialsNonExpired = isCredentialsNonExpired;
    }

    public void setEnabled(boolean isEnabled) {
         this.isEnabled = isEnabled;
    }

    public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
         this.authorities = authorities;
    }

	public User() {
		super();
	}

	@Override
	public String toString() {
		return "User [username=" + username + ", password=" + password + ", name=" + name + ", certified=" + certified
				+ ", isAccountNonExpired=" + isAccountNonExpired + ", isAccountNonLocked=" + isAccountNonLocked
				+ ", isCredentialsNonExpired=" + isCredentialsNonExpired + ", isEnabled=" + isEnabled + ", authorities="
				+ authorities + "]";
	}
}

기존의 코드에서 certified를 추가합니다.

 

4. UserController

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;

import com.security_blog.yg1110.domain.User;
import com.security_blog.yg1110.servicer.IUserService;

@Controller
public class UserController {

	@Autowired
	private IUserService userservice;

    // 회원가입 처리
    @PostMapping("/signup")
    public String execSignup(User user) {
    	System.out.println(user);
    	List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
//		authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
		authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
		user.setAuthorities(authorities);
		user.setAccountNonExpired(true);
		user.setAccountNonLocked(true);
		user.setCredentialsNonExpired(true);
		user.setEnabled(true);
		user.setCertified(certified_key());
    	userservice.createUser(user);
        return "redirect:/user/login";
    }

    private String certified_key() {
		Random random = new Random();
		StringBuffer sb = new StringBuffer();
		int num = 0;

		do {
			num = random.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() < 10);
		return sb.toString();
	}

}

기존의 가입로직에서 10자리 임의의 문자열을 만드는 certified_key()함수를 생성하고 가입시에 certified 필드에 추가합니다.

가입을 했을경우 다음과 같이 certified필드에 임의의 문자가 들어갑니다.

5. EmailService

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;

@Service
public class EmailService {
	
    private JavaMailSender javaMailSender;

    public EmailService(JavaMailSender javaMailSender) {
        this.javaMailSender = javaMailSender;
    }

    public void sendMail(String toEmail, String subject, String message) throws MessagingException {
    	MimeMessage mimeMessage = javaMailSender.createMimeMessage();
    	MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, "utf-8");
    	
    	helper.setFrom("YG1110BLOG"); //보내는사람
    	helper.setTo(toEmail); //받는사람
    	helper.setSubject(subject); //메일제목
    	helper.setText(message, true); //ture넣을경우 html


        javaMailSender.send(mimeMessage);
    }
}

EmailService를 생성하고 보내는사람, 받는사람, 메일제목. 메세지 내용을 설정합니다.

setText의 경우 true옵션을 추가할 경우 메일을 보낼때 html문법이 적용됩니다.

 

6. EmailController

import javax.mail.MessagingException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

import com.security_blog.yg1110.domain.User;
import com.security_blog.yg1110.servicer.EmailService;
import com.security_blog.yg1110.servicer.IUserService;

@RestController
public class EmailController {

	...
    
	@Autowired
	private EmailService emailService;
	
	@GetMapping(value = "/user/email/send")
	public void sendmail(User user) throws MessagingException {
		StringBuffer emailcontent = new StringBuffer();
		emailcontent.append("<!DOCTYPE html>");
		emailcontent.append("<html>");
		emailcontent.append("<head>");
		emailcontent.append("</head>");
		emailcontent.append("<body>");
		emailcontent.append(
				" <div" 																																																	+ 
				"	style=\"font-family: 'Apple SD Gothic Neo', 'sans-serif' !important; width: 400px; height: 600px; border-top: 4px solid #02b875; margin: 100px auto; padding: 30px 0; box-sizing: border-box;\">"		+ 
				"	<h1 style=\"margin: 0; padding: 0 5px; font-size: 28px; font-weight: 400;\">"																															+ 
				"		<span style=\"font-size: 15px; margin: 0 0 10px 3px;\">YG1110 BLOG</span><br />"																													+ 
				"		<span style=\"color: #02b875\">메일인증</span> 안내입니다."																																				+ 
				"	</h1>\n"																																																+ 
				"	<p style=\"font-size: 16px; line-height: 26px; margin-top: 50px; padding: 0 5px;\">"																													+ 
				user.getName()																																																+
				"		님 안녕하세요.<br />"																																													+ 
				"		YG1110 BLOG에 가입해 주셔서 진심으로 감사드립니다.<br />"																																						+ 
				"		아래 <b style=\"color: #02b875\">'메일 인증'</b> 버튼을 클릭하여 회원가입을 완료해 주세요.<br />"																													+ 
				"		감사합니다."																																															+ 
				"	</p>"																																																	+ 
				"	<a style=\"color: #FFF; text-decoration: none; text-align: center;\""																																	+
				"	href=\"http://localhost:8080/user/email/certified?username=" + user.getUsername() + "&certified=" + user.getCertified() + "\" target=\"_blank\">"														+ 
				"		<p"																																																	+
				"			style=\"display: inline-block; width: 210px; height: 45px; margin: 30px 5px 40px; background: #02b875; line-height: 45px; vertical-align: middle; font-size: 16px;\">"							+ 
				"			메일 인증</p>"																																														+ 
				"	</a>"																																																	+
				"	<div style=\"border-top: 1px solid #DDD; padding: 5px;\"></div>"																																		+
				" </div>"
		);
		emailcontent.append("</body>");
		emailcontent.append("</html>");
		emailService.sendMail(user.getUsername(), "[YG1110 이메일 인증]", emailcontent.toString());
	}
    
    	...
}

EmailContorller를 생성하고 보내고싶은 html코드를 작성합니다.

이메일 전송은 페이지 이동 버튼을 눌렀을때 페이지 이동이 일어나지 않도록 하기 위해 RestController로 비동기통신 하도록 구현했습니다.

중간에 a태그로 user/email.certified주소로 username과 certified를 넘기고 있는데, 추후에 전송된 이 값들을 이용하여 이메일 인증을 진행합니다.

 

7. 메일전송 테스트

<script
	src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>

<sec:authorize access="isAuthenticated()">
	<sec:authentication property="principal" var="user" />

    <form id="email_form">
        <input type="hidden" name="certified" value="${user.certified}">
        <input type="hidden" name="username" value="${user.username}">
        <input type="hidden" name="name" value="${user.name}">												
    </form>
</sec:authorize>

<button id="email_send_buttton">이메일 인증하기</button>

<script type="text/javascript">
		$('#email_send_buttton').on(
				'click',
				function() {
					$.ajax({
						url : "/user/email/send",
						type : "GET",
						data : $("#email_form").serialize(),
						success : function(data) {
							alert("이메일이 전송되었습니다.")
						},
						error : function(e) {
							console.log(e);
						}
					});
 			});
	</script>

기존의 security가 적용된 코드이기 때문에 isAuthenticated()로 로그인 여부를 검사하고

로그인 정보를 user로 받아옵니다.

그값들 중 username, certified, name을 from을 이용하여 넘깁니다.

위에서 이메일전송은 비동기로 구현하였기 때문에 ajax cdn을 추가하고 ajax로 데이터를 전송했습니다.

 

보낸사람은 properties에서 설정한 구글이며, 현재 가입은 네이버로 하여 이메일 전송버튼을 누른다면

다음과 같이 위에서 만든 html코드에 따라 메일이 온것을 확인할 수 있습니다.

메일인증 버튼을 누를경우

http://localhost:8080/user/email/certified?username=younggil94@naver.com&certified=XKqqAq8azF

다음과 같은 주소로 요청을 하게됩니다.

 

8. emailController 추가

	@Autowired
	private IUserService userservice;

	@GetMapping(value = "/user/email/certified")
	@Transactional
	public ModelAndView checkmail(HttpServletRequest request, User user) throws MessagingException {
		HttpSession session = request.getSession();
		User u = userservice.email_certified_check(user);
		
		if(u != null) {
			userservice.email_certified_update(user);
			SecurityContextHolder.getContext().setAuthentication(null);
			session.removeAttribute("Authorization");
		}

		return new ModelAndView("email_success");
	}

 

사용자에게 입력받은 username과 certified값을 이용하여 email_certified_check를 진행하여 유저 정보를 찾게됩니다.

만약 유저정보를 찾았을경우 email_certified_update를 이용하여 certified정보를 수정합니다.

 

email_certified_check 에서 처리한 쿼리문이 정상적으로 완료가 되고, email_certified_update 에서 처리 도중 에러가 났을

email_certified_check 에서 처리한 쿼리를 자동 rollback 해주기 위해 @Transactional를 사용하였습니다.

 

emailContorller는 RestContoller 어노테이션이 붙어있기 때문에 return값을 ModelView로 하였고, 현재 로그인정보를 초기화시키고email_success페이지로 이동시켰습니다.

 

9. IUserService 추가

import com.security_blog.yg1110.domain.User;

public interface IUserService extends UserDetailsService {
	public User email_certified_check(User user);
	public void email_certified_update(User user);
}

 

10. UserService

import com.security_blog.yg1110.domain.User;
import com.security_blog.yg1110.mapper.UserMapper;

@Service
public class UserService implements IUserService {

	@Autowired
    private UserMapper userMapper;

	@Override
	public User email_certified_check(User user) {
		return userMapper.email_certified_check(user);
	}

	@Override
	public void email_certified_update(User user) {
		userMapper.email_certified_update(user);		
	}
}

 

11. UserMapper

import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.security_blog.yg1110.domain.User;

@Mapper
public interface UserMapper {
	public List<User> Userlist();
	public User readUser(String username);
	public List<String> readAuthority(String username);
	public void createUser(User user);
	public void createAuthority(User user);
	public void deleteUser(String username);
	public void deleteAuthority(String username);
	public String email_duplicate_check(User user);
	public User email_certified_check(User user);
	public void email_certified_update(User user);
}

 

12. User.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.security_blog.yg1110.mapper.UserMapper">
	<select id="email_certified_check" parameterType="User" resultType="User">
		select username from user where username = #{username} and certified=#{certified}
	</select>
	
	<update id="email_certified_update" parameterType="User">
		update user set certified = 'Y' where username = #{username}
	</update>
</mapper>

인증이 완료되면 Y로 변경시킵니다.