https://offbyone.tistory.com/167
https://m.blog.naver.com/jny9708/221773002779
항상 그렇듯 참고한 글들이다.
먼저 회원가입 관련해서 메일 인증을 하기 위해 총 3가지 정도를 구현하고자 한다.
1) 아이디를 잊어버렸을 경우 아이디를 가입한 이메일로 전송
2) 비밀번호를 잊어버렸을 경우 임시 비밀번호 발급 및 임시 비밀번호를 이메일로 전송
3) 비밀번호 변경
4) 회원가입 시 메일 인증
1. 기본적인 설정
일단 나는 gmail로 메일을 보내려 한다. 그러려면 2가지 설정을 해야 한다.
- https://myaccount.google.com/
- https://myaccount.google.com/u/1/signinoptions/two-step-verification/enroll-welcome
접속 및 시작하기, 2단계 인증 설정(문자나 메일 인증)
- https://security.google.com/settings/security/apppasswords?pli=1
접속 및 앱(메일), 기기 선택 클릭

그러면 이런 것이 나온다. 비밀번호를 메모하자.
- https://support.google.com/accounts/answer/6010255?hl=ko
위 링크에서 보안 수준이 낮음으로 설정을 변경하자. 그러면 설정은 끝났다.
2. 코드
항상 그렇듯 build.gradle 설정, application.properties 설정을 하자.
build.gradle
dependencies{ implementation 'commons-io:commons-io:2.6' implementation group: 'com.sun.mail', name: 'javax.mail', version: '1.6.2' implementation group: 'org.springframework', name: 'spring-context-support', version: '5.3.10' }
이렇게 3줄을 추가하자.
application.properties
spring.mail.host=smtp.gmail.com spring.mail.port=587 #gmail이기 때문에 587 사용, 다른 메일인 경우에는 다른 것을 사용. spring.mail.username={메일을 전송할 메일. google에서 사용 설정한 메일을 작성} spring.mail.password=rzhtznbalzgvghrw spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.auth=true
smtp를 이용해 메일을 전송하기 때문에 위와 같은 설정을 해 주어야 한다. 추가적으로, local에서는 localhost로 인증 메일을 보내면 되지만 서버 인증 메일은 서버 ip를 작성해야 하기 때문에 profile에 맞춰서 잘 적자.
spring.config.activate.on-profile=common spring.mail.host=smtp.gmail.com spring.mail.port=587 spring.mail.username=swmteam12@gmail.com spring.mail.password=rzhtznbalzgvghrw spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.auth=true #--- spring.config.activate.on-profile=localdb ... mail.verification.link=http://localhost:8080/member/verify/ #--- spring.config.activate.on-profile=cidb ... mail.verification.link=http://34.132.212.36:8080/member/verify/ #--- spring.profiles.group.local=localdb,common #--- spring.profiles.group.ci=cidb,common #--- spring.profiles.active=ci
1) 아이디를 잊어버렸을 경우 아이디를 가입한 이메일로 전송
flow는 아래와 같다.
- 프론트에서 회원가입 시 등록한 이메일을 post로 전송
- 해당 mail에 해당하는 사용자를 찾아서 member id를 가져옴
- 찾은 member id를 메일로 전송
2) 비밀번호 변경
flow는 아래와 같다.
- 프론트에서 로그인 된 상태에서 기존 비밀번호와 새 비밀번호를 post로 전송
- 기존 비밀번호가 맞는 경우 새 비밀번호를 재설정
3) 비밀번호를 잊어버렸을 경우 임시 비밀번호 발급 및 임시 비밀번호를 이메일로 전송
flow는 아래와 같다.
- 프론트에서 회원가입 시 등록한 이메일을 post로 전송
- 임의로 비밀번호 발급 및 해당 사용자의 비밀번호를 임시 비밀번호로 변경
- 임시 비밀번호를 이메일로 전송
4) 회원가입 시 메일 인증
- 회원가입 시 사용자 역할을 ROLE_NOT_PERMITTED로 두고
- 추가적으로 인증 링크가 담긴 인증 메일 전송
- 인증 링크가 클릭되었을 경우 해당 사용자의 role을 ROLE_USER로 변경
일단 제일 먼저 메일을 보내는 함수를 만들어 보자.
EmailService.java
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.stereotype.Service; import lombok.AllArgsConstructor; @Service @AllArgsConstructor public class EmailService{ @Autowired private JavaMailSenderImpl mailSender; public void sendMail(String to, String title, String text){ SimpleMailMessage simpleMailMessage = new SimpleMailMessage(); simpleMailMessage.setTo(to); simpleMailMessage.setSubject(title); simpleMailMessage.setText(text); mailSender.send(simpleMailMessage); } public String idTitle(){ return "비즈킥스 아이디 찾기"; } public String idText(String memberId, String name){ String msg = ""; msg += name + "님의 아아디는 다음과 같습니다.\n"; msg += "아이디 : " + memberId; return msg; } public String tempPasswordTItle(){ return "비즈킥스 임시 비밀번호"; } public String tempPasswordText(String name, String tempPassword){ String msg = ""; msg += name + "님의 임시 비밀번호입니다. 비밀번호를 변경하여 사용하세요.\n"; msg += "임시 비밀번호 : " + tempPassword; return msg; } public String verifyEmailTitle(){ return "비즈킥스 인증 메일"; } public String verifyEmailText(String link){ String msg = ""; msg += "접속 링크 : " + link; return msg; } }
나는 텍스트를 한 줄에 적는 게 싫어서 이렇게 뺐다. 그리고 디자인은 할 줄 모르기 대문에 간단하게 적었다.
sendMail 함수에서 누구에게 보낼지, 제목은 뭔지, 내용은 뭔지 담아서 보내줄 것이다.
1) 아이디를 잊어버렸을 경우 아이디를 가입한 이메일로 전송
- 프론트에서 회원가입 시 등록한 이메일을 post로 전송
EmailDto.java
@Data @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor public class EmailDto { private String email; }
AuthController.java
@PostMapping(value="/member/find/id") public ResponseEntity<Object> findMemberId(@RequestBody EmailDto emailDto) { String email = emailDto.getEmail(); authService.sendIdEmail(email); JSONObject jsonObject = new JSONObject(); jsonObject.put("msg", "Success"); log.info("아이디 찾기 요청 : " + email); return new ResponseEntity<Object>(jsonObject.toString(), HttpStatus.OK); }
AuthService.java
public void sendIdEmail(String email){ Member member = memberRepository.getMemberByEmail(email); String text = emailService.idText(member.getMemberId(), member.getName()); String title = emailService.idTitle(); emailService.sendMail(member.getEmail(), title, text); }
- 해당 mail에 해당하는 사용자를 찾아서 member id를 가져옴
- 찾은 member id를 메일로 전송
2) 비밀번호 변경
flow는 아래와 같다.
- 프론트에서 로그인 된 상태에서 기존 비밀번호와 새 비밀번호를 post로 전송
AuthController.java
@PostMapping(value="/member/modify/password") public ResponseEntity<Object> changeMemberPassword(@RequestBody PasswordDto passwordDto) { Member member = memberService.getCurrentMemberInfo(); authService.changeMemberPassword(member, passwordDto.getOld_password(), passwordDto.getNew_password()); JSONObject jsonObject = new JSONObject(); jsonObject.put("msg", "Success"); log.info("사용자 {} 비밀번호 수정 요청", member.getMemberId()); return new ResponseEntity<Object>(jsonObject.toString(), HttpStatus.OK); }
memberService.getCurrentMemberInfo()는 현재 로그인 된 사용자의 정보를 가져오는 함수이다.
AuthService.java
public void changeMemberPassword(Member member, String oldPassword, String newPassword){ if(!this.passwordEncoder.matches(oldPassword, member.getPassword())){ throw new CustomException(ErrorCode.PASSWORD_NOT_VALID); } member.setPassword(passwordEncoder.encode(newPassword)); memberRepository.save(member); }
AuthService 내에 passwordEncoder를 미리 선언해 두었다. 이후 해당 encoder에 oldPassword를 넣었는데 encoding된 값과 동일하지 않다 -> 그러면 old password가 틀린 비밀번호다. 이 경우에는 password가 틀렸다는 오류를 발생시키고, 아니라면 새 비밀번호를 설정하고 memberRepository에 update해 준다.
3) 비밀번호를 잊어버렸을 경우 임시 비밀번호 발급 및 임시 비밀번호를 이메일로 전송
AuthController.java
@PostMapping(value="/member/find/password") public ResponseEntity<Object> findMemberPassword(@RequestBody EmailDto emailDto) { authService.reissuePassword(emailDto.getEmail()); JSONObject jsonObject = new JSONObject(); jsonObject.put("msg", "Success"); log.info("비밀번호 찾기 요청 : " + emailDto.getEmail()); return new ResponseEntity<Object>(jsonObject.toString(), HttpStatus.OK); }
프론트에서 회원가입 시 등록한 이메일을 post로 전송
AuthService.java
public void reissuePassword(String email){ Member member = memberRepository.getMemberByEmail(email); String tempPassword = ""; for (int i = 0; i < 12; i++) { tempPassword += (char) ((Math.random() * 26) + 97); } member.setPassword(passwordEncoder.encode(tempPassword)); memberRepository.save(member); String text = emailService.tempPasswordText(member.getName(), tempPassword); String title = emailService.tempPasswordTItle(); emailService.sendMail(member.getEmail(), title, text); }
email로 멤버를 검색하고, 랜덤을 이용해서 12개 길이를 가진 알파벳 문자열을 만든다. 그리고 비밀번호를 임시 비밀번호로 바꾸고, memberRepository에 update한다. 이후 메일을 보낸다.
4) 회원가입 시 메일 인증
AuthService.java
@PostMapping("/member/signup") public ResponseEntity<Object> signup(@RequestBody MemberDto memberDto){ Member member = authService.signup(memberDto); authService.sendVerificationEmail(member); JSONObject returnObject = new JSONObject(); returnObject.put("msg", "Success"); log.info("새로운 회원가입"); return new ResponseEntity<Object>(returnObject.toString(), HttpStatus.CREATED); }
회원가입 시 사용자 역할을 ROLE_NOT_PERMITTED로 두어야 한다. authService.signup() 함수 내부에 이 내용을 추가한다. 추가적으로 authService.sendVerificationEmail() 함수를 call해서 인증 링크가 담긴 인증 메일 전송한다.
AuthService.java
@Value("${mail.verification.link}") private String VERIFICATION_LINK; public static final Long EmailExpireTime = 1000 * 60 * 30L; // 30분 ... public void sendVerificationEmail(Member member){ UUID uuid = UUID.randomUUID(); redisUtil.set(uuid.toString(), member.getMemberId(), EmailExpireTime); String link = this.VERIFICATION_LINK + uuid.toString(); String title = emailService.verifyEmailTitle(); String text = emailService.verifyEmailText(link); emailService.sendMail(member.getEmail(), title, text); } public void verifyEmail(String key){ if(!redisUtil.hasKey(key)){ throw new CustomException(ErrorCode.LINK_NOT_EXIST); } String memberId = redisUtil.get(key); Member member = getWithNullCheck.getMemberByMemberId(memberRepository, memberId); modifyUserRole(member, UserRole.ROLE_USER); } public void modifyUserRole(Member member, UserRole userRole){ member.setUserROle(userRole); memberRepository.save(member); return; }
UUID는 unique한 값을 생성해 주는 함수이다. 이걸 만들고, uuid를 key로, member id를 value로 redis에 30분간 저장되게 저장한다. 이후 uuid가 담긴 링크를 메일로 보낸다.
AuthController.java
@GetMapping(value = "/member/verify/{key}") public ResponseEntity<Object> verifyUserWIthKey(@PathVariable(value = "key", required = true) String key){ authService.verifyEmail(key); JSONObject jsonObject = new JSONObject(); jsonObject.put("msg", "Success"); log.info("이메일 인증 완료"); return new ResponseEntity<Object>(jsonObject.toString(), HttpStatus.OK); }
이후 인증 링크가 클릭되었을 경우 method를 하나 만든다.
AuthService.java
public void verifyEmail(String key){ if(!redisUtil.hasKey(key)){ throw new CustomException(ErrorCode.LINK_NOT_EXIST); } String memberId = redisUtil.get(key); redisUtil.delete(key); Member member = memberRepository.getMemberByMemberId(memberId); modifyUserRole(member, UserRole.ROLE_USER); } public void modifyUserRole(Member member, UserRole userRole){ member.setUserROle(userRole); memberRepository.save(member); return; }
인증 링크가 클릭되었을 경우 key에 해당하는 value가 있는지, 있는 경우 redis에서 key에 대한 값을 삭제하고 member id로 값을 찾고, 해당 사용자의 role을 ROLE_USER로 승격시킨다.
진짜 마지막으로 security config만 바꾸면 된다.
@EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig extends WebSecurityConfigurerAdapter{ private final JwtUtil jwtUtil; private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; private final JwtAccessDeniedHandler jwtAccessDeniedHandler; @Bean public static PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception{ http .httpBasic().disable() .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/member/modify/**").hasAnyRole("USER", "MANAGER") .antMatchers("/member/**","/v2/api-docs", "/configuration/**", "/swagger*/**", "/webjars/**", "/admin/upload-csv").permitAll() .antMatchers("/manage/**", "/dashboard/**").hasRole("MANAGER") .antMatchers("/myapp/**").hasAnyRole("USER", "MANAGER") .and() .authorizeRequests() .anyRequest() .authenticated() .and() .exceptionHandling() .authenticationEntryPoint(jwtAuthenticationEntryPoint) .accessDeniedHandler(jwtAccessDeniedHandler) .and() .apply(new JwtSecurityConfig(jwtUtil)); } }
member/modify는 로그인 되어 있어야 한다. 이외의 것들은 없어도 되므로 이렇게 설정, 관리자 페이지는 관리자만 접속할 수 있으니 이렇게 하고 마지막 로직은 모든 사용자가 이용할 수 있으므로 위와 같이 설정했다.
그러면 완성!
'Development > Spring' 카테고리의 다른 글
[Spring] DTO와 Entity 간의 변환 (0) | 2024.03.12 |
---|---|
[Spring] Spring image 업로드 / 다운로드(리턴) / 인코딩 (0) | 2022.10.05 |
[Spring] Spring exception handler 추가 - ambiguous handler (0) | 2022.10.05 |
[Spring] slf4j를 이용한 spring logging (0) | 2022.10.05 |
[Spring + Swagger] Swagger 사용 (0) | 2022.10.05 |