Skip to content

Commit e910065

Browse files
committed
✨ 애플 콜백 작성
1 parent 70f887c commit e910065

File tree

6 files changed

+246
-22
lines changed

6 files changed

+246
-22
lines changed

src/main/java/com/boggle_boggle/bbegok/config/properties/AppleProperties.java

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
11
package com.boggle_boggle.bbegok.config.properties;
22

3+
import io.jsonwebtoken.Jwts;
4+
import io.jsonwebtoken.SignatureAlgorithm;
35
import lombok.Getter;
46
import lombok.Setter;
7+
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
8+
import org.bouncycastle.openssl.PEMParser;
9+
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
510
import org.springframework.boot.context.properties.ConfigurationProperties;
11+
import org.springframework.core.io.ClassPathResource;
12+
import org.springframework.http.*;
613
import org.springframework.stereotype.Component;
14+
import org.springframework.util.LinkedMultiValueMap;
15+
import org.springframework.util.MultiValueMap;
16+
import org.springframework.web.client.HttpClientErrorException;
17+
import org.springframework.web.client.RestTemplate;
18+
import java.io.IOException;
19+
import java.io.Reader;
20+
import java.io.StringReader;
21+
import java.security.PrivateKey;
22+
import java.util.Collections;
23+
import java.util.Date;
24+
import java.util.HashMap;
25+
import java.util.Map;
726

827
@Setter
928
@Component
@@ -20,15 +39,77 @@ public static class Auth {
2039
private String teamId;
2140
private String keyId;
2241
private String keyPath;
23-
private String clientId;
2442
private String redirectUri;
2543
private String iss;
26-
private String aud;
44+
private String aud; //client-id
2745

2846
public String getAppleLoginUrl(String redirectUri) {
2947
return iss + "/auth/authorize"
3048
+ "?client_id=" + aud
31-
+ "&redirect_uri=" + redirectUri
32-
+ "&response_type=code%20id_token&scope=name%20email&response_mode=form_post";
49+
+ "&redirect_uri=" + this.redirectUri
50+
+ "&response_type=code%20id_token&scope=name%20email&response_mode=form_post"
51+
+ "&state=" + redirectUri;
3352
}
53+
54+
public String generateAuthToken(String code) throws IOException {
55+
if (code == null) throw new IllegalArgumentException("Failed get authorization code");
56+
57+
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
58+
params.add("grant_type", "authorization_code");
59+
params.add("client_id", aud);
60+
params.add("client_secret", createClientSecretKey());
61+
params.add("code", code);
62+
params.add("redirect_uri", redirectUri);
63+
64+
RestTemplate restTemplate = new RestTemplate();
65+
66+
HttpHeaders headers = new HttpHeaders();
67+
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
68+
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
69+
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers);
70+
71+
try {
72+
ResponseEntity<String> response = restTemplate.exchange(
73+
iss + "/auth/token",
74+
HttpMethod.POST,
75+
httpEntity,
76+
String.class
77+
);
78+
return response.getBody();
79+
} catch (HttpClientErrorException e) {
80+
throw new IllegalArgumentException("Apple Auth Token Error");
81+
}
82+
}
83+
84+
public String createClientSecretKey() throws IOException {
85+
// headerParams 적재
86+
Map<String, Object> headerParamsMap = new HashMap<>();
87+
headerParamsMap.put("kid", keyId);
88+
headerParamsMap.put("alg", "ES256");
89+
90+
// clientSecretKey 생성
91+
return Jwts
92+
.builder()
93+
.setHeaderParams(headerParamsMap)
94+
.setIssuer(teamId)
95+
.setIssuedAt(new Date(System.currentTimeMillis()))
96+
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 30)) // 만료 시간 (30초)
97+
.setAudience(iss)
98+
.setSubject(aud)
99+
.signWith(SignatureAlgorithm.ES256, getPrivateKey())
100+
.compact();
101+
}
102+
103+
private PrivateKey getPrivateKey() throws IOException {
104+
ClassPathResource resource = new ClassPathResource(keyPath);
105+
String privateKey = new String(resource.getInputStream().readAllBytes());
106+
107+
Reader pemReader = new StringReader(privateKey);
108+
PEMParser pemParser = new PEMParser(pemReader);
109+
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
110+
PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject();
111+
112+
return converter.getPrivateKey(object);
113+
}
114+
34115
}

