Spring + Security + JWT + Redis를 통한 회원인증/허가 구현 (4) - 회원가입 인증 이메일, 아이디 비밀번호 찾기

600g (Kim Dong Geun)·2020년 7월 23일
10

목록보기를 누르시면 이전글을 보실 수 있습니다!

회원가입 인증 이메일

처음에 사용자 Model 을 만들때 회원의 Role을 저장할 수 있는 곳을 부여했다.
실제로 UserRole은 Java Enum을 이용해서 관리하고있다.

public enum UserRole {
 ROLE_NOT_PERMITTED, ROLE_USER, ROLE_MANAGER, ROLE_ADMIN
}

스프링 Security는 Default로 Role에 대한 prefix값이 존재한다.
그래서 모든 ENUM 값 안에 ROLE_ 을 붙여준다.

유저 회원가입 인증 메일의 플로우는 다음과 같다.
1. 사용자가 회원가입을한다. (회원가입을 한 User의 권한은 NOT_PERMITTED일 것이다.)
2. 회원가입 완료와 동시에 사용자에게 회원인증 메일을 보낸다.
3. 회원인증 메일에는 회원인증을 할 수 있는 링크를 보낸다.
4. 사용자는 메일에 제시된 링크를 클릭했을시, 사용자의 role을 not permitted에서 user로 변경해준다.

물론 회원인증이 되지 않고 서버에 대한 서비스를 요구했을시, 클라이언트 측은 회원인증을 요구하는 페이지로 이동을 하고 서버는 접근 금지를 해야 할 것이다. 이건 추후에 따로 포스팅 하도록 하겠다.

먼저 스프링에서 SMTP 를 사용할 수 있도록 해야한다.

  • pom.xml
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

dependency를 추가해준다.

  • application.properties
## Email Send Configuration_SMTP
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username= 유저 id
spring.mail.password= 유저 비밀번호
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true

를 추가해줌으로써 smtp를 사용할 수 있다.

Spring boot starter mail의 경우 JavaMailSender 클래스를 가져와서 사용한다.
우리는 email에 관련된 서비스(혹은 Helper)를 하나 만들겠다.

  • EmailServiceImpl.java
@Service
public class EmailServiceImpl implements EmailService {
    @Autowired
    private JavaMailSender emailSender;

    @Override
    public void sendMail(String to,String sub, String text){
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(to);
        message.setSubject(sub);
        message.setText(text);
        emailSender.send(message);
    }
}

이제 메일을 보낼 준비는 완성됐고, 회원가입시 동봉된 링크를 만들차례이다.
링크의 경우, 그 url값을 고정시키지 않고 유동적으로 변경함으로써, 제시된 경로가 아니면 함부로 회원의 권한을 변경할 수 없도록 해야 한다.
우린 여기서 또 redis를 사용할 것이다. 회원가입 인증 메일을 '요청'한 사용자에 한해 몇 분 동안만 그 링크를 살아있게 함으로써 보안적인 측면을 강화하는 것이다.

레디스에 들어가는 데이터 값은 다음과 같을 것이다.

Keyvalue
uuid(or RandomString)userId

(본인은 Uuid값을 사용했다.)

그것을 구현한 부분을 AuthService.java 추가하도록 하겠다.

    @Override
    public void sendVerificationMail(Member member) throws NotFoundException {
        String VERIFICATION_LINK = "http://localhost:8080/user/verify/";
        if(member==null) throw new NotFoundException("멤버가 조회되지 않음");
        UUID uuid = UUID.randomUUID();
        redisUtil.setDataExpire(uuid.toString(),member.getUsername(), 60 * 30L);
        emailService.sendMail(member.getEmail(),"[김동근 스프링] 회원가입 인증메일입니다.",VERIFICATION_LINK+uuid.toString());
    }

