Skip to content

Commit

Permalink
✨ Feat: 영수증 검증 및 구독 상태 조회 API 구현 (#219)
Browse files Browse the repository at this point in the history
* ✨ Feat: Subscription Entity 작성

* ✨ Feat: SubscriptionRepository 작성

* ✨ Feat: 영수증 검증 및 구독 상태 조회 API 구현

* ✨ Feat: 구독 상품 검증을 위한 설정 추가

* 🐛 Fix: 로컬에서 JWT 만료되는 버그 해결

* ♻️ Refactor: 영수증 검증 로직 리팩토링

* ✨ Feat: 구독 조회 시 상태 업데이트 로직 추가

* ✨ Feat: 스케줄링으로 구독 상태 업데이트 로직 추가

* ✨ Feat: Feign Client 방식에서 SDK 방식으로 변경

* ✨ Feat: 결제 정보 검증 로직 보완

* ✨ Feat: 사용자 인증 로직 구현

* ♻️ Refactor: 트랜잭션 범위 최소화를 위한 외부 API 호출과 DB 작업 분리

* ♻️ Refactor: 구독 상태 확인 로직을 JPA exists 쿼리로 변경

* ♻️ Refactor: 회원 구독 정보 조회 방식을 DB 조회로 최적화

* ♻️ Refactor: Subscription API 기본 경로 통합

* 🐛 Fix: ErrorCode 오타 수정
  • Loading branch information
ahnsugyeong authored Aug 19, 2024
1 parent 1cbf9f1 commit bd507dc
Show file tree
Hide file tree
Showing 22 changed files with 566 additions and 83 deletions.
6 changes: 6 additions & 0 deletions Briefing-Api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ dependencies {

//spring batch 의존성
implementation 'org.springframework.boot:spring-boot-starter-batch'

// google play
implementation "com.google.api-client:google-api-client:1.33.0"
implementation 'com.google.auth:google-auth-library-oauth2-http:1.6.0'
implementation 'com.google.apis:google-api-services-androidpublisher:v3-rev20220411-1.32.1'
implementation 'com.google.http-client:google-http-client-jackson2:1.41.7'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;


@OpenAPIDefinition(
Expand All @@ -20,6 +21,7 @@
})
@SpringBootApplication(scanBasePackages = {"com.example.briefingapi","com.example.briefingcommon","com.example.briefinginfra"})
@RequiredArgsConstructor
@EnableScheduling
@EnableCaching
@EnableFeignClients(basePackages = "com.example.briefinginfra")
@EnableRedisRepositories
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.example.briefingapi.config;

import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.androidpublisher.AndroidPublisher;
import com.google.api.services.androidpublisher.AndroidPublisherScopes;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.GoogleCredentials;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;

@Component
public class GoogleCredentialsConfig {

@Value("${subscription.google.keyfile.content}")
private String googleAccountFileContent;

public AndroidPublisher androidPublisher() throws IOException, GeneralSecurityException {
InputStream inputStream = new ByteArrayInputStream(googleAccountFileContent.getBytes());
GoogleCredentials credentials = GoogleCredentials.fromStream(inputStream)
.createScoped(AndroidPublisherScopes.ANDROIDPUBLISHER);

JsonFactory jsonFactory = GsonFactory.getDefaultInstance();

return new AndroidPublisher.Builder(
GoogleNetHttpTransport.newTrustedTransport(),
jsonFactory,
new HttpCredentialsAdapter(credentials))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package com.example.briefingapi.member.business;

import java.util.List;

import com.example.briefingapi.fcm.implementation.FcmCommandService;
import com.example.briefingapi.member.implement.MemberCommandAdapter;
import com.example.briefingapi.member.implement.MemberQueryAdapter;
import com.example.briefingapi.member.presentation.dto.MemberRequest;
import com.example.briefingapi.member.presentation.dto.MemberResponse;
import com.example.briefingapi.redis.service.RedisService;
import com.example.briefingapi.security.provider.TokenProvider;
import com.example.briefingcommon.entity.Member;
import com.example.briefingcommon.entity.enums.MemberRole;
Expand All @@ -15,16 +14,13 @@
import com.example.briefinginfra.feign.oauth.apple.client.AppleOauth2Client;
import com.example.briefinginfra.feign.oauth.google.client.GoogleOauth2Client;
import com.example.briefinginfra.feign.oauth.google.dto.GoogleUserInfo;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


import com.example.briefingapi.redis.service.RedisService;

import lombok.RequiredArgsConstructor;
import java.util.List;

@Service
@RequiredArgsConstructor
Expand Down Expand Up @@ -79,10 +75,10 @@ public MemberResponse.TestTokenDTO getTestToken(){
return MemberResponse.TestTokenDTO.builder()
.token(
tokenProvider.createAccessToken(
member.getId(),
member.getSocialType().toString(),
member.getSocialId(),
List.of(new SimpleGrantedAuthority(MemberRole.ROLE_USER.name()))))
member.getId(),
member.getSocialType().toString(),
member.getSocialId(),
List.of(new SimpleGrantedAuthority(MemberRole.ROLE_USER.name()))))
.refeshToken(redisService.generateTestRefreshToken())
.build();
}
Expand Down Expand Up @@ -116,4 +112,4 @@ public void subScribeDailyPush(MemberRequest.ToggleDailyPushAlarmDTO request, Me
fcmCommandService.unSubScribe(dailyPushTopic,request.getFcmToken());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.example.briefingapi.security.handler.annotation;

import io.swagger.v3.oas.annotations.Parameter;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Parameter(hidden = true)
public @interface AuthMember {}
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
package com.example.briefingapi.security.provider;
import java.security.Key;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;

import com.example.briefingapi.exception.JwtAuthenticationException;
import com.example.briefingcommon.common.exception.common.ErrorCode;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
Expand All @@ -22,8 +17,14 @@
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;

@Component
public class TokenProvider implements InitializingBean {
Expand All @@ -38,8 +39,6 @@ public class TokenProvider implements InitializingBean {

private final long accessTokenValidityInMilliseconds;

// private final RefreshTokenRepository refreshTokenRepository;

private Key key;

public enum TokenType {
Expand All @@ -50,12 +49,10 @@ public enum TokenType {
public TokenProvider(
@Value("${jwt.secret}") String secret,
@Value("${jwt.authorities-key}") String authoritiesKey,
@Value("${jwt.access-token-validity-in-seconds}")
long accessTokenValidityInMilliseconds) {
@Value("${jwt.access-token-validity-in-seconds}") long accessTokenValidityInSeconds) {
this.secret = secret;
this.AUTHORITIES_KEY = authoritiesKey;
this.accessTokenValidityInMilliseconds = accessTokenValidityInMilliseconds;
// this.refreshTokenRepository = refreshTokenRepository;
this.accessTokenValidityInMilliseconds = accessTokenValidityInSeconds * 1000;
}

@Override
Expand All @@ -64,82 +61,51 @@ public void afterPropertiesSet() throws Exception {
this.key = Keys.hmacShaKeyFor(keyBytes);
}

// 수정 해야함
public String createAccessToken(
Long userId,
String socialType,
String socialId,
Collection<? extends GrantedAuthority> authorities) {
long now = (new Date()).getTime();
Date validity = new Date(now + this.accessTokenValidityInMilliseconds);
public String createAccessToken(Long userId, String socialType, String socialId, Collection<? extends GrantedAuthority> authorities) {
Instant issuedAt = Instant.now().truncatedTo(ChronoUnit.SECONDS);
Instant expiration = issuedAt.plus(accessTokenValidityInMilliseconds, ChronoUnit.MILLIS);

return Jwts.builder()
.setSubject(String.valueOf(userId))
.claim(AUTHORITIES_KEY, authorities)
.claim(AUTHORITIES_KEY, authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")))
.claim("socialType", socialType)
.claim("socialID", socialId)
.setIssuedAt(Date.from(issuedAt))
.setExpiration(Date.from(expiration))
.signWith(key, SignatureAlgorithm.HS512)
.setExpiration(validity)
.compact();
}

public String createAccessToken(
Long userId, String phoneNum, Collection<? extends GrantedAuthority> authorities) {
long now = (new Date()).getTime();
Date validity = new Date(now + this.accessTokenValidityInMilliseconds);

return Jwts.builder()
.setSubject(String.valueOf(userId))
.claim(AUTHORITIES_KEY, authorities)
.claim("phoneNum", phoneNum)
.signWith(key, SignatureAlgorithm.HS512)
.setExpiration(validity)
.compact();
}

public Authentication getAuthentication(String token) {
Claims claims =
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();

Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
Collection<? extends GrantedAuthority> authorities = Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}

public boolean validateToken(String token, TokenType type) throws JwtAuthenticationException {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
LOGGER.info("JWT Token is valid. Expiration: {}", claimsJws.getBody().getExpiration());
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
LOGGER.error("Invalid JWT signature: {}", e.getMessage());
throw new JwtAuthenticationException(ErrorCode.INVALID_TOKEN_EXCEPTION);
} catch (ExpiredJwtException e) {
if (type == TokenType.ACCESS)
throw new JwtAuthenticationException(ErrorCode.EXPIRED_JWT_EXCEPTION);
else throw new JwtAuthenticationException(ErrorCode.RELOGIN_EXCEPTION);
LOGGER.warn("Expired JWT token: {}", e.getMessage());
throw new JwtAuthenticationException(ErrorCode.EXPIRED_JWT_EXCEPTION);
} catch (UnsupportedJwtException e) {
LOGGER.error("Unsupported JWT token: {}", e.getMessage());
throw new JwtAuthenticationException(ErrorCode.INVALID_TOKEN_EXCEPTION);
} catch (IllegalArgumentException e) {
LOGGER.error("JWT token compact of handler are invalid: {}", e.getMessage());
throw new JwtAuthenticationException(ErrorCode.INVALID_TOKEN_EXCEPTION);
}
}

// public Long validateAndReturnId(String token) throws JwtAuthenticationException{
// try{
// Claims body =
// Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
// return Long.valueOf(body.getSubject());
// }catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e){
// throw new JwtAuthenticationException(Er.JWT_BAD_REQUEST);
// }catch (UnsupportedJwtException e){
// throw new JwtAuthenticationException(Code.JWT_UNSUPPORTED_TOKEN);
// }catch (IllegalArgumentException e){
// throw new JwtAuthenticationException(Code.JWT_BAD_REQUEST);
// }
// }

public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.example.briefingapi.subscription.business;

import com.example.briefingapi.subscription.presentation.dto.SubscriptionRequest;
import com.example.briefingapi.subscription.presentation.dto.SubscriptionResponse;
import com.example.briefingcommon.entity.Member;
import com.example.briefingcommon.entity.Subscription;
import com.example.briefingcommon.entity.enums.SubscriptionStatus;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class SubscriptionMapper {

public static Subscription toSubscription(Member member, SubscriptionRequest.ReceiptDTO request, LocalDateTime expiryDate) {
return Subscription.builder()
.member(member)
.type(request.getSubscriptionType())
.status(LocalDateTime.now().isBefore(expiryDate) ? SubscriptionStatus.ACTIVE : SubscriptionStatus.EXPIRED)
.expiryDate(expiryDate)
.build();
}

public static SubscriptionResponse.SubscriptionDTO toSubscriptionDTO(Subscription subscription) {
return SubscriptionResponse.SubscriptionDTO.builder()
.id(subscription.getId())
.memberId(subscription.getMember().getId())
.type(subscription.getType())
.status(subscription.getStatus())
.expiryDate(subscription.getExpiryDate())
.build();
}

}
Loading

0 comments on commit bd507dc

Please sign in to comment.