기재한 이메일로 실제로 이메일을 전송하고 이메일 속 링크를 클릭하면 회원가입이 되도록 해보자.
ALTER TABLE `spring3`.`users`
ADD COLUMN `address_postal` VARCHAR(5) NOT NULL AFTER `nickname`,
ADD COLUMN `address_primary` VARCHAR(100) NOT NULL AFTER `address_postal`,
ADD COLUMN `address_secondary` VARCHAR(100) NOT NULL AFTER `address_primary`;
ALTER TABLE `spring3`.`users`
ADD COLUMN `email_verified_flag` BOOLEAN NOT NULL DEFAULT FALSE AFTER `address_secondary`;
adress~
console에서 주소찾기에 대한 3개의 열을 추가하자.
email_verified_flag
이메일 인증이 완료되었는지에 대한 열도 추가한다.
addressPostal : 우편주소
address_primary : 기본 주소
address_secondary : 상세 주소
-> UserEntity
private String addressPostal; private String addressPrimary; private String addressSecondary; private boolean emailVerifiedFlag;
- entity 추가 + getter,setter
-> register.html
<label>
<span>우편번호</span>
<input maxlength="5" name="addressPostal" placeholder="우편번호" type="text" readonly>
<input type="button" value="주소 찾기" rel="address-search-button">
</label>
<label>
<span>기본 주소</span>
<input maxlength="100" name="addressPrimary" placeholder="기본 주소" type="text" readonly>
</label>
<label>
<span>상세 주소</span>
<input maxlength="100" name="addressSecondary" placeholder="상세 주소" type="text">
</label>
-> register.html js
const addressSearchButton = registerForm.querySelector('[rel="address-search-button"]');
addressSearchButton.addEventListener('click', () => {
alert('!!')
});
우편번호찾기 서비스를 넣을 것이다.
https://postcode.map.daum.net/guide
주소를 script 맨 위에 붙여넣고 option
+ enter
노란색으로 뜨는거 다운로드 해준다.
addressSearchButton.addEventListener('click', () => {
new daum.Postcode({}).open();
});
아까 테스트해봤던 alert지우고 new daum.Postcode({}).open();
를 추가한다. 이거는 우리가 정하는 것이 아니다.
추가하게 되면 주소 찾기를 눌렀을 때 주소찾기서비스 창이 뜬다.
이 창을 옵션창이 되도록 js를 설정해보자
-> register.html, css 추가
<div id="address-search" class="address-search">
<div class="container"></div>
</div>
<style>
main > .address-search {
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 75%);
opacity: 0;
position: fixed;
pointer-events: none;
z-index: 1;
}
main > .address-search.visible {
opacity: 1;
pointer-events: all;
}
main > .address-search > .container {
top: 50%;
left: 50%;
width: 30rem;
height: 30rem;
position: fixed;
transform: translate(-50%,-50%);
}
</style>
-> register.html js
const addressSearchButton = registerForm.querySelector('[rel="address-search-button"]');
const addressSearch = window.document.getElementById('address-search');
const addressSearchContainer = addressSearch.querySelector('.container');
const showAddressSearch = () => {
addressSearch.classList.add('visible');
}
const hideAddressSearch = () => {
addressSearch.classList.remove('visible');
addressSearchContainer.innerHTML ='';
}
변수추가
addressSearchButton.addEventListener('click', () => {
showAddressSearch();
new daum.Postcode({}).embed(addressSearchContainer)
});
addressSearch.addEventListener('click', () => {
hideAddressSearch();
})
배경을 클릭하게 되면 옵션창이 사라지게 한다.
선택한 주소와 우편번호가 input에 들어가도록 설정해보자.
addressSearchButton.addEventListener('click', () => {
showAddressSearch();
new daum.Postcode({
oncomplete: e => {
registerForm['addressPostal'].value = e.zonecode;
registerForm['addressPrimary'].value = e.address;
registerForm['addressSecondary'].value = '';
registerForm['addressSecondary'].focus();
hideAddressSearch();
}).embed(addressSearchContainer)
postcode안의 내용을 추가!
e를 매개변수로 받는 함수 oncomplete (이미 정해져 있는 것)
zonecode : 우편번호 => 우편번호자리에 선택한 우편번호가 들어가게 된다.
addressSecondary 얘는 사용자가 직접 입력하는 부분이기 때문에 빈칸으로 설정한다.
주소찾기에서 선택한 결과 그대로 반영이 되는 것을 확인 할 수 있다.
Postal(우편번호)이랑 Primary(주소)만 정규화하자.
const addressPostalRegex = new RegExp('^([0-9]{5})$');
const addressPrimaryRegex = new RegExp('^(?=.{8,100})([가-힣][0-9가-힣]*[0-9])$');
const addressPostalInput = registerForm['addressPostal'];
const addressPrimaryInput = registerForm['addressPrimary'];
if(!addressPostalRegex.test(addressPostalInput.value) || !addressPrimaryRegex.test(addressPrimaryInput.value)) {
alert('주소 찾기를 통해 주소를 입력해주세요');
addressSearchButton.focus();
e.preventDefault();
return false;
}
주소를 입력하지 않으면 alert에 썼던 구문이 나타난다.
-> dependency추가
회원가입 할 때 인증메일을 이메일로 전송하고 이메일을 클릭해서 회원가입 절차가 완료될 수 있게 하자.
dependency추가 = spring-boot-starter-mail
gmail로 앱 비밀번호를 가져오자.
썸네일 -> 구글계정관리 -> 보안 -> 2단계인증에서 추가 -> 앱 비밀번호
생성을 누르게 되면 앱비밀번호가 나올 것이다. 이거는 다시는 뜨지 않는 걸로 알고 있으니 잘 간직할 수 있도록하자..
-> application.properties 추가
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=본인 구글 계정
spring.mail.password=본인 비밀번호
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.outh=true
-> UserCotroller
@RequestMapping(value = "register", method = RequestMethod.POST)
public ModelAndView postRegister(
UserRegisterVo userRegisterVo,
ModelAndView modelAndView
) {
return this.getRegister(modelAndView); // 위 modelAndView를 전부 가져온다.
-> enums 패키지 : UsesrRegisterResult 추가
DUPLICATE_EMAIL,
DUPLICATE_NICKNAME,
FAILURE,
ILLEGAL,
SUCCESS
ILLEGAL : controller가 최종적으로 봤을 때 result가 ILLEGAL이면 차단대상이다. (좋지 않은 방법으로 접근)
-> vos 패키지 : UserRegisterVo 추가
public class UserRegisterVo {
}
-> interfaces 패키지 : IResult 추가
public interface IResult<T extends Enum<?>> {
T getResult();
void setResult(T t);
}
IResult라는 인터페이스는 제네릭이다.
T는 enum이여야한다.
T를 반환하는 getResult와 매가변수를 t로 가지는 반환값이 없는 setResult메서드를 만든다.
-> UserRegisterVo 추가
public class UserRegisterVo extends UserEntity implements IResult<UserRegisterResult> {
private UserRegisterResult result;
@Override
public UserRegisterResult getResult() {
return null;
}
@Override
public void setResult(UserRegisterResult userRegisterResult) {
}
}
UserResisterVo은 IResult를 구현하는데 제네릭으로서 UserRegisterResult을 가진다. UserEntity를 상속받는다.
private UserRegisterResult result; + getter,setter을 하게 되면
자동으로 override가 되고 오류도 사라지게 된다.
=> 평범한 getter,setter에 override가 붙게 된 이유는 IResult라는 인터페이스 때문이다. IResult 제네릭에 UserRegisterResult라는 열거형이 들어갔으니 T자리에 전부 UserRegisterResult 들어간 것이다.
전달받은 값들에 대해서 정규화를 한다. 정규식을 추가하자.
정규식을 UserService에 정적인 메서드를 만들어서 모으자. ( 위 아래 다 사용하지 말고 )
-> UserService
public static boolean checkAddressPostal(String input) {
return input != null && input.matches("^([0-9]{5})$");
}
public static boolean checkAddressPrimary(String input) {
return input != null && input.matches("^(?=.{8,100})([가-힣][0-9가-힣]*[0-9])$");
}
public static boolean checkEmail(String input) {
return input != null && input.matches("^(?=.{10,100}$)([0-9a-z][0-9a-z_]*[0-9a-z])@([0-9a-z][0-9a-z\\-]*[0-9a-z]\\.)?([0-9a-z][0-9a-z\\-]*[0-9a-z])\\.([a-z]{2,15})(\\.[a-z]{2})?$");
}
public static boolean checkNickname(String input) {
return input != null && input.matches("^([0-9a-zA-Z가-힣]{2,10})$");
}
private static boolean checkPassword(String input) {
return input != null && input.matches("^([0-9a-zA-Z`~!@#$%^&*()\\-_=+\\[{\\]}\\\\|;:'\",<.>/?]{8,100})$");
}
사용했던 정규식 붙어넣어서 만든다.
public int getCountByEmail(UserEntity userEntity) {
if(!UserService.checkEmail(userEntity.getEmail())) {
return -1;
//자바에서는 문자열 객체의 mathches 메서드를 이용한다.(<->자바스크립트)
}
return this.userMapper.selectCountByEmail(userEntity);
}
public int getCountByNickname(UserEntity userEntity) {
// 정적메소드 접근할 때는 앞에 클래스부터 접근을 해주는 편. 아닐 때에는 this.~ 으로 접근.
if(!UserService.checkNickname(userEntity.getNickname())) {
return -1;
}
return this.userMapper.selectCountByNickname(userEntity);
}
-> register을 하기위한 정규화를 해주자.
public void register(UserRegisterVo userRegisterVo) { if(!UserService.checkEmail(userRegisterVo.getEmail()) || !UserService.checkPassword(userRegisterVo.getPassword()) || !UserService.checkNickname(userRegisterVo.getNickname()) || !UserService.checkAddressPostal(userRegisterVo.getAddressPostal()) || !UserService.checkAddressPrimary(userRegisterVo.getAddressPrimary())) { userRegisterVo.setResult(UserRegisterResult.ILLEGAL); return; } if(this.userMapper.selectCountByEmail(userRegisterVo) > 0) { userRegisterVo.setResult(UserRegisterResult.DUPLICATE_EMAIL); return; } if(this.userMapper.selectCountByNickname(userRegisterVo) > 0) { userRegisterVo.setResult(UserRegisterResult.DUPLICATE_NICKNAME); return; } }
- 값은 이미 자바스크립트에서 정규화가 되서 온다. 이 정규식에 위배되는 값을 입력을 했다면 애초에 요청이 들어오지 않았을 것이다.
자바스크립트에서 e.preventDefault(); return false; 로 인해 요청이 들어와도 실제로 요청이 나가지 않는다.
근데 여기서 정규화가 안됬다는 건 자바스크립트를 우회했다는 것이다. = 차단 대상!
- if문 1번 : 자바스크립트의 정규식을 우회해서 들어온 차단 대상을 막는다.
- if문 2번 : 이메일의 갯수를 셀껀데 둘다 userEntity 타입을 받고있다.
부모 타입에는 자식객체가 들어갈 수 있기 때문에 UserRegisterVo를 집어넣을 수 있다. 돌아온 이메일 갯수가 0보다 DUPLICATE_EMAIL(중복) 으로 빠져나갈 수 있게 한다.- if문 3번 : 2번이랑 동일. DUPLICATE_NICKNAME 으로 빠져나간다.
-> IUserMapper interface
int insert(UserEntity userEntity);
int타입을 반환하고 userEntity타입을 가지는 insert메서드 생성.
-> UserMapper.xml
<insert id="insert"
parameterType="dev.jwkim.bbsbasic.entities.UserEntity">
INSERT INTO `spring3`.`users` (`email`,`password`,`nickname`,`address_postal`, `address_primary`,`address_secondary`)
VALUES (#{email}, #{password}, #{nickname}, #{address_postal},#{address_primary},#{address_secondary})
</insert>
전달받는 값이 UserEntity로 밑에 아이랑 동일한데 얘는 resultType을 적어주지 않는다. 밑에서 얘기한 resultType은 SELECT해서 반환되는 값을 명시하는 것이다.
여기서 반환하는 이 _int는 resultType으로 명시하는 것이아닌 insert결과값이 알아서 int로 나간다. insert의 결과값이 int 외에 다른거 알 수 없다.
reulsType을 명시하지 않는다. SELECT만 resultType 명시.
<select id="selectCountByEmail"
parameterType="dev.jwkim.bbsbasic.entities.UserEntity"
resultType="_int">
SELECT COUNT(0) AS `count`
FROM `spring3`.`users`
WHERE `email` = #{email}
</select>
insert 하기전에 password를 hasing 해줘야한다. Hashing위해 CryptoUtil 생성.
SHA 알고리즘으로 Hashing하면 아래와 같이 작성 된다. 아직 이해는 못한다..
-> utils 패키지 생성 CryptoUtil클래스 생성
public final class CryptoUtil {
/**
* Enumeration for hashing algorithms.
*/
public enum Hash{
SHA256("SHA-256","%064x"),
SHA512("SHA-512","%0128x");
public final String algorithm;
private final String format;
Hash(String algorithm, String format){
this.algorithm = algorithm;
this.format = format;
}
}
/**
*
* <i>Null value</i> would be used for the <b>fallback</b> parameter.
* <br>
* <i>StandardCharset.UTF-8</i>> would be used for the <b>charset</b> parameter.
* @return Hashed String of the <b>input</b> parameter.
*/
public static String hash(Hash hash, String input) {
return CryptoUtil.hash(hash, input, null, StandardCharsets.UTF_8);
}
/**
*
* <i>StandardCharset.UTF-8</i> would be used
* @return Hashed String of the <b>input</b> parameter
*/
public static String hash(Hash hash, String input, String fallback) {
return CryptoUtil.hash(hash, input, fallback, StandardCharsets.UTF_8);
}
/**
* @return Hashed String of the <b>input</b> parameter.
*/
public static String hash(Hash hash, String input, String fallback, Charset charset) {
try{
MessageDigest md = MessageDigest.getInstance(hash.algorithm);
md.reset();
md.update(input.getBytes(charset));
return String.format(hash.format, new BigInteger(1, md.digest()));
} catch (NoSuchAlgorithmException ignored) {
return fallback;
}
}
private CryptoUtil() {
}
}
사용자가 추가된 걸 볼 수 있다.
회원가입을 하면 default false이기 때문에 email verifiedflag 가 false로 들어가게 된다.
user의 이메일과 비밀번호가 맞다고 해도 verifiedflag가 false일 때에는 아직 이메일 인증이 완료되지않았다라고 하고 로그인 시켜주지 않는다.
register에서 SUCCESS를 반환했다고 해도 email vertifiedflag를 true로 지정하기 위해서는 이메일로 실제 전송된 링크를 클릭해줘야하는데 전송 된 링크가 방금 가입한 사람의 email verifiedfalg를 true로 지정하기 위한 링크인지 어떻게 구분할까..?
일단 추후에 이메일인증을 하기 위한 주소를 /user/verify-email? 로 들어가질 것이다.(a태그 써서 메일을 보낸다. 그 링크를 클릭하면 /user/verify-email? 이 주소로 이동된다.)
그런데 이것가지고 이사람을 위한 이메일인증용 링크였는지 알 방법이 없다.
회원가입하는 모든사람에게 이 주소를 보낼껀데 누구의 것인지 확인을 할 수 없다.
어떻게 하면 우리가 생성하는 경로를 이 사람 전용으로 할 것인지 생각을 해보자..