일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- compateto
- AWS
- NestJS
- 자바스크립트
- 우아한테크코스
- api 요청 수 제한
- bucket4j
- 유효시간 설정 url
- 모던 자바스크립트
- api 비동기처리
- Deep Dive
- 프론트엔드 과제
- 타입스크립트
- 프리코스
- 스프링부트
- 코멘토 #코멘토실무PT #실무PT후기 #실무강의 #리액트강의 #웹프로그래밍 #react #웹개발실무
- 음악 url 파일 다운로드
- invalid_grant
- 딥다이브
- Dev-Matching
- oauth
- redis
- 검색
- 파일 url
- TypeORM
- 프론트엔드
- 프로그래머스
- concurrency limit
- this
- 우아한 테크코스
- Today
- Total
개발 알다가도 모르겠네요
[Spring] 애플 소셜 로그인을 구현해보자 본문
애플은 2019년 애플 로그인 기능을 발표한 동시에, App Store에 등록할 때 소셜 로그인이 하나라도 있다면 애플 로그인이 필수로 제공되어야 한다는 심사정책을 내놓았다.
5월에 MVP 출시 예정인 앱에는 소셜로그인 기능이 들어가기 때문에 애플로그인을 필수로 적용시켜야 하는 상황이었다.
문제는 애플로그인이 다른 소셜로그인들과 동작방식이 좀 다르다는 점이다.
애플의 경우에는 아래처럼
1. 서버 내부에서 별도로 client_secret라는 값을 생성해준 뒤에
2. 애플서버에 여러 설정정보를 함께 전달해줘야 한다.
3. 여기서 끝이 아니라 애플서버로부터 전달받은 id_token 값을 파싱해야 비로소 이메일과 같은 사용자 정보를 얻을 수 있었다.
여러 레퍼런스를 살펴보니까, Identity Token값을 활용해서 구현하는 방법도 있었는데,
Authorization Code만 클라이언트로부터 전달받아서 구현하는 방법이 그나마 공수가 덜 들 것 같아서 이 방법으로 구현했다.
[구현코드]
/**
* Apple 토큰 인증 API 호출 -> 응답받은 ID 토큰을 JWT Decoding 처리 -> AppleUserInfoResponseDto로 반환
* @param authorizationCode 사용자로부터 받은 인증 코드
* @return 디코딩된 사용자 정보를 담고 있는 AppleUserInfoResponseDto 객체
*/
public AppleUserInfoResponseDto getAppleUserProfile(String authorizationCode) throws IOException {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.valueOf(APPLICATION_FORM_URLENCODED_VALUE));
HttpEntity<String> request = new HttpEntity<>("client_id=" + appleProperties.getClientId() +
"&client_secret=" + generateClientSecret() +
"&grant_type=" + appleProperties.getGrantType() +
"&code=" + authorizationCode, headers);
ResponseEntity<AppleSocialTokenInfoResponseDto> response = socialConfig.restTemplate().exchange(
appleProperties.getAudience() + "/auth/token", HttpMethod.POST, request, AppleSocialTokenInfoResponseDto.class);
DecodedJWT decodedJWT = JWT.decode(Objects.requireNonNull(response.getBody()).getIdToken());
AppleUserInfoResponseDto appleUserInfoResponseDto = new AppleUserInfoResponseDto();
appleUserInfoResponseDto.setSubject(decodedJWT.getClaim("sub").asString());
appleUserInfoResponseDto.setEmail(decodedJWT.getClaim("email").asString());
return appleUserInfoResponseDto;
}
/**
* Apple의 인증 서버와의 통신에 사용될 JWT을 생성하기 위해 사용되는 ClientSecret
* ClientSecret은 토큰 요청 시 서명 목적으로 사용되며, 공개키/비공개키 인증 메커니즘이 포함됨
* @return 생성된 JWT ClientSecret
*/
private String generateClientSecret() {
LocalDateTime expiration = LocalDateTime.now().plusMinutes(5);
return Jwts.builder()
.setHeaderParam(JwsHeader.KEY_ID, appleProperties.getKeyId())
.setIssuer(appleProperties.getTeamId())
.setAudience(appleProperties.getAudience())
.setSubject(appleProperties.getClientId())
.setExpiration(Date.from(expiration.atZone(ZoneId.systemDefault()).toInstant()))
.setIssuedAt(new Date())
.signWith(getPrivateKey(), SignatureAlgorithm.ES256)
.compact();
}
/**
* 애플의 JWT 클라이언트 시크릿 생성을 위한 비공개 키 로드
* @return 로드된 RSA 비공개 키
*/
private PrivateKey getPrivateKey() {
Security.addProvider(new org.bouncycastle.jce.provider.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) {
throw new RuntimeException("Error converting private key from String", e);
}
}
/**
* 애플 유저 정보 DTO
*/
@Builder
@NoArgsConstructor(access = AccessLevel.PUBLIC)
@AllArgsConstructor
@Data
public class AppleUserInfoResponseDto {
// 고유ID
@JsonProperty("sub")
private String subject;
@JsonProperty("email")
private String email;
}
/**
* 애플 소셜 토큰 응답 정보 DTO
*/
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Data
public class AppleSocialTokenInfoResponseDto {
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("token_type")
private String tokenType;
@JsonProperty("expires_in")
private Long expiresIn;
@JsonProperty("refresh_token")
private String refreshToken;
@JsonProperty("id_token")
private String idToken;
}
[트러블슈팅]
애플 서버 /auth/token로 토큰 발급 요청시 Invalid_grant 에러 발생
1. Authorization Code의 유효시간은 5분, 일회용으로 제한되기 때문에 생긴 문제였다. 새로 발급받은 코드로 요청해서 해결했다.
2. 토큰 발급 시 파라미터로 들어가는 client_id값이 App ID가 아닌 Service ID를 사용했기 때문에 발생한 문제였다.
client_id는 애플 개발자 계정에서 Certificates, Identifiers & Profiles - Identifiers에서 설정한 id인데,
아래의 그림을 보면 APP IDs값과 Service IDs값을 사용하는 경우가 나눠져 있었다.
간단하게 설명하자면 소셜로그인을 ios앱에서 연동한다면 client_id값에 App IDs가 들어가게 되고,
웹사이트나 웹뷰 형식을 통해 연동하면 client_id값에 Service IDs가 들어가게 되는 것이다.
client_id값에 애플 디벨로퍼에서 설정한 App IDs 값으로 수정하니까 해결됐다.
[레퍼런스]
'웹 > Spring' 카테고리의 다른 글
[Spring] @Async를 이용해 비동기 처리를 해보자 (0) | 2024.05.16 |
---|---|
[Spring] SunoAI와 Spring Boot를 연동해보자 (0) | 2024.05.15 |
[Spring] Concurrency/Rate Limiter를 적용하여 API 요청을 제어해보자 (0) | 2023.08.25 |
Controller에서 jsp return하는 과정 (0) | 2023.02.21 |
스프링 빈 을 간단하게 알아보자 (0) | 2021.11.26 |