[API Gateway + Refresh JWT 인증서버 구축하기] Spring boot + Redis 이메일 인증

Sieun Sim·2020년 5월 31일
2

서버개발캠프

목록 보기
16/21

전체 흐름

  1. 유저 회원가입 요청

    유저가 입력한 회원정보 redis에 저장

  2. 이메일 전송

    username과 username + salt를 SHA512 해싱한 인증 토큰이 포함된 인증 url을 이메일로 전송.redis에 토큰 저장하면서 만료 기간도 정함

  3. 유저가 URL 클릭

    담당 컨트롤러에서 해당 GET 요청의 파라미터들이 redis에 있는 정보와 일치하는지 확인해서 Redis에 있는 회원정보를 mySQL에 저장

1. 유저 회원가입 요청

유저에게 받은 회원 정보를 Redis에 임시로 저장해둔다. expire time을 설정해 자동으로 사라지게 할 수 있다.

Redis는 key-value 형태이므로 Member 객체를 그대로 저장하기 위해 Serialize 과정이 필요하다. json String 형태로 저장한다.

Redis Configuration

@Configuration
public class RedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory();
        return lettuceConnectionFactory;
    }


    @Bean
    public RedisTemplate<String, Object> memberRedisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        //객체를 json 형태로 깨지지 않고 받기 위한 직렬화 작업
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Member.class));
        return redisTemplate;
    }
}

Controller

@Autowired
RedisTemplate<String, Object> memberRedisTemplate;
<...>
ValueOperations<String, Object> vop = memberRedisTemplate.opsForValue();
vop.set("toverify-"+username, member);

객체를 저장할 때는 위와 같이 configuration에서 객체 맞춤형 Bean으로 등록한 템플릿을 가져와서 사용하면 되고, 단순 string-string의 경우는 원래 있는 템플릿을 쓰면 된다.

@Autowired
StringRedisTemplate stringRedisTemplate;
<...>
stringRedisTemplate.opsForValue().set(redisKey, hash);

Redis에 table처럼 저장하기

RDBMS에서 table을 나누어 사용했듯이, refresh token / email 인증 token / 비밀번호 변경 token 등을 나누어 저장하고 싶었다. database를 나눠쓰는 방법도 있지만 비효율적이고 복잡했다.

다연이가 알려준 방법

key 네이밍을 table-column 식으로 하면 해결된다. 예를 들어

email-sieun, refresh-sieun, pw-sieun 처럼 유저네임과 원하는 정보를 같이 key에 저장해버리면 중복될 일도 없고, 정보를 분류별로 보고싶으면 email, refresh로 서치하면 된다.

2. 이메일 전송

redis에 email-username을 key로 토큰을 저장한다. 랜덤값은 혹시라도 겹치면 안되니까 username을 해싱했다. 토큰은 유효성을 검사하기 위한 용도가 아니라 username으로 찾아 존재유무만 검사할것이기 때문에 단방향인 SHA512을 사용했다. salt는 난수를 발생시켜 사용했다.

토큰 생성

public String getSHA512Token(String passwordToHash, String salt){
    String generatedPassword = null;
    try {
        MessageDigest md = MessageDigest.getInstance("SHA-512");
        md.update(salt.getBytes(StandardCharsets.UTF_8));
        byte[] bytes = md.digest(passwordToHash.getBytes(StandardCharsets.UTF_8));
        StringBuilder sb = new StringBuilder();
        for(int i=0; i< bytes.length ;i++){
            sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
        }
        generatedPassword = sb.toString();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    }
    return generatedPassword;
}

메일은 구글 메일을 사용했고 [application.properties](http://application.properties) 에 설정을 저장했다.

application.properties

spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=xxx
spring.mail.password=xxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true

메일은 대표적으로 simplemessage 도 있었지만 html string을 보내 적용시키려면 MimeMessage를 이용해야 한다. redis에 이메일 토큰을 저장해주고, RedisTemplate.expire() 를 사용해 만료 기간도 정해준다. url은 "http://localhost:8080" + "/auth/verify?username="+ username +"&key="+hash 형태가 된다.

MailSender

@Autowired
public JavaMailSenderImpl javaMailSender;

@Async
public void sendMail(String email, String username, int type) throws Exception {
    MimeMessage message = javaMailSender.createMimeMessage();
    message.addRecipient(Message.RecipientType.TO, new InternetAddress(email));
    message.setSubject("[본인인증] quadcore 이메일 인증");
    int rand = new Random().nextInt(999999);
    String formatted = String.format("%06d",rand);
    String hash = getSHA512Token(username, formatted);
    String redisKey = null;
    String htmlStr = null;
    if (type == 0) {
        redisKey = "email-" + username;
        stringRedisTemplate.opsForValue().set(redisKey, hash);
        htmlStr = "안녕하세요 " + username + "님. 인증하기를 눌러주세요"
                + "<a href='http://localhost:8080" + "/auth/verify?username="+ username +"&key="+hash+"'>인증하기</a></p>";
    } else if (type == 1) {
        redisKey = "changepw-" + username;
        stringRedisTemplate.opsForValue().set(redisKey, hash);
        htmlStr ="안녕하세요 " + username + "님. 비밀번호 변경하를 눌러주세요"
                + "<a href='http://localhost:8080" + "/auth/vfpwemail?username="+ username +"&key="+hash+"'>비밀번호 변경하기</a></p>";
    }
    stringRedisTemplate.expire(redisKey, 60*24*1000, TimeUnit.MILLISECONDS); // for one day

    message.setText(htmlStr, "UTF-8", "html");
    javaMailSender.send(message);
}

3. 유저가 URL 클릭

/auth/verify controller에서는 username과 token을 GET 파라미터로 받아 처리한다. redis에서 토큰이 확인되면 redis에서 유저 객체와 email token을 지워주고 mysql 유저 디비로 넣어주었다.

Verifying Controller

@GetMapping(path="/auth/verify")
public Map<String, Integer> verifyEmail(@RequestParam("username") String username, @RequestParam("key") String hash) {
    Map<String, Integer> m = new HashMap<>();

    logger.info("redis get : " + stringRedisTemplate.opsForValue().get("email-"+username));
    logger.info("hash : " + hash);
    if (stringRedisTemplate.opsForValue().get("email-"+username).equals(hash)) {
        ValueOperations<String, Object> memvop = memberRedisTemplate.opsForValue();
        Member member = (Member) memvop.get("toverify-"+username);
        memberRepository.save(member);
        stringRedisTemplate.delete("email-"+username);
        memberRedisTemplate.delete("toverify-"+username);
        m.put("errorCode", 10);
    } else m.put("errorCode", 70);
    return m;
}

비밀번호 변경도 거의 비슷하게 토큰을 따로 만들어 이메일로 url을 보내주고, 유저가 url을 클릭하면 비밀번호 변경 페이지로 이동하도록 한다.

0개의 댓글