다음은 요청된 키값을 받았을 시 redis에 key값이 있는지 유효성을 확인한 뒤 있을경우,
Key값에 존재하는 Value값의 유저의 권한을 변경하도록 하는 코드이다.
이역시 AuthService.java 내부에 존재한다.

    @Override
    public void verifyEmail(String key) throws NotFoundException {
        String memberId = redisUtil.getData(key);
        Member member = memberRepository.findByUsername(memberId);
        if(member==null) throw new NotFoundException("멤버가 조회되지않음");
        modifyUserRole(member,UserRole.ROLE_USER);
        redisUtil.deleteData(key);
    }
    
    @Override
    public void modifyUserRole(Member member, UserRole userRole){
            member.setRole(userRole);
            memberRepository.save(member);
    }

이제 컨트롤러 부분을 추가 시켜 주도록하자.

    @PostMapping("/verify")
    public Response verify(@RequestBody RequestVerifyEmail requestVerifyEmail, HttpServletRequest req, HttpServletResponse res) {
        Response response;
        try {
            Member member = authService.findByUsername(requestVerifyEmail.getUsername());
            authService.sendVerificationMail(member);
            response = new Response("success", "성공적으로 인증메일을 보냈습니다.", null);
        } catch (Exception exception) {
            response = new Response("error", "인증메일을 보내는데 문제가 발생했습니다.", exception);
        }
        return response;
    }

    @GetMapping("/verify/{key}")
    public Response getVerify(@PathVariable String key) {
        Response response;
        try {
            authService.verifyEmail(key);
            response = new Response("success", "성공적으로 인증메일을 확인했습니다.", null);

        } catch (Exception e) {
            response = new Response("error", "인증메일을 확인하는데 실패했습니다.", null);
        }
        return response;
    }

비밀번호 찾기

사실 비밀번호 찾기도 회원가입 인증 메일과 같은 플로우로 진행된다.
1. 사용자의 id와 이메일 정보가 맞으면 메일을 보낸다.
2. 메일에는 비밀번호를 변경할 수 있는 페이지의 주소가 담겨있다.
(물론 메일 링크는 지정된 시간내에만 살아있어야 한다.)
3. 지정된 페이지로부터 사용자의 비밀번호 변경요청을 받는다.
4. 비밀번호를 변경한다.

사실 1의 과정을 제외하면, 회원가입 인증메일과 진짜 별반 다를 바가 없다.
그래서 소스코드만 첨부하도록 하겠다.(귀찮)

  • AuthServiceImpl.java
 @Override
    public boolean isPasswordUuidValidate(String key){
        String memberId = redisUtil.getData(key);
        return !memberId.equals("");
    }

    @Override
    public void changePassword(Member member,String password) throws NotFoundException{
        if(member == null) throw new NotFoundException("changePassword(),멤버가 조회되지 않음");
        String salt = saltUtil.genSalt();
        member.setSalt(new Salt(salt));
        member.setPassword(saltUtil.encodePassword(salt,password));
        memberRepository.save(member);
    }


    @Override
    public void requestChangePassword(Member member) throws NotFoundException{
        String CHANGE_PASSWORD_LINK = "http://localhost:8080/user/password/";
        if(member == null) throw new NotFoundException("멤버가 조회되지 않음.");
        String key = REDIS_CHANGE_PASSWORD_PREFIX+UUID.randomUUID();
        redisUtil.setDataExpire(key,member.getUsername(),60 * 30L);
        emailService.sendMail(member.getEmail(),"[김동근 스프링] 사용자 비밀번호 안내 메일",CHANGE_PASSWORD_LINK+key);
    }

다음은 Controller이다.

  • MemberController.java
