diff --git a/README.md b/README.md index 426efe8c8..bebbd37a4 100644 --- a/README.md +++ b/README.md @@ -40,12 +40,6 @@ - [x] CORS 설정하기 - [x] CORS 설정 테스트 -## https 통신을 위한 설정 -- [ ] DuckDns를 이용하여 도메인 설정하기 -- [ ] Nginx를 이용하여 https 설정하기 -- [ ] Cerbot을 이용하여 SSL 인증서 발급하기 -- [ ] OpenSSL을 이용하여 SSL 인증서 발급하기 - --- # 6주차 3단계 구현해야할 기능 목록들 정리 ## 1. 회원가입 API 수정하기 @@ -59,14 +53,7 @@ - [x] 포인트가 상품 금액의 50%를 넘지 않는지 검증하는 로직 추가 - [x] 사용자는 구매한 상품의 5%를 포인트로 적립받는다. - [x] 반환받은 포인트값 업데이트하기 - - +- [x] jwt 토큰 길이 수정 (512비트 이상으로) + - 현재 발생하고 있는 오류는 JWT 토큰을 검증하기 위한 키가 HS512 알고리즘에 대해 충분히 안전하지 않기 때문이다. + - JWT JWA 명세서(RFC 7518, 섹션 3.2)에 따르면, HS512와 함께 사용되는 키는 최소 512비트 이상이어야 한다. --- -# 6주차 4단계 구현해야할 기능 목록들 정리 -### 추가적으로 생각해봐야 할 API들 -- 카카오 로그인 API -- 특정 카테고리 조회, 새로운 카테고리 추가, 카테고리 삭제 API -- 옵션 추가 및 삭제 API -- 주문 추가 API -- 상품 추가, 상품 정보 수정, 상품 삭제 API - diff --git a/build.gradle b/build.gradle index 5aeacdb26..b74b5e7e7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,12 @@ plugins { id 'java' id 'org.springframework.boot' version '3.3.1' - id 'io.spring.dependency-management' version '1.1.4' // 버전까지 추가해줘야함 - + id 'io.spring.dependency-management' version '1.1.4' // id 'jacoco' +} +tasks.bootJar { + archiveFileName.set("spring-gift-1.0-SNAPSHOT.jar") // 자바 빌드 후 jar 파일명 명확히 기재 } group = 'camp.nextstep.edu' @@ -38,63 +40,6 @@ dependencies { } test { - //finalizedBy jacocoTestReport + // finalizedBy jacocoTestReport useJUnitPlatform() } - - -//jacoco { -// toolVersion = "0.8.10" -// reportsDirectory = layout.buildDirectory.dir('jacocoReport') -//} -// -// -//jacocoTestReport { -// -// dependsOn test -// -// reports { -// xml.required = false -// csv.required = false -// html.required = true -// } -// -// afterEvaluate { -// classDirectories.setFrom( -// files(classDirectories.files.collect { -// fileTree(dir: it, excludes: [ -// '**/domain/**', -// '**/global/**', -// '**/*Application*' -// ]) -// }) -// ) -// } -// finalizedBy 'jacocoTestCoverageVerification' -//} - -/* -jacocoTestCoverageVerification { - violationRules { - rule { - enabled = true - element = 'CLASS' - - // 라인 커버리지 제한을 90%로 설정 - limit { - counter = 'LINE' - value = 'COVEREDRATIO' - minimum = 0.90 - } - - excludes = [ - '**.*Application', - '**.domain.**', - '**.global.**' - - ] - } - - } -} -*/ diff --git a/src/main/java/gift/common/auth/JwtUtil.java b/src/main/java/gift/common/auth/JwtUtil.java index f23e22c87..7659fe8a3 100644 --- a/src/main/java/gift/common/auth/JwtUtil.java +++ b/src/main/java/gift/common/auth/JwtUtil.java @@ -1,23 +1,27 @@ package gift.common.auth; +import gift.member.model.Member; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import java.nio.charset.StandardCharsets; -import java.security.Key; +import javax.crypto.SecretKey; +import java.util.Date; @Component public class JwtUtil { - private final Key key; + private final SecretKey key; + private final long expirationTime; - public JwtUtil(@Value("${jwt.secret}") String secretKey) { - this.key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); + public JwtUtil(@Value("${jwt.secret}") String secretKey, @Value("${jwt.expiration}") long expirationTime) { + this.key = Keys.hmacShaKeyFor(secretKey.getBytes()); + this.expirationTime = expirationTime; } - // JWT 토큰을 파싱하여 그 안에 포함된 클레임을 반환 public Claims extractClaims(String token) { return Jwts.parser() @@ -27,7 +31,7 @@ public Claims extractClaims(String token) { .getBody(); } - // 토큰이 유효한지 확인 + // 토큰 유효성 검사 public boolean isTokenValid(String token) { try { Jwts.parser() @@ -35,7 +39,7 @@ public boolean isTokenValid(String token) { .build() .parseClaimsJws(token); return true; - } catch (Exception e) { + } catch (JwtException e) { e.printStackTrace(); return false; } @@ -46,4 +50,21 @@ public String extractEmail(String token) { Claims claims = extractClaims(token); return claims.get("email", String.class); } + + // 헤더값에서 토큰 추출하기 (Bearer 제거) + public String extractToken(String authorizationHeader) { + return authorizationHeader.startsWith("Bearer ") ? authorizationHeader.substring(7) : null; + } + + + // JWT 생성 + public String generateToken(Member member) { + return Jwts.builder() + .setSubject(member.getId().toString()) + .claim("email", member.getEmail()) // 이메일 클레임 추가 + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + expirationTime)) + .signWith(SignatureAlgorithm.HS512, key) + .compact(); + } } diff --git a/src/main/java/gift/common/auth/LoginMemberArgumentResolver.java b/src/main/java/gift/common/auth/LoginMemberArgumentResolver.java index 072e80407..f70673eed 100644 --- a/src/main/java/gift/common/auth/LoginMemberArgumentResolver.java +++ b/src/main/java/gift/common/auth/LoginMemberArgumentResolver.java @@ -41,7 +41,7 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m } // JWT 토큰 검증 - String token = header.substring(7); // "Bearer " 접두사 제거 + String token = jwtUtil.extractToken(header); if (!jwtUtil.isTokenValid(token)) { throw new UserNotFoundException("Invalid JWT token"); } diff --git a/src/main/java/gift/member/controller/MemberController.java b/src/main/java/gift/member/controller/MemberController.java index c4be3a36a..abdf0d161 100644 --- a/src/main/java/gift/member/controller/MemberController.java +++ b/src/main/java/gift/member/controller/MemberController.java @@ -1,20 +1,16 @@ package gift.member.controller; +import gift.common.auth.JwtUtil; import gift.common.util.CommonResponse; -import gift.member.dto.LoginRequest; -import gift.member.dto.LoginResponse; -import gift.member.dto.SignUpReqeust; -import gift.member.dto.SignUpResponse; +import gift.member.dto.*; import gift.member.service.MemberService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/members") @@ -22,9 +18,11 @@ public class MemberController { private final MemberService memberService; + private final JwtUtil jwtUtil; - public MemberController(MemberService memberService) { + public MemberController(MemberService memberService, JwtUtil jwtUtil) { this.memberService = memberService; + this.jwtUtil = jwtUtil; } // 회원가입 @@ -51,11 +49,31 @@ public ResponseEntity login(@Valid @RequestBody LoginRequest request) { String token = memberService.login(email, password); if (token == null) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid email or password"); // 403 Forbidden + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("회원가입하지 않은 사용자입니다."); // 403 Forbidden } LoginResponse response = new LoginResponse(email, token); return ResponseEntity.ok(new CommonResponse<>(response, "로그인 후 토큰 발기 성공", true)); } + + // 특정 회원의 포인트 조회 + @Operation(summary = "포인트 조회", description = "특정 회원의 포인트를 조회한다.") + @GetMapping("/points") + public ResponseEntity getPoint( + @Parameter(hidden = true) @RequestHeader("Authorization") String authorizationHeader + ) { + // 토큰 추출 + String token = jwtUtil.extractToken(authorizationHeader); + if (token == null || !jwtUtil.isTokenValid(token)) { + // 401 Unauthorized + return ResponseEntity.status(401).body(new CommonResponse<>(null, "Invalid or missing token", false)); + } + System.out.println("KKKKtoken = " + token); + String memberEmail = jwtUtil.extractEmail(token); + System.out.println("KKKKmemberEmail = " + memberEmail); + Long points = memberService.getPoint(memberEmail); + + return ResponseEntity.ok(new CommonResponse<>(new PointResponse(points), "포인트 조회 성공", true)); + } } diff --git a/src/main/java/gift/member/dto/PointResponse.java b/src/main/java/gift/member/dto/PointResponse.java new file mode 100644 index 000000000..eef82272d --- /dev/null +++ b/src/main/java/gift/member/dto/PointResponse.java @@ -0,0 +1,10 @@ +package gift.member.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record PointResponse( + Long points +) { +} diff --git a/src/main/java/gift/member/model/Member.java b/src/main/java/gift/member/model/Member.java index 99daca0a2..ea4f4d3bc 100644 --- a/src/main/java/gift/member/model/Member.java +++ b/src/main/java/gift/member/model/Member.java @@ -25,10 +25,20 @@ public class Member { @Column(nullable = false) private String password; + private Long points; + @OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE, orphanRemoval = true) private List wishList = new ArrayList<>(); // 활용 메서드들 + public void addPoints(Long points) { + this.points += points; + } + + public void subtractPoints(Long points) { + this.points -= points; + } + public void addWish(Wish wish) { this.wishList.add(wish); wish.setMember(this); @@ -58,6 +68,12 @@ public Member(String email, String password) { this.password = password; } + public Member(String email, String password, Long points) { + this.email = email; + this.password = password; + this.points = points; + } + // Getters and setters public Long getId() { return id; @@ -90,4 +106,12 @@ public List getWishList() { public void setWishList(List wishList) { this.wishList = wishList; } + + public Long getPoints() { + return points; + } + + public void setPoints(Long points) { + this.points = points; + } } diff --git a/src/main/java/gift/member/service/MemberService.java b/src/main/java/gift/member/service/MemberService.java index dbb0d3363..b844d38b2 100644 --- a/src/main/java/gift/member/service/MemberService.java +++ b/src/main/java/gift/member/service/MemberService.java @@ -6,7 +6,9 @@ public interface MemberService { void registerMember(String email, String password); - String generateToken(Member member); - String login(String email, String password); + + Long getPoint(String memberEmail); + + Member getMemberByToken(String token); } diff --git a/src/main/java/gift/member/service/MemberServiceImpl.java b/src/main/java/gift/member/service/MemberServiceImpl.java index 135ad646b..4b60419e6 100644 --- a/src/main/java/gift/member/service/MemberServiceImpl.java +++ b/src/main/java/gift/member/service/MemberServiceImpl.java @@ -1,29 +1,26 @@ package gift.member.service; +import gift.common.auth.JwtUtil; import gift.member.model.Member; import gift.member.repository.MemberRepository; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import java.nio.charset.StandardCharsets; -import java.util.Date; import java.util.Optional; -import java.security.Key; @Service public class MemberServiceImpl implements MemberService { private final MemberRepository memberRepository; - private final Key key; + private final JwtUtil jwtUtil; - public MemberServiceImpl(MemberRepository memberRepository, @Value("${jwt.secret}") String secretKey) { + public MemberServiceImpl(MemberRepository memberRepository, JwtUtil jwtUtil) { this.memberRepository = memberRepository; - this.key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); + this.jwtUtil = jwtUtil; } + // 회원가입 public void registerMember(String email, String password) { - Member member = new Member(email, password); + // 신규 회원은 3000포인트를 부여 + Member member = new Member(email, password, 3000L); Optional existingMember = memberRepository.findByEmail(email); // validation @@ -34,25 +31,31 @@ public void registerMember(String email, String password) { memberRepository.save(member); } - public String generateToken(Member member) { - return Jwts.builder() - .setSubject(member.getId().toString()) - .claim("email", member.getEmail()) // 이메일 클레임 추가 - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1시간 만료 - .signWith(key) - .compact(); + // 로그인 + public String login(String email, String password) { + Member member = memberRepository.findByEmail(email).orElse(null); + if (member == null || !member.getPassword().equals(password)) { + return null; + } + + return jwtUtil.generateToken(member); } - public String login(String email, String password) { + // 포인트 조회 + public Long getPoint(String email) { Member member = memberRepository.findByEmail(email).orElseThrow( () -> new IllegalArgumentException("No such member: ") ); - if (!member.getPassword().equals(password)) { - return null; - } + return member.getPoints(); + } - return generateToken(member); + // 토큰으로 사용자 정보 조회하기 + public Member getMemberByToken(String token) { + String email = jwtUtil.extractEmail(token); + + return memberRepository.findByEmail(email).orElseThrow( + () -> new IllegalArgumentException("토큰값에 해당하는 사용자 없음!!!") + ); } } diff --git a/src/main/java/gift/option/repository/OptionJpaRepository.java b/src/main/java/gift/option/repository/OptionJpaRepository.java index 05f55f366..b244cc227 100644 --- a/src/main/java/gift/option/repository/OptionJpaRepository.java +++ b/src/main/java/gift/option/repository/OptionJpaRepository.java @@ -11,7 +11,4 @@ @Repository public interface OptionJpaRepository extends JpaRepository { List