Skip to content

Commit 19201c0

Browse files
authored
Merge pull request #170 from 1223v/login
Fix: 애플 로그인 secret key 파싱 jwt 토큰 방식 변경
2 parents 38fa3c2 + c7f3f1e commit 19201c0

File tree

6 files changed

+187
-34
lines changed

6 files changed

+187
-34
lines changed

build.gradle

+9
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ dependencies {
4848
// swagger
4949
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
5050

51+
// Bouncy Castle Provider
52+
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
53+
54+
// Bouncy Castle PKIX/CMS/EAC/PKCS/OCSP/TSP/OPENSSL
55+
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
56+
57+
// Apache Commons IO
58+
implementation 'commons-io:commons-io:2.11.0'
59+
5160
// //Querydsl 추가
5261
// implementation 'com.querydsl:querydsl-core:5.0.0'
5362
// implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'

src/main/java/com/readyvery/readyverydemo/config/OauthConfig.java

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
@Slf4j
88
@Configuration
99
public class OauthConfig {
10+
1011
public static final String KAKAO_NAME = "kakao";
1112
public static final String APPLE_NAME = "apple";
1213
}

src/main/java/com/readyvery/readyverydemo/config/SpringSecurityConfig.java

+16
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
import org.springframework.security.config.http.SessionCreationPolicy;
1212
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
1313
import org.springframework.security.crypto.password.PasswordEncoder;
14+
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
15+
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
16+
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
1417
import org.springframework.security.web.SecurityFilterChain;
1518
import org.springframework.security.web.authentication.logout.LogoutFilter;
1619
import org.springframework.web.cors.CorsConfiguration;
@@ -23,6 +26,7 @@
2326
import com.readyvery.readyverydemo.security.exception.CustomAuthenticationEntryPoint;
2427
import com.readyvery.readyverydemo.security.jwt.filter.JwtAuthenticationProcessingFilter;
2528
import com.readyvery.readyverydemo.security.jwt.service.JwtService;
29+
import com.readyvery.readyverydemo.security.oauth2.CustomRequestEntityConverter;
2630
import com.readyvery.readyverydemo.security.oauth2.handler.OAuth2LoginFailureHandler;
2731
import com.readyvery.readyverydemo.security.oauth2.handler.OAuth2LoginSuccessHandler;
2832
import com.readyvery.readyverydemo.security.oauth2.service.CustomOAuth2UserService;
@@ -70,8 +74,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
7074
// [PART 3]
7175
//== 소셜 로그인 설정 ==//
7276
.oauth2Login(oauth2 -> oauth2
77+
.tokenEndpoint(token -> token.accessTokenResponseClient(accessTokenResponseClient())) // 토큰 엔드포인트 설정
7378
.successHandler(oAuth2LoginSuccessHandler) // 동의하고 계속하기를 눌렀을 때 Handler 설정
7479
.failureHandler(oAuth2LoginFailureHandler) // 소셜 로그인 실패 시 핸들러 설정
80+
7581
.userInfoEndpoint(userInfo -> userInfo
7682
.userService(customOAuth2UserService)))
7783

@@ -126,4 +132,14 @@ public JwtAuthenticationProcessingFilter jwtAuthenticationProcessingFilter() {
126132
userRepository, refreshTokenRepository);
127133
return jwtAuthenticationFilter;
128134
}
135+
136+
@Bean
137+
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
138+
139+
DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
140+
new DefaultAuthorizationCodeTokenResponseClient();
141+
accessTokenResponseClient.setRequestEntityConverter(new CustomRequestEntityConverter());
142+
143+
return accessTokenResponseClient;
144+
}
129145
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.readyvery.readyverydemo.security.oauth2;
2+
3+
import java.io.IOException;
4+
import java.io.StringReader;
5+
import java.security.PrivateKey;
6+
import java.time.LocalDateTime;
7+
import java.time.ZoneId;
8+
import java.util.Date;
9+
import java.util.HashMap;
10+
import java.util.Map;
11+
12+
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
13+
import org.bouncycastle.openssl.PEMParser;
14+
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
15+
import org.springframework.beans.factory.annotation.Value;
16+
import org.springframework.core.convert.converter.Converter;
17+
import org.springframework.http.RequestEntity;
18+
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
19+
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter;
20+
import org.springframework.util.MultiValueMap;
21+
22+
import io.jsonwebtoken.Jwts;
23+
import lombok.Getter;
24+
import lombok.extern.slf4j.Slf4j;
25+
26+
@Slf4j
27+
@Getter
28+
public class CustomRequestEntityConverter implements Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> {
29+
30+
private OAuth2AuthorizationCodeGrantRequestEntityConverter defaultConverter;
31+
32+
public CustomRequestEntityConverter() {
33+
defaultConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
34+
}
35+
36+
@Value("${app.apple.url}")
37+
private String appleUrl;
38+
39+
@Value("${app.apple.private-key}")
40+
private String privateKeyString;
41+
@Value("${app.apple.client-id}")
42+
private String appleClientId;
43+
44+
@Value("${app.apple.team-id}")
45+
private String appleTeamId;
46+
47+
@Value("${app.apple.key-id}")
48+
private String appleKeyId;
49+
50+
@Override
51+
public RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest req) {
52+
RequestEntity<?> entity = defaultConverter.convert(req);
53+
String registrationId = req.getClientRegistration().getRegistrationId();
54+
MultiValueMap<String, String> params = (MultiValueMap<String, String>)entity.getBody();
55+
if (registrationId.contains("apple")) {
56+
try {
57+
params.set("client_secret", createClientSecret());
58+
} catch (IOException e) {
59+
throw new RuntimeException(e);
60+
}
61+
}
62+
return new RequestEntity<>(params, entity.getHeaders(),
63+
entity.getMethod(), entity.getUrl());
64+
}
65+
66+
public PrivateKey getPrivateKey() throws IOException {
67+
PEMParser pemParser = new PEMParser(new StringReader(privateKeyString));
68+
PrivateKeyInfo object = (PrivateKeyInfo)pemParser.readObject();
69+
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
70+
return converter.getPrivateKey(object);
71+
}
72+
73+
public String createClientSecret() throws IOException {
74+
Date expirationDate = Date.from(LocalDateTime.now().plusDays(30).atZone(ZoneId.systemDefault()).toInstant());
75+
Map<String, Object> jwtHeader = new HashMap<>();
76+
jwtHeader.put("kid", appleKeyId);
77+
jwtHeader.put("alg", "ES256");
78+
79+
return Jwts.builder()
80+
.setHeaderParams(jwtHeader)
81+
.setIssuer(appleTeamId)
82+
.setIssuedAt(new Date(System.currentTimeMillis())) // 발행 시간 - UNIX 시간
83+
.setExpiration(expirationDate) // 만료 시간
84+
.setAudience(appleUrl)
85+
.setSubject(appleClientId)
86+
.signWith(getPrivateKey())
87+
.compact();
88+
}
89+
}