src/main/java/com/boggle_boggle/bbegok/controller/AppleController.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.boggle_boggle.bbegok.controller;
22

33
import com.boggle_boggle.bbegok.dto.base.DataResponseDto;
4+
import com.boggle_boggle.bbegok.entity.user.User;
5+
import com.boggle_boggle.bbegok.exception.exception.GeneralException;
46
import com.boggle_boggle.bbegok.service.AppleService;
57
import jakarta.servlet.http.HttpServletRequest;
68
import jakarta.servlet.http.HttpServletResponse;
@@ -19,12 +21,22 @@
1921
public class AppleController {
2022

2123
private final AppleService appleService;
24+
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
2225

2326
@GetMapping("/oauth2/apple")
2427
public void loginRequest(HttpServletResponse response,
2528
@RequestParam(value = "redirect_uri", required = true) String redirectUri) throws IOException {
2629
response.sendRedirect(appleService.getAppleLoginUrl(redirectUri));
2730
}
2831

32+
@PostMapping("/login/oauth2/code/apple")
33+
public void callback(HttpServletRequest request, HttpServletResponse response) throws IOException {
34+
User user = appleService.process(request.getParameter("code"));
2935

36+
if(user != null) {
37+
String accessToken = appleService.loginSuccess(request, response, user);
38+
redirectStrategy.sendRedirect(request, response, appleService.determineSuccessRedirectUrl(accessToken, request.getParameter("state")));
39+
}
40+
else throw new GeneralException();
41+
}
3042
}

