Skip to content

Commit

Permalink
Merge pull request #22 from Central-MakeUs/feature/8
Browse files Browse the repository at this point in the history
Feature/8: Member API 구현-2
  • Loading branch information
yxhwxn authored Aug 3, 2024
2 parents c3db577 + 1bbc6a9 commit 00df760
Show file tree
Hide file tree
Showing 20 changed files with 601 additions and 216 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-impl:0.12.2'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.2'

implementation 'org.springframework.boot:spring-boot-starter-mail'

compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand Down
88 changes: 88 additions & 0 deletions src/main/java/com/cmc/suppin/global/config/MailConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.cmc.suppin.global.config;

import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component
@RequiredArgsConstructor
public class MailConfig {
private final JavaMailSender javaMailSender;

public boolean sendMail(String toEmail, String code) {
try {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

helper.setFrom(new InternetAddress("[email protected]", "Suppin", "UTF-8"));
helper.setTo(toEmail);
helper.setSubject("Suppin 인증번호");

// Format the current date and time
String formattedDateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd, HH:mm"));

// Use StringBuilder to construct the HTML email body
StringBuilder emailBody = new StringBuilder();
emailBody.append("<!DOCTYPE html>")
.append("<html lang=\"en\">")
.append("<head>")
.append("<meta charset=\"UTF-8\">")
.append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">")
.append("<title>Suppin Email Verification</title>")
.append("</head>")
.append("<body style=\"font-family: Arial, sans-serif;\">")
.append("<div style=\"padding: 20px; border: 1px solid #eaeaea; max-width: 600px; margin: 0 auto;\">")
.append("<div style=\"padding: 20px; border-bottom: 1px solid #eaeaea; text-align: center;\">")
.append("<img src=\"cid:suppinLogo\" alt=\"Suppin Logo\" style=\"width: 100px;\">")
.append("<h2 style=\"color: #333;\"><span style=\"color: #1a73e8;\">[Suppin]</span> 인증번호를 안내해 드립니다.</h2>")
.append("</div>")
.append("<div style=\"padding: 20px;\">")
.append("<p>안녕하세요, Suppin을 이용해주셔서 감사합니다 :)</p>")
.append("<p>Suppin 회원가입을 위해 인증번호를 안내해 드립니다. 아래 인증번호를 입력하여 이메일 인증을 완료해 주세요.</p>")
.append("<div style=\"font-size: 24px; font-weight: bold; margin: 20px 0; color: #333; text-align: center;\">")
.append(code)
.append("</div>")
.append("<table style=\"width: 100%; border-collapse: collapse;\">")
.append("<tbody>")
.append("<tr><td style=\"padding: 10px; border: 1px solid #eaeaea;\">인증 번호</td><td style=\"padding: 10px; border: 1px solid #eaeaea;\">")
.append(code)
.append("</td></tr>")
.append("<tr><td style=\"padding: 10px; border: 1px solid #eaeaea;\">요청 일시</td><td style=\"padding: 10px; border: 1px solid #eaeaea;\">")
.append(formattedDateTime)
.append("</td></tr>")
.append("</tbody>")
.append("</table>")
.append("</div>")
.append("<div style=\"padding: 20px; border-top: 1px solid #eaeaea; text-align: center; color: #999;\">")
.append("<p>감사합니다.</p>")
.append("<p style=\"font-size: 12px;\">※ 본 메일은 Suppin 서비스 이용에 관한 안내 메일입니다.</p>")
.append("</div>")
.append("</div>")
.append("</body>")
.append("</html>");

helper.setText(emailBody.toString(), true);

// Add inline image
ClassPathResource logoImage = new ClassPathResource("static/images/suppin-logo.png");
helper.addInline("suppinLogo", logoImage);

javaMailSender.send(message);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}




Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ public enum MemberErrorCode implements BaseErrorCode {
MEMBER_NOT_FOUND("mem-404/01", HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다."),
VALIDATION_FAILED("mem-400/01", HttpStatus.BAD_REQUEST, "입력값에 대한 검증에 실패했습니다."),
MEMBER_ALREADY_DELETED("mem-400/02", HttpStatus.BAD_REQUEST, "탈퇴한 회원입니다."),
PASSWORD_CONFIRM_NOT_MATCHED("mem-400/03", HttpStatus.BAD_REQUEST, "비밀번호가 확인이 일치하지 않습니다."),
PASSWORD_CONFIRM_NOT_MATCHED("mem-400/03", HttpStatus.BAD_REQUEST, "비밀번호가 잘못 입력되었습니다."),
PASSWORD_FORMAT_NOT_MATCHED("mem-400/04", HttpStatus.BAD_REQUEST, "비밀번호는 8자 이상 20자 이하로 입력해주세요."),
DUPLICATE_MEMBER_EMAIL("mem-409/01", HttpStatus.CONFLICT, "이미 존재하는 이메일입니다."),
DUPLICATE_NICKNAME("mem-409/01", HttpStatus.CONFLICT, "이미 존재하는 닉네임입니다.");


private final String code;
private final HttpStatus status;
private final String message;
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/cmc/suppin/global/response/ApiResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,16 @@ public static <T> ApiResponse<T> of(ResponseCode responseCode, T data) {
public static <T> ApiResponse<T> of(ResponseCode responseCode) {
return new ApiResponse<>(responseCode.getCode(), responseCode.getMessage(), null);
}

public static <T> ApiResponse<T> confirm(T data) {
return new ApiResponse<>(ResponseCode.CONFIRM.getCode(), ResponseCode.CONFIRM.getMessage(), data);
}

public static <T> ApiResponse<T> confirm(ResponseCode responseCode, T data) {
return new ApiResponse<>(responseCode.getCode(), responseCode.getMessage(), data);
}

public static <T> ApiResponse<T> confirm(ResponseCode responseCode) {
return new ApiResponse<>(responseCode.getCode(), responseCode.getMessage(), null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
public enum ResponseCode {

SUCCESS("200", "정상 처리되었습니다."),
CREATE("201", "정상적으로 생성되었습니다.");
CREATE("201", "정상적으로 생성되었습니다."),
CONFIRM("202", "검증 및 확인 되었습니다.");

private final String code;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,23 +102,27 @@ public SecurityFilterChain securityFilterChainDefault(HttpSecurity http) throws
return http.build();
}

// 인증 및 인가가 필요한 엔드포인트에 적용되는 RequestMatcher
private RequestMatcher[] requestHasRoleUser() {
List<RequestMatcher> requestMatchers = List.of(
antMatcher("/api/v1/members/**"),
antMatcher(PATCH, "/api/members")
antMatcher(PATCH, "/api/v1/members")
);
return requestMatchers.toArray(RequestMatcher[]::new);
}

// permitAll 권한을 가진 엔드포인트에 적용되는 RequestMatcher
private RequestMatcher[] requestPermitAll() {
List<RequestMatcher> requestMatchers = List.of(
antMatcher("/"),
antMatcher("/swagger-ui/**"),
antMatcher("/actuator/**"),
antMatcher("/v3/api-docs/**"),
antMatcher("/api/v1/members/login/**"),
antMatcher("/api/v1/members/join"),
antMatcher("/api/v1/survey/reply/**")
antMatcher("/api/v1/members/join/**"),
antMatcher("/api/v1/members/checkUserId"),
antMatcher("/api/v1/members/checkEmail"),
antMatcher("/api/v1/survey/reply/**") // 설문조사 응답 시 적용
);
return requestMatchers.toArray(RequestMatcher[]::new);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import com.cmc.suppin.global.security.user.UserDetailsImpl;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
Expand All @@ -17,10 +18,7 @@

import javax.crypto.SecretKey;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;

import static org.springframework.util.StringUtils.hasText;
Expand All @@ -33,17 +31,15 @@ public class JwtTokenProvider {
private static final String AUTHENTICATION_CLAIM_NAME = "roles";
private static final String AUTHENTICATION_SCHEME = "Bearer ";

// 토큰 블랙리스트 관리용 Set
private final Set<String> tokenBlacklist = new HashSet<>();

@Value("${JWT_SECRET_KEY}")
private String secretKey;

@Value("${ACCESS_EXPIRY_SECONDS}")
private int accessExpirySeconds;

// @Value("${jwt.refresh-expiry-seconds}")
// private int refreshExpirySeconds;

// private final RedisKeyRepository redisKeyRepository;

public String createAccessToken(UserDetailsImpl userDetails) {
Instant now = Instant.now();
Instant expirationTime = now.plusSeconds(accessExpirySeconds);
Expand All @@ -64,17 +60,6 @@ public String createAccessToken(UserDetailsImpl userDetails) {
.compact();
}

// public String createRefreshToken() {
// Instant now = Instant.now();
// Instant expirationTime = now.plusSeconds(refreshExpirySeconds);
//
// return Jwts.builder()
// .issuedAt(Date.from(now))
// .expiration(Date.from(expirationTime))
// .signWith(extractSecretKey())
// .compact();
// }

/**
* 권한 체크
*/
Expand Down Expand Up @@ -121,19 +106,49 @@ private Claims verifyAndExtractClaims(String accessToken) {
.getPayload();
}

/*
public void validateAccessToken(String accessToken) {
Jwts.parser()
.verifyWith(extractSecretKey())
.build()
.parse(accessToken);
}
*/

public boolean validateAccessToken(String accessToken) {
if (isTokenBlacklisted(accessToken)) {
return false;
}

try {
Jwts.parser()
.verifyWith(extractSecretKey())
.build()
.parse(accessToken);
return true;
} catch (JwtException | IllegalArgumentException e) {
log.error("Invalid JWT token: {}", e.getMessage());
return false;
}
}

/**
* SecretKey 추출
*/
private SecretKey extractSecretKey() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
}

/**
* 토큰 블랙리스트 관리
*/
public void addTokenToBlacklist(String token) {
tokenBlacklist.add(token);
}

public boolean isTokenBlacklisted(String token) {
return tokenBlacklist.contains(token);
}
}


Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.cmc.suppin.global.security.service;

import com.cmc.suppin.global.enums.UserStatus;
import com.cmc.suppin.global.security.exception.SecurityException;
import com.cmc.suppin.global.security.user.UserDetailsImpl;
import com.cmc.suppin.member.domain.Member;
Expand All @@ -26,7 +27,7 @@ public class MemberDetailsService implements UserDetailsService {

@Override
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
Member member = memberRepository.findByUserId(userId)
Member member = memberRepository.findByUserIdAndStatusNot(userId, UserStatus.DELETED)
.orElseThrow(() -> new SecurityException(MEMBER_NOT_FOUND));

if (member.isDeleted()) {
Expand Down
Loading

0 comments on commit 00df760

Please sign in to comment.