src/main/java/com/readyvery/readyverydemo/security/oauth2/OAuthAttributes.java

+1-4
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,11 @@ public OAuthAttributes(String nameAttributeKey, OAuth2UserInfo oauth2UserInfo) {
3737
*/
3838
public static OAuthAttributes of(SocialType socialType,
3939
String userNameAttributeName, Map<String, Object> attributes) {
40-
System.out.println("attributes = " + attributes);
40+
4141
if (socialType == SocialType.KAKAO) {
4242
return ofKakao(userNameAttributeName, attributes);
4343
}
44-
if (socialType == SocialType.APPLE) {
4544

46-
return ofApple(userNameAttributeName, attributes);
47-
}
4845
return ofGoogle(userNameAttributeName, attributes);
4946

5047
}

src/main/java/com/readyvery/readyverydemo/security/oauth2/service/CustomOAuth2UserService.java

+71-30
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22

33
import static com.readyvery.readyverydemo.config.OauthConfig.*;
44

5+
import java.nio.charset.StandardCharsets;
6+
import java.util.Base64;
57
import java.util.Collections;
8+
import java.util.HashMap;
69
import java.util.Map;
710

811
import org.springframework.security.core.authority.SimpleGrantedAuthority;
912
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
1013
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
1114
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
1215
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
16+
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
1317
import org.springframework.security.oauth2.core.user.OAuth2User;
1418
import org.springframework.stereotype.Service;
1519

20+
import com.fasterxml.jackson.core.JsonProcessingException;
21+
import com.fasterxml.jackson.databind.ObjectMapper;
1622
import com.readyvery.readyverydemo.domain.SocialType;
1723
import com.readyvery.readyverydemo.domain.UserInfo;
1824
import com.readyvery.readyverydemo.domain.repository.UserRepository;
@@ -31,42 +37,58 @@ public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequ
3137

3238
@Override
3339
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
34-
System.out.println("userRequest = " + userRequest);
40+
String registrationId = userRequest.getClientRegistration().getRegistrationId();
41+
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
42+
43+
Map<String, Object> attributes;
3544
/**
3645
* DefaultOAuth2UserService 객체를 생성하여, loadUser(userRequest)를 통해 DefaultOAuth2User 객체를 생성 후 반환
3746
* DefaultOAuth2UserService의 loadUser()는 소셜 로그인 API의 사용자 정보 제공 URI로 요청을 보내서
3847
* 사용자 정보를 얻은 후, 이를 통해 DefaultOAuth2User 객체를 생성 후 반환한다.
3948
* 결과적으로, OAuth2User는 OAuth 서비스에서 가져온 유저 정보를 담고 있는 유저
4049
*/
41-
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
42-
OAuth2User oAuth2User = delegate.loadUser(userRequest);
43-
44-
/**
45-
* userRequest에서 registrationId 추출 후 registrationId으로 SocialType 저장
46-
* http://localhost:8080/oauth2/authorization/kakao에서 kakao가 registrationId
47-
* userNameAttributeName은 이후에 nameAttributeKey로 설정된다.
48-
*/
49-
String registrationId = userRequest.getClientRegistration().getRegistrationId();
50-
SocialType socialType = getSocialType(registrationId);
51-
52-
String userNameAttributeName = userRequest.getClientRegistration()
53-
.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); // OAuth2 로그인 시 키(PK)가 되는 값
54-
Map<String, Object> attributes = oAuth2User.getAttributes(); // 소셜 로그인에서 API가 제공하는 userInfo의 Json 값(유저 정보들)
55-
56-
// socialType에 따라 유저 정보를 통해 OAuthAttributes 객체 생성
57-
58-
OAuthAttributes extractAttributes = OAuthAttributes.of(socialType, userNameAttributeName, attributes);
59-
60-
UserInfo createdUser = getUser(extractAttributes, socialType); // getUser() 메소드로 User 객체 생성 후 반환
61-
62-
// DefaultOAuth2User를 구현한 CustomOAuth2User 객체를 생성해서 반환
63-
return new CustomOAuth2User(
64-
Collections.singleton(new SimpleGrantedAuthority(createdUser.getRole().getKey())),
65-
attributes,
66-
extractAttributes.getNameAttributeKey(),
67-
createdUser.getEmail(),
68-
createdUser.getRole()
69-
);
50+
if (registrationId.contains(APPLE_NAME)) {
51+
String idToken = userRequest.getAdditionalParameters().get("id_token").toString();
52+
attributes = decodeJwtTokenPayload(idToken);
53+
attributes.put("id_token", idToken);
54+
Map<String, Object> userAttributes = new HashMap<>();
55+
userAttributes.put("resultcode", "00");
56+
userAttributes.put("message", "success");
57+
userAttributes.put("response", attributes);
58+
59+
return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority("ROLE_GUEST")),
60+
userAttributes, "response");
61+
} else {
62+
OAuth2User oAuth2User = delegate.loadUser(userRequest);
63+
attributes = oAuth2User.getAttributes();
64+
65+
/**
66+
* userRequest에서 registrationId 추출 후 registrationId으로 SocialType 저장
67+
* http://localhost:8080/oauth2/authorization/kakao에서 kakao가 registrationId
68+
* userNameAttributeName은 이후에 nameAttributeKey로 설정된다.
69+
*/
70+
71+
SocialType socialType = getSocialType(registrationId);
72+
73+
String userNameAttributeName = userRequest.getClientRegistration()
74+
.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); // OAuth2 로그인 시 키(PK)가 되는값
75+
attributes = oAuth2User.getAttributes(); // 소셜 로그인에서 API가 제공하는 userInfo의 Json 값(유저 정보들)
76+
77+
// socialType에 따라 유저 정보를 통해 OAuthAttributes 객체 생성
78+
79+
OAuthAttributes extractAttributes = OAuthAttributes.of(socialType, userNameAttributeName, attributes);
80+
81+
UserInfo createdUser = getUser(extractAttributes, socialType); // getUser() 메소드로 User 객체 생성 후 반환
82+
83+
// DefaultOAuth2User를 구현한 CustomOAuth2User 객체를 생성해서 반환
84+
return new CustomOAuth2User(
85+
Collections.singleton(new SimpleGrantedAuthority(createdUser.getRole().getKey())),
86+
attributes,
87+
extractAttributes.getNameAttributeKey(),
88+
createdUser.getEmail(),
89+
createdUser.getRole()
90+
);
91+
}
7092
}
7193