src/main/java/com/boggle_boggle/bbegok/entity/user/User.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ public class User {
3737
@Size(max = 128)
3838
private String password;
3939

40+
@JsonIgnore
41+
@Column(name = "access_token", length = 512)
42+
private String accessToken;
43+
4044
@Column(name = "email", length = 512, unique = true, nullable = true)
4145
@Size(max = 512)
4246
private String email;
@@ -104,6 +108,18 @@ public static User createUser(
104108
return new User(userId, emailVerifiedYn, providerType, roleType, createdAt, modifiedAt);
105109
}
106110

111+
public static User createUser(
112+
@NotNull String userId,
113+
@NotNull String emailVerifiedYn,
114+
@NotNull ProviderType providerType,
115+
@NotNull RoleType roleType,
116+
@NotNull LocalDateTime createdAt,
117+
@NotNull LocalDateTime modifiedAt,
118+
@NotNull String accessToken
119+
){
120+
return new User(userId, emailVerifiedYn, providerType, roleType, createdAt, modifiedAt);
121+
}
122+
107123
public void updateNickName(String nickName){
108124
this.userName = nickName;
109125
}

src/main/java/com/boggle_boggle/bbegok/oauth/info/impl/AppleOAuth2UserInfo.java

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,156 @@
11
package com.boggle_boggle.bbegok.service;
22

3+
import com.boggle_boggle.bbegok.config.properties.AppProperties;
34
import com.boggle_boggle.bbegok.config.properties.AppleProperties;
5+
import com.boggle_boggle.bbegok.entity.user.User;
6+
import com.boggle_boggle.bbegok.entity.user.UserRefreshToken;
7+
import com.boggle_boggle.bbegok.entity.user.UserSettings;
8+
import com.boggle_boggle.bbegok.oauth.entity.ProviderType;
9+
import com.boggle_boggle.bbegok.oauth.entity.RoleType;
10+
import com.boggle_boggle.bbegok.oauth.exception.OAuthProviderMissMatchException;
11+
import com.boggle_boggle.bbegok.oauth.handler.OAuth2AuthenticationSuccessHandler;
12+
import com.boggle_boggle.bbegok.oauth.repository.OAuth2AuthorizationRequestBasedOnCookieRepository;
13+
import com.boggle_boggle.bbegok.oauth.token.AuthToken;
14+
import com.boggle_boggle.bbegok.oauth.token.AuthTokenProvider;
15+
import com.boggle_boggle.bbegok.repository.user.UserRefreshTokenRepository;
16+
import com.boggle_boggle.bbegok.repository.user.UserRepository;
17+
import com.boggle_boggle.bbegok.repository.user.UserSettingsRepository;
18+
import com.boggle_boggle.bbegok.utils.CookieUtil;
19+
import com.boggle_boggle.bbegok.utils.UuidUtil;
20+
import com.fasterxml.jackson.core.JsonProcessingException;
421
import com.fasterxml.jackson.databind.ObjectMapper;
22+
import com.google.gson.JsonObject;
23+
import com.google.gson.JsonParser;
524
import com.nimbusds.jwt.ReadOnlyJWTClaimsSet;
625
import com.nimbusds.jwt.SignedJWT;
26+
import jakarta.servlet.http.HttpServletRequest;
27+
import jakarta.servlet.http.HttpServletResponse;
728
import lombok.RequiredArgsConstructor;
829
import lombok.extern.slf4j.Slf4j;
9-
import net.minidev.json.JSONObject;
1030
import net.minidev.json.parser.JSONParser;
31+
import net.minidev.json.parser.ParseException;
1132
import org.springframework.boot.context.properties.EnableConfigurationProperties;
33+
import org.springframework.http.HttpEntity;
34+
import org.springframework.http.HttpMethod;
35+
import org.springframework.http.MediaType;
36+
import org.springframework.http.ResponseEntity;
1237
import org.springframework.stereotype.Service;
38+
import org.springframework.util.LinkedMultiValueMap;
39+
import org.springframework.util.MultiValueMap;
40+
import org.springframework.web.client.HttpClientErrorException;
41+
import org.springframework.web.client.RestTemplate;
42+
import org.springframework.web.util.UriComponentsBuilder;
43+
44+
import java.io.IOException;
45+
import java.net.http.HttpHeaders;
46+
import java.time.LocalDateTime;
47+
import java.util.Collections;
48+
import java.util.Date;
49+
50+
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.DEVICE_CODE;
51+
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.REFRESH_TOKEN;
1352

1453
@Slf4j
1554
@Service
1655
@RequiredArgsConstructor
1756
@EnableConfigurationProperties({ AppleProperties.class })
1857
public class AppleService {
1958
private final AppleProperties appleProperties;
59+
private final UserRepository userRepository;
60+
private final UserSettingsRepository userSettingsRepository;
61+
private final AccessTokenService accessTokenService;
62+
private final AuthTokenProvider tokenProvider;
63+
private final AppProperties appProperties;
64+
private final UserRefreshTokenRepository userRefreshTokenRepository;
2065

2166
public String getAppleLoginUrl(String redirectUri) {
2267
return appleProperties.getAppleLoginUrl(redirectUri);
2368
}
2469

70+
public User process(String code) {
71+
User savedUser = null;
72+
try {
73+
JsonObject jsonObj = (JsonObject) JsonParser.parseString(appleProperties.generateAuthToken(code));
74+
String accessToken = String.valueOf(jsonObj.get("access_token"));
75+
76+
// ID TOKEN을 통해 회원 고유 식별자 받기
77+
SignedJWT signedJWT = SignedJWT.parse(String.valueOf(jsonObj.get("id_token")));
78+
ReadOnlyJWTClaimsSet getPayload = signedJWT.getJWTClaimsSet();
79+
80+
ObjectMapper objectMapper = new ObjectMapper();
81+
JsonObject payload = objectMapper.readValue(getPayload.toJSONObject().toJSONString(), JsonObject.class);
82+
83+
String userId = String.valueOf(payload.get("sub"));
84+
savedUser = userRepository.findByUserIdAndIsDeleted(userId, false);
85+
86+
if (savedUser != null) { //서로 다른 인증제공자간 충돌을 방지
87+
if (ProviderType.APPLE != savedUser.getProviderType()) {
88+
throw new OAuthProviderMissMatchException(
89+
"Looks like you're signed up with " + ProviderType.APPLE +
90+
" account. Please use your " + savedUser.getProviderType() + " account to login."
91+
);
92+
}
93+
} else {
94+
savedUser = createAppleUser(userId,accessToken);
95+
userSettingsRepository.saveAndFlush(UserSettings.createUserSettings(savedUser));
96+
}
97+
98+
return savedUser;
99+
100+
} catch (JsonProcessingException e) {
101+
throw new RuntimeException("Failed to parse json data");
102+
} catch (IOException | java.text.ParseException e) {
103+
throw new RuntimeException(e);
104+
}
105+
}
106+
107+
public String loginSuccess(HttpServletRequest request, HttpServletResponse response, User user) {
108+
// access 토큰 설정 : GUEST는 그냥 저장, User의 경우 약관정보 확인 후 LIMITED_USER 또는 USER를 저장
109+
Date now = new Date();
110+
AuthToken accessToken = accessTokenService.createAccessToken(user, user.getRoleType(), now);
111+
112+
// refresh 토큰 설정
113+
long refreshTokenExpiry = appProperties.getAuth().getRefreshTokenExpiry();
114+
AuthToken refreshToken = tokenProvider.createAuthToken(
115+
appProperties.getAuth().getTokenSecret(),
116+
new Date(now.getTime() + refreshTokenExpiry)
117+
);
118+
119+
// 디바이스코드 생성 및 리프레쉬토큰 DB에 저장
120+
String deviceId = UuidUtil.createUUID().toString();
121+
userRefreshTokenRepository.saveAndFlush(UserRefreshToken.createUserRefreshToken(user, refreshToken.getToken(), deviceId));
122+
123+
//디바이스코드, 토큰을 쿠키에 저장
124+
saveCookie(response, request, DEVICE_CODE, refreshTokenExpiry, deviceId);
125+
saveCookie(response, request, REFRESH_TOKEN, refreshTokenExpiry, refreshToken.getToken());
126+
127+
return accessToken.getToken();
128+
}
129+
130+
public String determineSuccessRedirectUrl(String accessToken, String baseUrl) {
131+
return UriComponentsBuilder.fromUriString(baseUrl)
132+
.queryParam("token", accessToken)
133+
.build().toUriString();
134+
}
135+
136+
private User createAppleUser(String userId, String accessToken) {
137+
LocalDateTime now = LocalDateTime.now();
138+
User user = User.createUser(
139+
userId,
140+
"Y",
141+
ProviderType.APPLE,
142+
RoleType.GUEST,
143+
now,
144+
now,
145+
accessToken
146+
);
147+
148+
return userRepository.saveAndFlush(user);
149+
}
150+
151+
protected void saveCookie(HttpServletResponse response, HttpServletRequest request, String cookieName, long tokenExpiry, String tokenValue) {
152+
int cookieMaxAge = (int) tokenExpiry / 60;
153+
CookieUtil.deleteCookie(request, response, cookieName);
154+
CookieUtil.addCookie(response, cookieName, tokenValue, cookieMaxAge);
155+
}
25156
}

src/main/java/com/boggle_boggle/bbegok/service/UserService.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,8 @@ public boolean isNicknameAvailable(String userSeq, String nickname) {
6060
public String getAuthorization(String userSeq) {
6161
User user = getUser(userSeq);
6262
RoleType role = user.getRoleType();
63-
String recentUpdatedVersion = termsRepository.getLatestTermsVersion();
64-
6563
if(role.equals(RoleType.USER)) {
64+
String recentUpdatedVersion = termsRepository.getLatestTermsVersion();
6665
if(user.getAgreedVersion()==null || !user.getAgreedVersion().equals(recentUpdatedVersion)) role = RoleType.LIMITED_USER;
6766
}
6867

0 commit comments

Comments
 (0)