22
33import com .fasterxml .jackson .databind .JsonNode ;
44import com .fasterxml .jackson .databind .ObjectMapper ;
5- import io .jsonwebtoken .Claims ;
6- import io .jsonwebtoken .ExpiredJwtException ;
7- import io .jsonwebtoken .JwtParser ;
8- import io .jsonwebtoken .Jwts ;
5+ import io .jsonwebtoken .*;
96import lombok .RequiredArgsConstructor ;
107import lombok .extern .slf4j .Slf4j ;
118import org .springframework .beans .factory .annotation .Value ;
9+ import org .springframework .core .io .Resource ;
1210import org .springframework .stereotype .Service ;
11+ import org .springframework .transaction .annotation .Transactional ;
12+ import org .springframework .web .reactive .function .BodyInserters ;
13+ import org .springframework .web .reactive .function .client .WebClient ;
1314import umc .plantory .domain .apple .converter .AppleConverter ;
1415import umc .plantory .domain .member .dto .MemberDataDTO ;
1516import umc .plantory .domain .member .dto .MemberRequestDTO ;
1617import umc .plantory .global .apiPayload .code .status .ErrorStatus ;
1718import umc .plantory .global .apiPayload .exception .handler .AppleHandler ;
1819
20+ import java .io .BufferedReader ;
21+ import java .io .IOException ;
22+ import java .io .InputStreamReader ;
1923import java .math .BigInteger ;
24+ import java .nio .charset .StandardCharsets ;
2025import java .security .KeyFactory ;
26+ import java .security .NoSuchAlgorithmException ;
27+ import java .security .PrivateKey ;
28+ import java .security .interfaces .ECPrivateKey ;
2129import java .security .interfaces .RSAPublicKey ;
30+ import java .security .spec .InvalidKeySpecException ;
31+ import java .security .spec .PKCS8EncodedKeySpec ;
2232import java .security .spec .RSAPublicKeySpec ;
33+ import java .time .Instant ;
2334import java .util .Base64 ;
2435import java .net .URL ;
36+ import java .util .Date ;
2537import java .util .List ;
38+ import java .util .stream .Collectors ;
2639
2740@ Slf4j
2841@ Service
@@ -33,8 +46,20 @@ public class AppleOidcService {
3346 private static final String JWK_URL = "https://appleid.apple.com/auth/keys" ;
3447 // iss 검증 값
3548 private static final String ISSUER = "https://appleid.apple.com" ;
49+ // 약 30일
50+ private static final long MAX_EXP_SECONDS = 60L * 60L * 24L * 30L ;
51+
52+ private final WebClient webClient ;
53+
3654 @ Value ("${apple.bundle-id}" )
3755 private String BUNDLE_ID ;
56+ @ Value ("${apple.p8.location:}" )
57+ private Resource p8Resource ;
58+ @ Value ("${apple.key-id}" )
59+ private String KEY_ID ;
60+ @ Value ("${apple.team-id}" )
61+ private String TEAM_ID ;
62+
3863
3964 /**
4065 * identity_token을 검증하고 담겨있는 멤버 데이터 추출
@@ -97,4 +122,89 @@ public MemberDataDTO.MemberData verifyAndParseIdToken(MemberRequestDTO.AppleOAut
97122 throw new AppleHandler (ErrorStatus .ERROR_ON_VERIFYING );
98123 }
99124 }
125+
126+ /**
127+ * Authorization Code 를 통해 apple refresh_token 값을 받아오는 메서드
128+ */
129+ public String createAppleRefreshToken (String authorizationCode , String clientSecret ) {
130+ return webClient .post ()
131+ .uri ("https://appleid.apple.com/auth/token" )
132+ .header ("Content-Type" , "application/x-www-form-urlencoded;charset=utf-8" )
133+ .body (BodyInserters .fromFormData ("grant_type" , "authorization_code" )
134+ .with ("code" , authorizationCode )
135+ .with ("client_id" , BUNDLE_ID )
136+ .with ("client_secret" , clientSecret ))
137+ .retrieve ()
138+ .bodyToMono (String .class )
139+ .doOnNext (System .out ::println )
140+ .block ();
141+ }
142+
143+ /**
144+ * Apple Client_Secret 생성 메서드
145+ */
146+ public String createAppleClientSecret () {
147+ try {
148+ ECPrivateKey privateKey = (ECPrivateKey ) loadPrivateKeyFromPem ();
149+
150+ Date iat = Date .from (Instant .now ());
151+ Date exp = Date .from (Instant .now ().plusSeconds (MAX_EXP_SECONDS ));
152+
153+ String jwt = Jwts .builder ()
154+ .setHeaderParam ("kid" , KEY_ID )
155+ .setIssuer (TEAM_ID )
156+ .setSubject (BUNDLE_ID )
157+ .setAudience (ISSUER )
158+ .setIssuedAt (iat )
159+ .setExpiration (exp )
160+ .signWith (privateKey , SignatureAlgorithm .ES256 )
161+ .compact ();
162+
163+ return jwt ;
164+ } catch (Exception e ) {
165+ log .error ("Failed to Refresh Apple Client_Secret" , e );
166+ throw new RuntimeException ("Failed to Refresh Apple Client_Secret" , e );
167+ }
168+ }
169+
170+ /**
171+ * pem 파일에 적힌 privateKey 읽어오는 메서드
172+ */
173+ private PrivateKey loadPrivateKeyFromPem () throws Exception {
174+ String pem ;
175+ if (p8Resource != null && p8Resource .exists ()) {
176+ try (BufferedReader br = new BufferedReader (new InputStreamReader (p8Resource .getInputStream (), StandardCharsets .UTF_8 ))) {
177+ pem = br .lines ().collect (Collectors .joining ("\n " ));
178+ }
179+ } else {
180+ throw new AppleHandler (ErrorStatus ._INTERNAL_SERVER_ERROR );
181+ }
182+
183+ String normalized = pem
184+ .replace ("-----BEGIN PRIVATE KEY-----" , "" )
185+ .replace ("-----END PRIVATE KEY-----" , "" )
186+ .replaceAll ("\\ s" , "" );
187+
188+ byte [] der = Base64 .getDecoder ().decode (normalized );
189+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec (der );
190+ KeyFactory kf = KeyFactory .getInstance ("EC" );
191+ return kf .generatePrivate (keySpec );
192+ }
193+
194+ /**
195+ * 플랜토리 - 애플 연동 해제 메서드
196+ */
197+ public void unlinkUser (String refreshToken , String clientSecret ) {
198+ webClient .post ()
199+ .uri ("https://appleid.apple.com/auth/revoke" )
200+ .header ("Content-Type" , "application/x-www-form-urlencoded;charset=utf-8" )
201+ .body (BodyInserters .fromFormData ("token_type_hint" , "refresh_token" )
202+ .with ("token" , refreshToken )
203+ .with ("client_id" , BUNDLE_ID )
204+ .with ("client_secret" , clientSecret ))
205+ .retrieve ()
206+ .bodyToMono (String .class )
207+ .doOnNext (System .out ::println )
208+ .block ();
209+ }
100210}
0 commit comments