Skip to content

Commit

Permalink
Merge branch 'develop' into release
Browse files Browse the repository at this point in the history
  • Loading branch information
hajungIm committed May 30, 2024
2 parents 91dd0af + a8f8149 commit 064fdd3
Show file tree
Hide file tree
Showing 16 changed files with 214 additions and 13 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ package.json
.env
application-oauth.yml
application.properties
/src/main/resources/firebase-private-key.json
/src/main/resources/muffler-apple-auth-key.p8

# Generated files
src/main/generated/
/src/main/resources/firebase-private-key.json
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dependencies {
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
testImplementation 'org.springframework.boot:spring-boot-starter-test'

implementation 'com.h2database:h2'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.umc5th.muffler.domain.member.dto;

import lombok.Getter;

@Getter
public class AppleIdToken {
private String sub;
}
18 changes: 18 additions & 0 deletions src/main/java/com/umc5th/muffler/domain/member/dto/AppleToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.umc5th.muffler.domain.member.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;

@Getter
public class AppleToken {
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("token_type")
private String tokenType;
@JsonProperty("expires_in")
private String expiresIn;
@JsonProperty("refresh_token")
private String refreshToken;
@JsonProperty("id_token")
private String idToken;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package com.umc5th.muffler.domain.member.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@AllArgsConstructor
@NoArgsConstructor
public class KakaoUnlinkResponse {
private Long id;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.umc5th.muffler.domain.member.service;

import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import javax.annotation.PostConstruct;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;
import org.springframework.util.FileCopyUtils;

@Component
@ConfigurationProperties(prefix = "social-login.apple")
@Getter
@Setter
@RequiredArgsConstructor
public class AppleProperties {
private String grantType;
private String clientId;
private String keyId;
private String teamId;
private String audience;
private String privateKeyPath;
private String privateKey;

private final ResourceLoader resourceLoader;

@PostConstruct
public void init() throws IOException {
Resource resource = resourceLoader.getResource(privateKeyPath);
try (InputStreamReader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) {
this.privateKey = FileCopyUtils.copyToString(reader);
}
this.privateKey = processKey(this.privateKey);
}

private String processKey(String pemKey) {
return pemKey.replaceAll("-----BEGIN PRIVATE KEY-----", "")
.replaceAll("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,67 @@
package com.umc5th.muffler.domain.member.service;

import static com.umc5th.muffler.global.response.code.ErrorCode.INTERNAL_SERVER_ERROR;

import com.umc5th.muffler.domain.member.dto.AppleIdToken;
import com.umc5th.muffler.domain.member.dto.LoginRequest;
import com.umc5th.muffler.global.feign.AppleClient;
import com.umc5th.muffler.global.response.exception.MemberException;
import com.umc5th.muffler.global.security.jwt.JwtDecoder;
import com.umc5th.muffler.global.util.DateTimeProvider;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.security.PrivateKey;
import java.security.Security;
import java.util.Base64;
import lombok.RequiredArgsConstructor;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AppleService {
private final AppleClient appleClient;
private final AppleProperties appleProperties;
private final DateTimeProvider dateTimeProvider;

public String login(LoginRequest request) {
return request.getIdToken();
String authentication = request.getIdToken();
String idToken = appleClient.getIdToken(
appleProperties.getClientId(),
generateClientSecret(),
appleProperties.getGrantType(),
authentication
).getIdToken();

return JwtDecoder.decodePayload(idToken, AppleIdToken.class).getSub();
}

private String generateClientSecret() {
return Jwts.builder()
.setHeaderParam(JwsHeader.KEY_ID, appleProperties.getKeyId())
.setIssuer(appleProperties.getTeamId())
.setAudience(appleProperties.getAudience())
.setSubject(appleProperties.getClientId())
.setExpiration(dateTimeProvider.getDateAfterMinutes(5))
.setIssuedAt(dateTimeProvider.getIssuedDate())
.signWith(getPrivateKey(), SignatureAlgorithm.ES256)
.compact();
}

private PrivateKey getPrivateKey() {
Security.addProvider(new BouncyCastleProvider());
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");

try {
byte[] privateKeyBytes = Base64.getDecoder().decode(appleProperties.getPrivateKey());
PrivateKeyInfo privateKeyInfo = PrivateKeyInfo.getInstance(privateKeyBytes);
return converter.getPrivateKey(privateKeyInfo);
} catch (Exception e) {
e.printStackTrace();
throw new MemberException(INTERNAL_SERVER_ERROR, "String 타입 ApplePrivateKey convert 중 에러 발생");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public String login(String idToken) {
public void leave(String memberId) {
try {
kakaoClient.unlinkMember(Long.valueOf(memberId));
log.info("아이디 {}의 카카오 계정 연결 끊기가 완료되었습니다.", memberId);
} catch (FeignException e) {
String errorResult = e.getErrorResult();
if (!errorResult.contains(KAKAO_LEAVE_EXCEPTION)) {
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/umc5th/muffler/global/feign/AppleClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.umc5th.muffler.global.feign;

import com.umc5th.muffler.domain.member.dto.AppleToken;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(
name = "apple-client",
url = "https://appleid.apple.com"
)
public interface AppleClient {
@PostMapping("/auth/token")
AppleToken getIdToken(
@RequestParam("client_id") String clientId,
@RequestParam("client_secret") String clientSecret,
@RequestParam("grant_type") String grantType,
@RequestParam("code") String code
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "kakao-client", url = "https://kapi.kakao.com", configuration = {KakaoAuthInterceptor.class})
@FeignClient(
name = "kakao-client",
url = "https://kapi.kakao.com",
configuration = {KakaoAuthInterceptor.class}
)
public interface KakaoClient {

@PostMapping("/v1/user/unlink?target_id_type=user_id")
KakaoUnlinkResponse unlinkMember(@RequestParam(value = "target_id") Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,23 @@ public enum ErrorCode {
FORBIDDEN(HttpStatus.FORBIDDEN, "금지된 요청입니다."),
INVALID_PERMISSION(HttpStatus.UNAUTHORIZED, "권한이 유효하지 않습니다."),
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "토큰이 유효하지 않습니다."),

TOKEN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "토큰 에러입니다."),

// Member 에러
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 사용자입니다."),
UNSUPPORTED_SOCIAL_TYPE(HttpStatus.UNSUPPORTED_MEDIA_TYPE,"지원하지 않는 소셜 로그인 입니다."),


// Goal 에러
GOAL_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 목표입니다."),
INVALID_GOAL_INPUT(HttpStatus.BAD_REQUEST, "올바르지 않은 목표 입력입니다."),
NO_GOAL_IN_GIVEN_DATE(HttpStatus.BAD_REQUEST, "해당 날짜에 일치하는 목표가 없습니다"),
CATEGORY_GOAL_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 카테고리 목표입니다."),


// DailyPlan 에러
DAILYPLAN_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 일일 소비 계획입니다."),
NO_DAILY_PLAN_GIVEN_DATE(HttpStatus.NOT_FOUND, "수정하려는 소비날짜에 맞는 일일 목표가 없습니다. "
+ "목표가 설정된 다른 날로 수정을 하거나 수정하려는 날에 목표를 설정해주세요."),


// Category 에러
CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 카테고리입니다."),
DUPLICATED_CATEGORY_NAME(HttpStatus.BAD_REQUEST, "카테고리 이름이 중복되었습니다."),
Expand All @@ -52,7 +49,6 @@ public enum ErrorCode {
CANNOT_UPDATE_OTHER_MEMBER_EXPENSE(HttpStatus.UNAUTHORIZED, "다른 유저의 소비 내역을 수정할 수 없습니다"),
CANNOT_UPDATE_TO_ZERO_DAY(HttpStatus.CONFLICT, "제로 데이로 지정한 날로는 소비 내역을 옮길 수 없습니다."),


// Routine 에러
INVALID_ROUTINE_INPUT(HttpStatus.BAD_REQUEST, "올바르지 않은 루틴 입력입니다."),
ROUTINE_TYPE_NOT_FOUND(HttpStatus.BAD_REQUEST, "루틴 타입이 없거나 유효하지 않습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.umc5th.muffler.global.security.jwt;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.umc5th.muffler.global.response.code.ErrorCode;
import com.umc5th.muffler.global.response.exception.CommonException;
import java.util.Base64;
import java.util.Base64.Decoder;

public class JwtDecoder {
private JwtDecoder() {}

public static <T> T decodePayload(String token, Class<T> targetClass) {
String[] tokenParts = token.split("\\.");
String payloadJWT = tokenParts[1];
Decoder decoder = Base64.getUrlDecoder();
String payload = new String(decoder.decode(payloadJWT));
ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

try {
return objectMapper.readValue(payload, targetClass);
} catch (Exception e) {
throw new CommonException(ErrorCode.TOKEN_ERROR, "JWT 토큰 페이로드 decode 중 에러 발생");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@

public interface DateTimeProvider {
LocalDate nowDate();
Date getIssuedDate();
Date getDateAfterDays(int duration);
Date getDateAfterMinutes(int minutes);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public LocalDate nowDate() {
return LocalDate.now(ZoneId.of(ZONE_ID));
}

@Override
public Date getIssuedDate() {
return Date.from(ZonedDateTime.now(ZoneId.of(ZONE_ID)).toInstant());
}

/**
* @return (서울 시간대의 현재 시각 + duration 일) 후의 {@code Date} 반환
*/
Expand All @@ -26,4 +31,11 @@ public Date getDateAfterDays(int duration) {
ZonedDateTime.now(ZoneId.of(ZONE_ID))
.plus(duration, ChronoUnit.DAYS).toInstant());
}

@Override
public Date getDateAfterMinutes(int minutes) {
return Date.from(
ZonedDateTime.now(ZoneId.of(ZONE_ID))
.plus(minutes, ChronoUnit.MINUTES).toInstant());
}
}
8 changes: 8 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ jwt:
aud: ${JWT_AUD}
kakao:
admin-key: ${KAKAO_APP_ADMIN}
social-login:
apple:
grant-type: authorization_code
client-id: ${APPLE_CLIENT_ID}
key-id: ${APPLE_KEY_ID}
team-id: ${APPLE_TEAM_ID}
audience: https://appleid.apple.com
private-key-path: "classpath:muffler-apple-auth-key.p8"

server:
servlet:
Expand Down
8 changes: 8 additions & 0 deletions src/test/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ jwt:
aud: testAUD
kakao:
admin-key: testAdmin
social-login:
apple:
grant-type: authorization_code
client-id: testAppleClientId
key-id: testAppleKeyId
team-id: testAppleTeamId
audience: https://appleid.apple.com
private-key-path: "classpath:muffler-apple-auth-key.p8"
server:
servlet:
encoding:
Expand Down

0 comments on commit 064fdd3

Please sign in to comment.