@GetMapping("/password/{key}")
    public Response isPasswordUUIdValidate(@PathVariable String key) {
        Response response;
        try {
            if (authService.isPasswordUuidValidate(key))
                response = new Response("success", "정상적인 접근입니다.", null);
            else
                response = new Response("error", "유효하지 않은 Key값입니다.", null);
        } catch (Exception e) {
            response = new Response("error", "유효하지 않은 key값입니다.", null);
        }
        return response;
    }

    @PostMapping("/password")
    public Response requestChangePassword(@RequestBody RequestChangePassword1 requestChangePassword1) {
        Response response;
        try {
            Member member = authService.findByUsername(requestChangePassword1.getUsername());
            if (!member.getEmail().equals(requestChangePassword1.getEmail())) throw new NoSuchFieldException("");
            authService.requestChangePassword(member);
            response = new Response("success", "성공적으로 사용자의 비밀번호 변경요청을 수행했습니다.", null);
        } catch (NoSuchFieldException e) {
            response = new Response("error", "사용자 정보를 조회할 수 없습니다.", null);
        } catch (Exception e) {
            response = new Response("error", "비밀번호 변경 요청을 할 수 없습니다.", null);
        }
        return response;
    }

    @PutMapping("/password")
    public Response changePassword(@RequestBody RequestChangePassword2 requestChangePassword2) {
        Response response;
        try{
            Member member = authService.findByUsername(requestChangePassword2.getUsername());
            authService.changePassword(member,requestChangePassword2.getPassword());
            response = new Response("success","성공적으로 사용자의 비밀번호를 변경했습니다.",null);
        }catch(Exception e){
            response = new Response("error","사용자의 비밀번호를 변경할 수 없었습니다.",null);
        }
        return response;

    }
profile
수동적인 과신과 행운이 아닌, 능동적인 노력과 치열함

5개의 댓글

comment-user-thumbnail
2020년 7월 26일

안녕하세요!!!
아직 자바 학원을 다니고있는 햇병아리입니다...
3개월됐는데
현재 프로젝트를 진행중인데 이제 16일이후에 프로젝트가 끝이나는대요..
5명이서 작업중입니다..
홈페이지 구성은 거의다 끝이났는데
저희가 기능이 실시간 스트리밍(클라이언트1이 클라이언트2에게만 영상을 송출하는방식)
채팅(클라이언트1 클라이언트2 서로 채팅가능)
이렇게 두개의 큰 기능이 남았습니다 ㅠㅠ..
근데 node.js로 채팅이랑 실시간영상(webRTC)을 구현해봤는데

저희는 서블릿과 톰캣으로 프로젝트를 진행하는데..
nodejs같은 경우에는 톰캣과 동등한 서버라고해서요..

실제로 node.js로 구현한 채팅과 실시간 스트리밍같은 경우에는
이클립스에서 tomcat으로 실행하지않고
node.js서버를 열어주고나서 node.js의 포트넘버 3000인 localhost:3000을 브라우저에 입력해야 정상적으로 되더라구요

그치만 저희는 프로젝트를 우클릭눌러서 현재 저희 톰캣 포트넘버인 8787로 열어야하는데..
이럴경우에는 어떻게 구현을 접근해야 할까요.. 너무 걱정이 큽니다 ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ

처음에는 클라이언트1이 클라이언트2의 아이디를 클릭해서 채팅 및 실시간 스트리밍을 요청하면
클라이언트2에게 요청alert이 뜨게해서 동의하면
클라이언트1의 영상이 클라이언트2에게 보이게되고 채팅도 서로할수잇게 하려했습니다.

하지만 너무나도 어려울거같아서
차라리 채팅 및 영상 방을 만들어서(클라이언트1이 생성)
그리고 클라이언트2가 그방을 들어가는 방법으로 잡으려고하는데 어떤 방식이 더 나을까요?

그리고 클라이언트1 ,2,3,4가 있으면
1과 2가 영상및 채팅을하고있으면
3과 4도 동시에 둘이서 영상 및 채팅을 하게 만들고싶습니다..

조언주시면 정말 감사하겠습니다 ㅠ

1개의 답글
comment-user-thumbnail
2021년 10월 12일

감사합니다.

답글 달기
comment-user-thumbnail
2022년 3월 23일

API서버 인증서버 구현하는데에 많은 레퍼런스가 되었습니다. 감사합니다^^

답글 달기