7294
private SocialType getSocialType(String registrationId) {
@@ -103,4 +125,23 @@ private UserInfo saveUser(OAuthAttributes attributes, SocialType socialType) {
103125
UserInfo createdUser = attributes.toEntity(socialType, attributes.getOauth2UserInfo());
104126
return userRepository.save(createdUser);
105127
}
128+
129+
private Map<String, Object> decodeJwtTokenPayload(String jwtToken) {
130+
Map<String, Object> jwtClaims = new HashMap<>();
131+
try {
132+
String[] parts = jwtToken.split("\\.");
133+
Base64.Decoder decoder = Base64.getUrlDecoder();
134+
135+
byte[] decodedBytes = decoder.decode(parts[1].getBytes(StandardCharsets.UTF_8));
136+
String decodedString = new String(decodedBytes, StandardCharsets.UTF_8);
137+
ObjectMapper mapper = new ObjectMapper();
138+
139+
Map<String, Object> map = mapper.readValue(decodedString, Map.class);
140+
jwtClaims.putAll(map);
141+
142+
} catch (JsonProcessingException e) {
143+
// logger.error("decodeJwtToken: {}-{} / jwtToken : {}", e.getMessage(), e.getCause(), jwtToken);
144+
}
145+
return jwtClaims;
146+
}
106147
}

0 commit comments

Comments
 (0)