메일을 보내기 위한 로직
-> UserService
private final JavaMailSender javamailSender; private final SpringTemplateEngine springTemplateEngine;
option + enter
add constructor parameter 선택
-> user_email_verification_codes
테이블 생성
CREATE TABLE `spring3`.`user_email_verification_codes`
(
`index` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`created_at` DATETIME NOT NULL DEFAULT NOW(),
`expires_at` DATETIME NOT NULL,
`expired_flag` BOOLEAN NOT NULL DEFAULT FALSE,
`code` VARCHAR(128) NOT NULL,
`salt` VARCHAR(256) NOT NULL,
`user_index` INT UNSIGNED NOT NULL,
CONSTRAINT PRIMARY KEY (`index`),
CONSTRAINT UNIQUE (`code`, `salt`),
CONSTRAINT FOREIGN KEY (`user_index`) REFERENCES `spring3`.`users` (`index`)
ON DELETE CASCADE
ON UPDATE CASCADE
);
created_at : 이 키가 언제만들어졌는가?
expires_at : 해당 키를 언제까지 나타낼 것인가?created+10분 (default값으로는 못들어간다.)
expired_flag : 기본적으로 false, true가 도되면 해당 키를 더 이상 사용할 수 없다.
salt: 보안성 강화하기 위해서 사용.
-> UserMapper.xml
<insert id="insert" parameterType="dev.jwkim.bbsbasic.entities.UserEntity" useGeneratedKeys="true" keyColumn="index" keyProperty="index"> INSERT INTO `spring3`.`users` (`email`,`password`,`nickname`,`address_postal`, `address_primary`,`address_secondary`) VALUES (#{email}, #{password}, #{nickname}, #{addressPostal},#{addressPrimary},#{addressSecondary}) </insert>
keyColumn : users 테이블에 있는 인덱스 이름
keyProperty : UserEntity에 있는 인덱스 이름
uerGeneratedKeys : 자동 생성 키 값들을 사용하기 위해서 사용된다는 것을 허용한다. ( insert, update에만 적용)
- keyProperty, keyColumn 이 두 개의 키 값은 항상 일치한다.
-> UserService
register메서드에System.out.println(userRegisterVo.getIndex());
을 찍고 회원가입을 하고나니 이렇게 인덱스가 찍히는 것을 확인할 수 있다.
파스칼케이싱 후 적용
-> pom.xml
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency>
apache-commons-lnag 의존성 추가.
-> UserService
Date expiresAt = DateUtils.addMinutes(createAt, 30);
- DateUtils 는 아까 추가한 것을 선택해줘야한다.
final int codeValidMinutes = 30;
final int codeHashIterationCount = 10; // hasing을 10번한다.
final int saltHashIterationCount = 20;
Date createAt = new Date();
Date expiresAt = DateUtils.addMinutes(createAt, codeValidMinutes);
String code = String.format("%d%s%s%s%f%f",
userRegisterVo.getIndex(),
userRegisterVo.getEmail(),
userRegisterVo.getPassword(),
new SimpleDateFormat("yyyyMMddHHmmssSSS").format(createAt),
Math.random(),
Math.random());
String saltA = userRegisterVo.getEmail();
String saltB = userRegisterVo.getPassword();
for(int i =0; i < codeHashIterationCount; i++) {
code = CryptoUtil.hash(CryptoUtil.Hash.SHA512, code);
}
for(int i =0; i < saltHashIterationCount; i++) {
saltA = CryptoUtil.hash(CryptoUtil.Hash.SHA512, saltA);
saltB = CryptoUtil.hash(CryptoUtil.Hash.SHA512, saltB);
}
-> Entites 패키지 UserEmailVerificationCodeEntity
추가
private int index; private Date createdAt; private Date expiresAt; private boolean isExpired; private String code; private String salt; private int userIndex;
+getter,setter
-> UserService
UserEmailVerificationCodeEntity userEmailVerificationCodeEntity = new UserEmailVerificationCodeEntity();
userEmailVerificationCodeEntity.setCreateAt(createAt);
userEmailVerificationCodeEntity.setExpiresAt(expiresAt);
userEmailVerificationCodeEntity.setExpired(false);
userEmailVerificationCodeEntity.setCode(code);
userEmailVerificationCodeEntity.setSalt(String.format("%s%s", saltA, saltB));
userEmailVerificationCodeEntity.setUserIndex(userRegisterVo.getIndex());
this.userMapper.insertUserEmailVerificationCode(userEmailVerificationCodeEntity);
-> IUserMapper 인터페이스
int insertUserEmailVerificationCode(UserEmailVerificationCodeEntity userEmailVerificationCodeEntity);
추가
-> UserMapper.xml 추가 및 수정
<insert id="insert"
parameterType="dev.jwkim.bbsbasic.entities.UserEntity"
useGeneratedKeys="true"
keyColumn="index"
keyProperty="index">
INSERT INTO `spring3`.`users` (`email`, `password`, `nickname`, `address_postal`, `address_primary`,
`address_secondary`, `email_verified_flag`, `deleted_flag`, `suspended_flag`, `admin_flag`)
VALUES (#{email}, #{password}, #{nickname}, #{addressPostal}, #{addressPrimary}, #{addressSecondary},
#{isEmailVerified}, #{isDeleted}, #{isSuspended}, #{isAdmin})
</insert>
<insert id="insertUserEmailVerificationCode"
parameterType="dev.jwkim.bbsbasic.entities.UserEmailVerificationCodeEntity">
INSERT INTO `spring3`.`user_email_verification_codes`(created_at, expires_at, expired_flag, code, salt, user_index)
VALUES (#{createdAt}, #{expiresAt}, #{isExpired}, #{code}, #{salt}, #{userIndex})
</insert>
여기서 회원가입 진행해보면 null이 뜨고 레코드가 추가되는 것을 확인해볼 수 있다.
-> UserService
- Context 추가
MimeMessage mimeMessage = this.javamailSender.createMimeMessage(); MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true); Context context = new Context(); context.setVariable("userRegisterVo", userRegisterVo); context.setVariable("userEmailVerificationCodeEntity", userEmailVerificationCodeEntity); mimeMessageHelper.setSubject("[사이트 이름] 회원가입 인증 메일"); mimeMessageHelper.setTo(userRegisterVo.getEmail()); mimeMessageHelper.setText(this.springTemplateEngine.process("emailVerificationTemplate", context)); this.javamailSender.send(mimeMessage);
-> emailVerificationTemplate
html 추가
request.getScheme(): "http" | "https"
http://127.0.0.1:8080//user/verify-email?c=abc...&s=abc...```
<a th:href="@{verify-email(c= )} 이게 안되는 이유..
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<body th:with="url=@{http://127.0.0.1:8080/user/verify-email(c=${userEmailVerificationCodeEntity.getCode()},s=${userEmailVerificationCodeEntity.getSalt()})}">
<div><b th:text="${userRegisterVo.getNickname()}"></b>님 환영합니다.</div>
<div>이 메세지는 귀하의 이메일 주소에 대해 회원가입 요청이 있었으므로 전송되었습니다. 본인이 가입한게 아니라면 해당 이메일을 폐기해주시기 바랍니다.</div>
<a th:href="${url}">
<button>인증하기</button>
</a>
<div>이메일 보안 정책상 위 링크가 작동하지 않을 경우 다음 주소를 복사하여 이용할 수 있습니다.</div>
<code th:text="${url}"></code>
</body>
</html>
@{${#request.getScheme()} + '://' + ${#request.getServerName()} + ':' + (${#request.getServerPort() != 80 && #request.getSevertPort() != 463 ? ':' + ${#request.getServerPort()} : ''}) + '/'+ ${#request.getContextPath()} + '/user/verify-email'(c=${userEmailVerificationCodeEntity.getCode()}, s=${userEmailVerificationCodeEntity.getSalt()})}
원래는 이렇게 작성해야한다. 정신건강을 위해 아래와 같이 작성하였다.
<body th:with="url=@{http://127.0.0.1:8080/user/verify-email(c=${userEmailVerificationCodeEntity.getCode()},s=${userEmailVerificationCodeEntity.getSalt()})}">
-> userservice
- success 날려준다. 확인을 해보자.
- 회원가입을 하니 회원정보가 userTable에 들어왔고
- 메일이 온 것을 볼 수 있다. ( 자세히 보면 나에게 쓰기로 되어있다.) html코드까지 전부 다 뜨게 된다.
- html이 구문이 작동하기를 받아들일 것이냐에 대한 질문에 true 추가 결과. => 인증하기 버튼이 뜬다.
-> register.html 맨위에 추가
<script th:if="${userRegisterVo != null && userRegisterVo.getResult().name().equals('DUPLICATE_EMAIL')}"> alert('이미 사용 중인 이메일입니다.'); window.history.back(); </script> <script th:if="${userRegisterVo != null && userRegisterVo.getResult().name().equals('DUPLICATE_NICKNAME')}"> alert('이미 사용 중인 닉네임입니다.'); window.history.back(); </script> <script th:if="${userRegisterVo != null && userRegisterVo.getResult().name().equals('FAIRURE')}"> alert('알 수 없는 이유로 회원가입에 실패하였습니다. \n\n잠시 후 다시 시도해주세요.'); window.history.back(); </script> <script th:if="${userRegisterVo != null && userRegisterVo.getResult().name().equals('SUCCESS')}"> alert('입력하신 이메일로 회원가입 인증과 관련된 내용이 전송되었습니다. \n\n해당 메일을 통해 회원가입을 완료해주세요.'); window.location.href = '/'; </script>
양식에 맞게 회원가입을 하게 되면 alart가 이렇게 뜬다.
=> http://localhost:8080/ 메인페이지로 이동한다.
-> UserController
- 입력받지 않은 모든 값들에 대해 기본값을 설정한다.
- login메서드를 만들자
- 가입했던 계정으로 로그인했을 때 아직 이메일인증이 안됬다라는 걸 띄울 수 있기 때문이다.
-> UserLoginVo 생성
public class UserLoginVo extends UserEntity implements IResult<UserLoginResult> {
private UserLoginResult result;
@Override
public UserLoginResult getResult() {
return null;
}
@Override
public void setResult(UserLoginResult userLoginResult) {
}
}
-> UserMapper추가
<select id="select"
parameterType="dev.jwkim.bbsbasic.entities.UserEntity"
resultType="dev.jwkim.bbsbasic.entities.UserEntity">
SELECT `index` AS `index`,
`email` AS `email`,
`password` AS `password`,
`nickname` AS `nickname`,
`address_postal` AS `addressPostal`,
`address_primary` AS addressPrimary,
`address_secondary` AS `addressSecondary`,
`email_verified_flag` AS `isEmailVerified`,
`deleted_flag` AS `isDeleted`,
`suspended_flag` AS `isSuspended`,
`admin_flag` AS `isAdmin`
FROM `spring3`.`users`
WHERE `email` = #{email}
AND `password` = #{password} LIMIT 1
</select>
-> IUserMapper추가
-> UserLoginResult 생성
public enum UserLoginResult {
DELETED,
EMAIL_NOT_VERIFIED,
FAILURE,
ILLEGAL,
SUCCESS,
SUSPENDED
}
-> controller
@RequestMapping(value = "login", method = RequestMethod.POST) // form요청 시(submit) 링크 주소는 /user/login
public String postLogin(
UserLoginVo userLoginVo,
@RequestParam(name = "prev", required = false, defaultValue = "/") String prevPath
) { // prev 이름을 가진 prevPath 값이 '/' 이다.
//System.out.println(prevPath);
userLoginVo.setResult(null);
return "redirect:" + prevPath;
-> UserService
public void login(UserLoginVo userLoginVo) {
if (!UserService.checkEmail(userLoginVo.getEmail()) ||
!UserService.checkPassword(userLoginVo.getPassword())) {
userLoginVo.setResult(UserLoginResult.ILLEGAL);
return;
}
String hashedPassword = CryptoUtil.hash(CryptoUtil.Hash.SHA512, userLoginVo.getPassword());
userLoginVo.setPassword(hashedPassword);
UserEntity userEntity = this.userMapper.select(userLoginVo);
if (userEntity == null || userEntity.getIndex() == 0) {
userLoginVo.setResult(UserLoginResult.FAILURE);
return;
}
if (userEntity.isDeleted()) {
userLoginVo.setResult(UserLoginResult.DELETED);
return;
}
if (userEntity.isSuspended()) {
userLoginVo.setResult(UserLoginResult.SUSPENDED);
return;
}
if (userEntity.isAdmin()) {
userLoginVo.setResult(UserLoginResult.ILLEGAL);
}
if (userEntity.isEmailVerified()) {
userLoginVo.setResult(UserLoginResult.EMAIL_NOT_VERIFIED);
return;
}
userLoginVo.setIndex(userEntity.getIndex());
userLoginVo.setEmail(userEntity.getEmail());
userLoginVo.setPassword(userEntity.getPassword());
userLoginVo.setNickname(userEntity.getNickname());
userLoginVo.setAddressPostal(userEntity.getAddressPostal());
userLoginVo.setAddressPrimary(userEntity.getAddressPrimary());
userLoginVo.setAddressSecondary(userEntity.getAddressSecondary());
userLoginVo.setAdmin(userEntity.isAdmin());
userLoginVo.setDeleted(userEntity.isDeleted());
userLoginVo.setEmailVerified(userEntity.isEmailVerified());
userLoginVo.setSuspended(userEntity.isSuspended());
userLoginVo.setResult(UserLoginResult.SUCCESS);
}
-> UserController
@RequestMapping(value = "login", method = RequestMethod.POST) // form요청 시(submit) 링크 주소는 /user/login
public ModelAndView postLogin(
HttpSession session,
UserLoginVo userLoginVo,
ModelAndView modelAndView
) {
userLoginVo.setResult(null);
this.userService.login(userLoginVo);
if(userLoginVo.getResult() == UserLoginResult.SUCCESS) {
session.setAttribute("userEntity", userLoginVo);
}
modelAndView.addObject("userLoginVo", userLoginVo);
modelAndView.setViewName("login");
return modelAndView;
}
-> login.html 추가
<title>로그인</title>
<script th:if="${userLoginVo.getResult().name().equals('DELETED')}">
alert('이미 탈퇴한 계정입니다.');
</script>
<script th:if="${userLoginVo.getResult().name().equals('EMAIL_NOT_VERIFIED')}">
alert('이메일 인증이 완료되지 않았습니다.');
</script>
<script th:if="${userLoginVo.getResult().name().equals('FAILURE')}">
alert('이메일 혹은 비밀번호가 올바르지 않습니다.');
</script>
<script th:if="${userLoginVo.getResult().name().equals('SUSPENDED')}">
alert('이용이 중지된 계정입니다.');
</script>
<script>
// http://127.0.0.1:8080/user/login?prev=/user/register
const url = new URL(window.location.href);
const searchParams = url.searchParams;
let prev = searchParams.get('prev') ?? '/';
window.location.href = prev;
</script>
// http://127.0.0.1:8080/user/login?prev=/user/register?? 앞에 온 값이 null이거나 undifined이면 ??뒤에 값을 대신해서 쓰겠다는 의미이다.