Skip to content

Commit

Permalink
feat: (#1) AppleLogin 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
BEMELON committed Mar 5, 2023
1 parent 3f68cf5 commit 2bf3e15
Show file tree
Hide file tree
Showing 9 changed files with 255 additions and 2 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.2' // JWT
implementation 'io.jsonwebtoken:jjwt-impl:0.11.2' // JWT
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2' // JWT
compileOnly group: 'org.bouncycastle', name: 'bcpkix-jdk14', version: '1.72'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/tune/server/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.tune.server.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tune.server.filter.AppleLoginFilter;
import com.tune.server.filter.ExceptionHandlerFilter;
import com.tune.server.filter.JwtAuthenticationFilter;
import com.tune.server.filter.KakaoLoginFilter;
Expand Down Expand Up @@ -47,6 +48,7 @@ protected DefaultSecurityFilterChain configure(HttpSecurity http) throws Excepti
.antMatchers("/member/**").authenticated()
.and()
.addFilterBefore(new KakaoLoginFilter("/login/kakao", objectMapper, memberService), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new AppleLoginFilter("/login/apple", objectMapper, memberService), KakaoLoginFilter.class)
.addFilterBefore(new ExceptionHandlerFilter(), KakaoLoginFilter.class)
.addFilterAfter(new JwtAuthenticationFilter(objectMapper, memberService), KakaoLoginFilter.class)
.build();
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/com/tune/server/dto/apple/AppleAuthTokenDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.tune.server.dto.apple;

import lombok.*;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class AppleAuthTokenDto {
private String access_token;
private String token_type;
private String expires_in;
private String refresh_token;
private String id_token;

@Setter
private String user_id;
}
13 changes: 13 additions & 0 deletions src/main/java/com/tune/server/dto/request/AppleLoginRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.tune.server.dto.request;

import lombok.*;

@Getter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class AppleLoginRequest {
private String authorizationCode;
private String user;
}
167 changes: 167 additions & 0 deletions src/main/java/com/tune/server/filter/AppleLoginFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package com.tune.server.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tune.server.domain.Member;
import com.tune.server.dto.apple.AppleAuthTokenDto;
import com.tune.server.dto.request.AppleLoginRequest;
import com.tune.server.dto.response.FullTokenResponse;
import com.tune.server.exceptions.login.TokenNotFoundException;
import com.tune.server.service.member.MemberService;
import com.tune.server.util.JwtUtil;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.security.PrivateKey;
import java.util.Collections;
import java.util.Date;

public class AppleLoginFilter extends OncePerRequestFilter {
private final String SIGNUP_URI;
private final String APPLE_TOKEN_URI = "https://appleid.apple.com/auth/token";
private final String APPLE_AUDIENCE_URI = "https://appleid.apple.com";
private final ObjectMapper objectMapper;
private final MemberService memberService;

@Value("${external.apple.clientId}")
private String clientId;

@Value("${external.apple.teamId}")
private String teamId;

@Value("${external.apple.keyId}")
private String keyId;

@Value("${external.apple.privateKey}")
private String appleSignKey;

public AppleLoginFilter(String signupUri, ObjectMapper objectMapper, MemberService memberService) {
this.SIGNUP_URI = signupUri;
this.objectMapper = objectMapper;
this.memberService = memberService;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (request.getRequestURI().equals(SIGNUP_URI) && request.getMethod().equals("POST")) {
// 1. AppleLoginRequest 파싱
AppleLoginRequest appleLoginRequest = getAppleLoginRequest(request);

// 2. Apple JWT 생성 및 검증
AppleAuthTokenDto appleAuthTokenDto = getAppleAuthTokenDto(appleLoginRequest);
appleAuthTokenDto.setUser_id(appleAuthTokenDto.getUser_id());

// 3. 회원정보로 로그인/회원가입
if (!memberService.isExistMember(appleAuthTokenDto)) {
// 회원가입
if (!memberService.signUp(appleAuthTokenDto)) {
throw new InternalError("회원가입에 실패했습니다.");
}
}

// 4. JWT 토큰 발급
Member member = memberService.getMember(appleAuthTokenDto);
FullTokenResponse fullTokenResponse = FullTokenResponse
.builder()
.access_token(JwtUtil.generateJwt(member))
.refresh_token(member.getRefreshToken())
.build();

// 5. 응답
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(fullTokenResponse));
return;
}

filterChain.doFilter(request, response);
}

private AppleAuthTokenDto getAppleAuthTokenDto(AppleLoginRequest appleLoginRequest) {
RestTemplate restTemplate = new RestTemplateBuilder().build();

MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("code", appleLoginRequest.getAuthorizationCode());
params.add("client_id", teamId);
params.add("client_secret", generateAppleSignKey());
params.add("grant_type", "authorization_code");

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers);

try {
ResponseEntity<AppleAuthTokenDto> response = restTemplate.postForEntity(APPLE_TOKEN_URI, httpEntity, AppleAuthTokenDto.class);
return response.getBody();
} catch (HttpClientErrorException e) {
throw new IllegalArgumentException("Apple Auth Token Error");
}
}

private String generateAppleSignKey() {
try {
PrivateKey privateKey = getPrivateKey();
return Jwts.builder()
.setHeaderParam("kid", keyId)
.setHeaderParam("alg", "ES256")
.setIssuer(teamId)
.setIssuedAt(new Date())
.setAudience(APPLE_AUDIENCE_URI)
.setExpiration(new Date(System.currentTimeMillis() + 86400000))
.setSubject(clientId)
.signWith(privateKey, SignatureAlgorithm.ES256)
.compact();
} catch (Exception e) {
throw new IllegalArgumentException("Apple Sign Key Error");
}
}

private AppleLoginRequest getAppleLoginRequest(HttpServletRequest request) {
try {
AppleLoginRequest appleLoginRequest = objectMapper.readValue(request.getInputStream(), AppleLoginRequest.class);
if (appleLoginRequest.getAuthorizationCode() == null) {
throw new TokenNotFoundException("Autorization Code is null");
}

return appleLoginRequest;
} catch (Exception e) {
throw new TokenNotFoundException("Apple Token Parse Error");
}
}

private PrivateKey getPrivateKey() throws IOException {
// PRIVATE_KEY 렌더링
appleSignKey = appleSignKey.replace(' ', '\n');
String SSH_SUFFIX = "\n-----END PRIVATE KEY-----";
String SSH_PREFIX = "-----BEGIN PRIVATE KEY-----\n";
appleSignKey = SSH_PREFIX + appleSignKey + SSH_SUFFIX;

Reader pemReader = new StringReader(appleSignKey);
PEMParser pemParser = new PEMParser(pemReader);
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject();
return converter.getPrivateKey(object);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.tune.server.domain.Member;
import com.tune.server.dto.MemberAuthDto;
import com.tune.server.dto.apple.AppleAuthTokenDto;
import com.tune.server.dto.kakao.KakaoUserInfo;
import com.tune.server.dto.request.MemberAgreementRequest;
import com.tune.server.dto.request.MemberNameRequest;
Expand All @@ -15,11 +16,12 @@
@Service
public interface MemberService {
boolean isExistMember(KakaoUserInfo kakaoUserInfo);

boolean isExistMember(AppleAuthTokenDto appleAuthTokenDto);
boolean signUp(KakaoUserInfo kakaoUserInfo);
boolean signUp(AppleAuthTokenDto appleAuthTokenDto);

Member getMember(KakaoUserInfo kakaoUserInfo);

Member getMember(AppleAuthTokenDto appleAuthTokenDto);
Map<String, String> refresh(String refreshToken);

Member updateAgreement(MemberAuthDto member, MemberAgreementRequest request);
Expand All @@ -35,4 +37,5 @@ public interface MemberService {
Member updateWorkspacePurpose(MemberAuthDto principal, MemberPurposeRequest request);

MemberOnboardingResponse getOnboardingStatus(MemberAuthDto principal);

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.tune.server.domain.*;
import com.tune.server.dto.MemberAuthDto;
import com.tune.server.dto.apple.AppleAuthTokenDto;
import com.tune.server.dto.kakao.KakaoUserInfo;
import com.tune.server.dto.request.MemberAgreementRequest;
import com.tune.server.dto.request.MemberNameRequest;
Expand Down Expand Up @@ -39,6 +40,11 @@ public boolean isExistMember(KakaoUserInfo id) {
return memberProvider.isPresent();
}

@Override
public boolean isExistMember(AppleAuthTokenDto appleAuthTokenDto) {
return memberProviderRepository.findByProviderIdAndAndProvider(appleAuthTokenDto.getUser_id(), "APPLE").isPresent();
}

@Override
@Transactional
public boolean signUp(KakaoUserInfo kakaoUserInfo) {
Expand All @@ -65,12 +71,44 @@ public boolean signUp(KakaoUserInfo kakaoUserInfo) {
}
}

@Override
public boolean signUp(AppleAuthTokenDto appleAuthTokenDto) {
try {
Member member = Member
.builder()
.refreshToken(JwtUtil.generateRefreshToken())
.refreshTokenExpiresAt(LocalDateTime.now().plusMonths(JwtUtil.REFRESH_TOKEN_EXPIRES_MONTH))
.build();

MemberProvider memberProvider = MemberProvider.builder()
.providerId(appleAuthTokenDto.getUser_id())
.refreshToken(appleAuthTokenDto.getRefresh_token())
.member(member)
.provider("APPLE")
.build();

memberRepository.save(member);
memberProviderRepository.save(memberProvider);
return true;
} catch (Exception e) {
log.error("회원가입 실패", e);
return false;
}
}

@Override
public Member getMember(KakaoUserInfo kakaoUserInfo) {
Optional<MemberProvider> memberProvider = memberProviderRepository.findByProviderIdAndAndProvider(kakaoUserInfo.getId().toString(), "KAKAO");
return memberProvider.map(MemberProvider::getMember).orElseThrow(() -> new MemberNotFoundException("해당하는 회원이 없습니다."));
}

@Override
public Member getMember(AppleAuthTokenDto appleAuthTokenDto) {
return memberProviderRepository.findByProviderIdAndAndProvider(appleAuthTokenDto.getUser_id(), "APPLE")
.map(MemberProvider::getMember)
.orElseThrow(() -> new MemberNotFoundException("해당하는 회원이 없습니다."));
}

@Override
@Transactional
public Map<String, String> refresh(String refreshToken) {
Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/application-server.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,8 @@ server:
external:
jwt:
secret: ${JWT_SECRET}
apple:
client-id: ${APPLE_CLIENT_ID}
team-id: ${APPLE_TEAM_ID}
bundle-id: ${APPLE_TEAM_ID}
private-key: ${APPLE_PRIVATE_KEY}
5 changes: 5 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,8 @@ server:
external:
jwt:
secret: ${JWT_SECRET}
apple:
client-id: ${APPLE_CLIENT_ID}
team-id: ${APPLE_TEAM_ID}
bundle-id: ${APPLE_TEAM_ID}
private-key: ${APPLE_PRIVATE_KEY}

0 comments on commit 2bf3e15

Please sign in to comment.