Skip to content

Commit

Permalink
feat(#5) : 애플 로그인 후 회원 탈퇴 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
aeeazip committed Aug 4, 2023
1 parent a969378 commit e6ebd9b
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 11 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ out/

### Security Files ###
#src/main/resources/application-secret.yml
src/main/resources/application-prod.yml
#src/main/resources/application-prod.yml
#src/main/resources/key/AuthKey_9LA3NLSR6X.p8

### Log Files ###
*.log
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter'

// 애플 로그인을 위한 FeignClient 연동
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package trothly.trothcam.auth.apple;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
Expand All @@ -9,6 +10,9 @@
import trothly.trothcam.exception.custom.InvalidTokenException;

import java.security.PublicKey;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.Map;
import java.util.logging.Logger;

Expand Down
58 changes: 58 additions & 0 deletions src/main/java/trothly/trothcam/auth/apple/PublicKeyGenerator.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
package trothly.trothcam.auth.apple;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
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.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;
import trothly.trothcam.dto.auth.apple.ApplePublicKey;
import trothly.trothcam.dto.auth.apple.ApplePublicKeys;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Slf4j
Expand All @@ -22,6 +39,15 @@ public class PublicKeyGenerator {
private static final String KEY_ID_HEADER_KEY = "kid";
private static final int POSITIVE_SIGN_NUMBER = 1;

@Value("${oauth.apple.iss}")
private String iss;

@Value("${oauth.apple.client-id}")
private String clientId;

@Value("${oauth.apple.key.path}")
private String path;

// Public Key 생성
public PublicKey generatePublicKey(Map<String, String> headers, ApplePublicKeys applePublicKeys) {
ApplePublicKey applePublicKey =
Expand All @@ -48,4 +74,36 @@ private PublicKey generatePublicKeyWithApplePublicKey(ApplePublicKey publicKey)
throw new IllegalStateException("Apple OAuth 로그인 중 public key 생성에 문제가 발생했습니다.");
}
}

// Client Secret 발급
public String createClientSecret() throws IOException {
// 애플에서 유효기간 최대 30일 권고
Date expirationDate = Date.from(LocalDateTime.now().plusDays(30).atZone(ZoneId.systemDefault()).toInstant());

try {
return Jwts.builder()
.setHeaderParam(KEY_ID_HEADER_KEY, "9LA3NLSR6X")
.setHeaderParam(SIGN_ALGORITHM_HEADER_KEY, "ES256")
.setIssuer("5JQS3FU5R6")
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(expirationDate)
.setAudience(iss)
.setSubject(clientId)
.signWith(SignatureAlgorithm.ES256, getPrivateKey())
.compact();
} catch(IOException e) {
throw new RuntimeException(e);
}
}

public PrivateKey getPrivateKey() throws IOException {
ClassPathResource resource = new ClassPathResource(path);
String privateKey = new String(Files.readAllBytes(Paths.get(resource.getURI())));

Reader pemReader = new StringReader(privateKey);
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 @@ -30,6 +30,8 @@
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import static trothly.trothcam.exception.base.ErrorCode.REQUEST_ERROR;

Expand Down Expand Up @@ -110,10 +112,11 @@ public BaseResponse<String> logout(HttpServletRequest request) {
throw new UnauthorizedException("유효하지 않거나 만료된 토큰입니다.");
}

// 회원 탈퇴
@DeleteMapping("/withdraw")
public BaseResponse<String> withdraw(@AuthenticationPrincipal Member member) {
String result = oauthService.withdraw(member);
// 애플 로그인 -> 회원 탈퇴
@DeleteMapping("/apple-revoke")
public BaseResponse<String> appleRevoke(@AuthenticationPrincipal Member member, String refreshToken) throws IOException {
oauthService.appleRevoke(refreshToken);
String result = oauthService.updateStatus(member);
return BaseResponse.onSuccess(result);
}

Expand Down
10 changes: 10 additions & 0 deletions src/main/java/trothly/trothcam/domain/member/Member.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package trothly.trothcam.domain.member;

import lombok.*;
import org.hibernate.annotations.ColumnDefault;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import trothly.trothcam.domain.core.BaseTimeEntity;
Expand Down Expand Up @@ -33,6 +34,10 @@ public class Member extends BaseTimeEntity implements UserDetails {
@Column(name = "image", length = 255, nullable = true)
private String image;

@Column(name = "status", nullable = false)
@ColumnDefault("'active'")
private String status;

@Column(name = "provider", nullable = false)
@Enumerated(EnumType.STRING)
private Provider provider;
Expand Down Expand Up @@ -65,6 +70,11 @@ public void refreshTokenExpires() {
this.refreshTokenExpiresAt = LocalDateTime.now();
}

// status 변경
public void updateStatus(String status) {
this.status = status;
}

/* 웹 */
@Column(name = "web_id")
private String webId;
Expand Down
59 changes: 53 additions & 6 deletions src/main/java/trothly/trothcam/service/auth/OAuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
//import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import trothly.trothcam.auth.apple.PublicKeyGenerator;
import trothly.trothcam.dto.auth.global.ProfileResDto;
import trothly.trothcam.dto.auth.global.TokenDto;
import trothly.trothcam.dto.auth.apple.AppleInfo;
Expand All @@ -30,7 +32,16 @@
import javax.servlet.http.HttpServletResponse;
//import javax.transaction.Transactional;
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import static trothly.trothcam.exception.base.ErrorCode.ALREADY_LOGOUT;
import static trothly.trothcam.exception.base.ErrorCode.MEMBER_NOT_FOUND;
Expand All @@ -39,8 +50,12 @@
@Service
@RequiredArgsConstructor
public class OAuthService {
@Value("${oauth.apple.client-id}")
private String clientId;

private final Logger logger = LoggerFactory.getLogger(getClass());
private final AppleOAuthUserProvider appleOAuthUserProvider;
private final PublicKeyGenerator publicKeyGenerator;

private final MemberRepository memberRepository;
// private final RedisTemplate<String, String> redisTemplate;
Expand Down Expand Up @@ -233,17 +248,49 @@ public String logout(String refreshToken) {
return "로그아웃 성공";
}

// 회원탈퇴
@Transactional
public String withdraw(Member member) {
// 애플 로그인 -> 회원 탈퇴
public void appleRevoke(String refreshToken) throws IOException {
String url = "https://appleid.apple.com/auth/revoke";
String clientSecret = publicKeyGenerator.createClientSecret();

Map<String, String> params = new HashMap<>();
params.put("client_secret", clientSecret);
params.put("token", refreshToken);
params.put("client_id", clientId);

try {
HttpRequest getRequest = HttpRequest.newBuilder()
.uri(new URI(url))
.POST(getParamsUrlEncoded(params))
.headers("Content-Type", "application/x-www-form-urlencoded")
.build();

HttpClient httpClient = HttpClient.newHttpClient();
httpClient.send(getRequest, HttpResponse.BodyHandlers.ofString());
} catch (Exception e) {
e.printStackTrace();
}
}

public HttpRequest.BodyPublisher getParamsUrlEncoded(Map<String, String> parameters) {
String urlEncoded = parameters.entrySet()
.stream()
.map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
return HttpRequest.BodyPublishers.ofString(urlEncoded);
}

// 회원탈퇴 후 status 변경
public String updateStatus(Member member) {
Optional<Member> getMember = memberRepository.findById(member.getId());
if(getMember.isEmpty())
throw new BaseException(MEMBER_NOT_FOUND);

memberRepository.delete(member);

member.updateStatus("inactive");
memberRepository.save(member);
return "회원탈퇴 성공";
}

// 개인정보 조회
public ProfileResDto getProfile(Member member) {
Optional<Member> getMember = memberRepository.findById(member.getId());
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ oauth:
apple:
iss: ${APPLE_ISSUE}
client-id: ${APPLE_BUNDLE_ID}
key:
path: /key/AuthKey_9LA3NLSR6X.p8

jwt:
secret: ${JWT_SECRET}
Expand Down
51 changes: 51 additions & 0 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://troth-cam.crojjqnidysp.ap-northeast-2.rds.amazonaws.com:3306/trothcam_db?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: trothly
password: teamtrothly

config:
activate:
on-profile: prod
# import: application-secret.yml

jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
show-sql: true
database: mysql
# Initial Data config
defer-datasource-initialization: true

# redis:
# host: localhost
# port: 6379

oauth:
apple:
iss: https://appleid.apple.com
client-id: com.parkjju.trothcam
key:
path: /key/AuthKey_9LA3NLSR6X.p8

jwt:
secret: a2FrYW9jbG91ZHNjaG9vbGxvY2Fsa2FrYW9jbG91ZGMK

app:
google:
url: https://accounts.google.com/o/oauth2/v2/auth
callback:
url: https://trothly.com/auth/google/callback
client:
id: 719044852761-5aiakgngg693sa7822dk7b7je52ieo11.apps.googleusercontent.com
secret: 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
auth:
scope: profile,email,openid
token:
url: https://oauth2.googleapis.com/token
userinfo:
url: https://www.googleapis.com/oauth2/v1/userinfo
6 changes: 6 additions & 0 deletions src/main/resources/key/AuthKey_9LA3NLSR6X.p8
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgpgKWeLlj8nev2fpz
ooqtVfEdsJxVBk77+Q3Y8A55bzKgCgYIKoZIzj0DAQehRANCAAQ/h+mLXHI1qKtj
U2K/RX2aHif1IOTC3zXx4ALhu/LZW/7mRm0p8JSN597ltQHPNLONC2nL1FhEOpUj
Gblb8CJW
-----END PRIVATE KEY-----

0 comments on commit e6ebd9b

Please sign in to comment.