Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/8: Member API 구현-2 #22

Merged
merged 18 commits into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading