bbs-basic-4, 이메일 인증, 난수화

김지원·2022년 6월 16일
0

WebDevelop

목록 보기
5/21

메일을 보내기 위한 로직

-> 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);
        }
  • 데이터 난수화(Randomization)
    일부러 잘못된 데이터를 저장해 고객의 개인 정보를 보호함과 동시에,
    기업에게는 신뢰성 있는 통계 데이터를 제공할 수 있는 방법 ( 계속 연구 중인 방안 )

-> 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);

-> emailVerificationTemplatehtml 추가

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);

    }
  • 로그인이기 때문에 이메일과 패스워드에 대해서만 해준다.
  • 선택하는 열의 별명을 파스칼케이싱한 다음에 앞에 set붙이고 메서드 호출할 수 있으면 UserEntity 객체화해서 값을 집어넣음으로써 이렇게만 해도 객체화가 알아서 잘된다.

-> 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>
  • URL 객체화하는데 location.href를 집어넣을 것이다.
  • 이 페이지가 표시 될 주소는 prev값이 있을 것이다 .
  • searchParams는 ??부터 뒤에 내용을 담당한다.

    ?? 앞에 온 값이 null이거나 undifined이면 ??뒤에 값을 대신해서 쓰겠다는 의미이다.

    // http://127.0.0.1:8080/user/login?prev=/user/register
    => prev 값을 javascript로 받아오는 것 뿐이다.
    redirect를 하면 thymelef에 잇는 tempalte를 못쓰고 값을 전달 못하니깐 redirect를 못시켰기 때문이다.

0개의 댓글