Skip to content

Commit

Permalink
Merge pull request #58 from dnd-side-project/feat/#57
Browse files Browse the repository at this point in the history
[#57] 탈퇴 후 재로그인 문제 수정
  • Loading branch information
youngreal authored Nov 1, 2024
2 parents 0f793b6 + 787d54a commit 35ae8f8
Show file tree
Hide file tree
Showing 15 changed files with 224 additions and 90 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation:3.1.0'

//OpenFeign
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.3'
Expand Down
44 changes: 12 additions & 32 deletions src/main/java/com/dnd/dndtravel/auth/controller/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@
import com.dnd.dndtravel.auth.controller.request.AppleWithdrawRequest;
import com.dnd.dndtravel.auth.controller.request.ReIssueTokenRequest;
import com.dnd.dndtravel.auth.controller.swagger.AuthControllerSwagger;
import com.dnd.dndtravel.auth.service.dto.response.AppleIdTokenPayload;
import com.dnd.dndtravel.auth.service.AppleOAuthService;
import com.dnd.dndtravel.auth.service.AuthService;
import com.dnd.dndtravel.auth.service.JwtTokenService;
import com.dnd.dndtravel.auth.controller.request.AppleLoginRequest;
import com.dnd.dndtravel.auth.service.dto.response.TokenResponse;
import com.dnd.dndtravel.auth.service.dto.response.ReissueTokenResponse;
import com.dnd.dndtravel.config.AuthenticationMember;
import com.dnd.dndtravel.member.domain.Member;
import com.dnd.dndtravel.member.service.MemberService;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
Expand All @@ -22,43 +19,26 @@
@RequiredArgsConstructor
@RestController
public class AuthController implements AuthControllerSwagger {
private final AppleOAuthService appleOAuthService;
private final AuthService authService;
private final JwtTokenService jwtTokenService;
private final MemberService memberService;

@PostMapping("/login/oauth2/apple")
public ResponseEntity<TokenResponse> appleOAuthLogin(@RequestBody AppleLoginRequest appleLoginRequest) {
// 클라이언트에서 준 code 값으로 apple의 IdToken Payload를 얻어온다
AppleIdTokenPayload tokenPayload = appleOAuthService.get(appleLoginRequest.appleToken());

// apple에서 가져온 유저정보를 DB에 저장
Member member = memberService.saveMember(tokenPayload.email(), appleLoginRequest.selectedColor());

// 클라이언트와 주고받을 user token(access , refresh) 생성
TokenResponse tokenResponse = jwtTokenService.generateTokens(member.getId());

// refresh token 재발급 필요시
if (tokenResponse == null) {
return ResponseEntity.noContent().build();
}

return ResponseEntity.ok(tokenResponse);
public ResponseEntity<TokenResponse> appleOAuthLogin(@RequestBody @Valid AppleLoginRequest request) {
return authService.processAppleLogin(request.authorizationCode(), request.selectedColor())
.map(ResponseEntity::ok)
.orElse(ResponseEntity.noContent().build());
}

@PostMapping("/reissue/token")
public ReissueTokenResponse reissueToken(@RequestBody ReIssueTokenRequest reissueTokenRequest) {
public ReissueTokenResponse reissueToken(@RequestBody @Valid ReIssueTokenRequest reissueTokenRequest) {
return jwtTokenService.reIssue(reissueTokenRequest.refreshToken());
}

@DeleteMapping("/withdraw")
public void withdraw(@Valid @RequestBody AppleWithdrawRequest withdrawRequest, AuthenticationMember authenticationMember) {
// 1. Apple 서버에서 Access Token 받아오기
String accessToken = appleOAuthService.getAccessToken(withdrawRequest.authorizationCode());

// 2. Apple 서버에 탈퇴 요청
appleOAuthService.revoke(accessToken);

// 3. 자체 회원 탈퇴 진행
memberService.withdrawMember(authenticationMember.id());
public void withdraw(
@RequestBody @Valid AppleWithdrawRequest withdrawRequest,
AuthenticationMember authenticationMember
) {
authService.processAppleRevoke(withdrawRequest.refreshToken(), authenticationMember.id());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public record AppleLoginRequest(
@Schema(description = "authorization code", requiredMode = REQUIRED)
@NotBlank(message = "authorization code는 필수 입니다.")
@Size(max = 300, message = "authorization code 형식이 아닙니다")
String appleToken,
String authorizationCode,

@Schema(description = "유저가 선택한 색상", requiredMode = REQUIRED)
@ColorValidation(enumClass = SelectedColor.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

/*
클라이언트에서 서버로 보내는 요청
*/
public record AppleWithdrawRequest(
@Schema(description = "authorization code", requiredMode = REQUIRED)
@NotBlank(message = "authorization code는 필수 입니다.")
@Size(max = 300, message = "authorization code 형식이 아닙니다.")
String authorizationCode
@Schema(description = "애플 refreshToken", requiredMode = REQUIRED)
@NotBlank(message = "refreshToken은 필수 입니다.")
@Size(max = 300, message = "refreshToken 형식이 아닙니다.")
String refreshToken
) {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
package com.dnd.dndtravel.auth.controller.request;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record ReIssueTokenRequest(
@Schema(description = "mapddang refresh token", requiredMode = REQUIRED)
@NotBlank(message = "refresh token은 필수 입니다.")
@Size(max = 300, message = "refresh token 형식이 아닙니다.")
String refreshToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.dnd.dndtravel.auth.exception;

public class RequireReAuthenticationException extends RuntimeException {
private static final String MESSAGE = "Apple 계정 재인증이 필요합니다.";

public RequireReAuthenticationException(Exception e) {
super(MESSAGE, e);
}
}
50 changes: 21 additions & 29 deletions src/main/java/com/dnd/dndtravel/auth/service/AppleOAuthService.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package com.dnd.dndtravel.auth.service;

import com.dnd.dndtravel.auth.exception.AppleTokenRevokeException;
import com.dnd.dndtravel.auth.exception.RequireReAuthenticationException;
import com.dnd.dndtravel.auth.service.dto.response.AppleSocialTokenInfoResponse;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwts;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.springframework.stereotype.Component;
Expand All @@ -20,6 +19,7 @@

import com.dnd.dndtravel.auth.service.dto.response.AppleIdTokenPayload;
import com.dnd.dndtravel.auth.config.AppleProperties;
import com.dnd.dndtravel.auth.service.dto.response.AppleTokenResponse;

/**
* private key와 기타 설정값들로 client secret을 생성한다
Expand Down Expand Up @@ -48,15 +48,30 @@ public class AppleOAuthService {
private final AppleClient appleClient;
private final AppleProperties appleProperties;

public AppleIdTokenPayload get(String authorizationCode) {
String idToken = appleClient.getIdToken(
public AppleTokenResponse get(String authorizationCode) {
AppleSocialTokenInfoResponse appleSocialTokenInfoResponse = appleClient.getIdToken(
appleProperties.getClientId(),
generateClientSecret(),
appleProperties.getGrantType(),
authorizationCode
).idToken();
);

return TokenDecoder.decodePayload(idToken, AppleIdTokenPayload.class);
AppleIdTokenPayload appleIdTokenPayload = TokenDecoder.decodePayload(appleSocialTokenInfoResponse.idToken(), AppleIdTokenPayload.class);
return AppleTokenResponse.of(appleIdTokenPayload, appleSocialTokenInfoResponse.refreshToken());
}

public void revoke(String refreshToken) {
try {
appleClient.revoke(
appleProperties.getClientId(),
generateClientSecret(),
refreshToken,
"refresh_token"
);
} catch (Exception e) {
// invalid_grant 응답메시지로, 정확한 예외Response 확인후 Refresh Token 만료되었다는 예외라면 재인증 요청하게끔 하는 코드로 수정하는것을 권장
throw new RequireReAuthenticationException(e);
}
}

private String generateClientSecret() {
Expand Down Expand Up @@ -86,27 +101,4 @@ private PrivateKey getPrivateKey() {
throw new RuntimeException("Error converting private key from String", e);
}
}

public String getAccessToken(String authorizationCode) {
AppleSocialTokenInfoResponse tokenInfo = appleClient.getIdToken(
appleProperties.getClientId(),
generateClientSecret(),
appleProperties.getGrantType(),
authorizationCode
);
return tokenInfo.accessToken();
}

public void revoke(String accessToken) {
try {
appleClient.revoke(
appleProperties.getClientId(),
generateClientSecret(),
accessToken,
"access_token"
);
} catch (Exception e) {
throw new AppleTokenRevokeException(e);
}
}
}
39 changes: 39 additions & 0 deletions src/main/java/com/dnd/dndtravel/auth/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.dnd.dndtravel.auth.service;

import java.util.Optional;

import org.springframework.stereotype.Component;

import com.dnd.dndtravel.auth.service.dto.response.AppleTokenResponse;
import com.dnd.dndtravel.auth.service.dto.response.TokenResponse;
import com.dnd.dndtravel.member.domain.Member;
import com.dnd.dndtravel.member.service.MemberService;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class AuthService {
private final AppleOAuthService appleOAuthService;
private final MemberService memberService;
private final JwtTokenService jwtTokenService;

public Optional<TokenResponse> processAppleLogin(String authorizationCode, String selectedColor) {
AppleTokenResponse appleToken = appleOAuthService.get(authorizationCode);

Member member = memberService.saveMember(
appleToken.email(),
appleToken.sub(),
selectedColor
);

return Optional.ofNullable(
jwtTokenService.generateTokens(member.getId(), appleToken.appleRefreshToken())
);
}

public void processAppleRevoke(String refreshToken, long memberId) {
appleOAuthService.revoke(refreshToken);
memberService.withdrawMember(memberId);
}
}
42 changes: 27 additions & 15 deletions src/main/java/com/dnd/dndtravel/auth/service/JwtTokenService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,49 @@ public class JwtTokenService {
private final RefreshTokenRepository refreshTokenRepository;

@Transactional
public TokenResponse generateTokens(Long memberId) {
public TokenResponse generateTokens(Long memberId, String appleRefreshToken) {
RefreshToken refreshToken = refreshTokenRepository.findByMemberId(memberId);

// 리프레시 토큰이 없는경우
if (refreshToken == null) {
String newRefreshToken = jwtProvider.refreshToken();
refreshTokenRepository.save(RefreshToken.of(memberId, newRefreshToken)); // refreshToken은 DB에 저장
return new TokenResponse(jwtProvider.accessToken(memberId), newRefreshToken);
return createNewTokens(memberId, appleRefreshToken);
}

// 리프레시 토큰이 만료됐으면 재발급 받으라고 멘트줌
// 리프레시 토큰이 만료됐으면 재발급
if (refreshToken.isExpire()) {
return null;
}

// 리프레시 토큰이 DB에 존재하고 유효한경우
refreshTokenRepository.delete(refreshToken);
String newRefreshToken = jwtProvider.refreshToken();
refreshTokenRepository.save(RefreshToken.of(refreshToken.getMemberId(), newRefreshToken));
return new TokenResponse(jwtProvider.accessToken(memberId), newRefreshToken);
return rotateTokens(memberId, appleRefreshToken, refreshToken);
}

@Transactional
public ReissueTokenResponse reIssue(String token) {
//validation
RefreshToken refreshToken = refreshTokenRepository.findByRefreshToken(token).orElseThrow(() -> new RefreshTokenInvalidException(token));
RefreshToken oldRefreshToken = refreshTokenRepository.findByRefreshToken(token).orElseThrow(() -> new RefreshTokenInvalidException(token));
String newRefreshToken = rotateRefreshToken(oldRefreshToken);
String newAccessToken = jwtProvider.accessToken(oldRefreshToken.getMemberId());

return new ReissueTokenResponse(newAccessToken, newRefreshToken);
}

private String rotateRefreshToken(RefreshToken oldRefreshToken) {
refreshTokenRepository.delete(oldRefreshToken);
String newRefreshToken = jwtProvider.refreshToken();
refreshTokenRepository.save(RefreshToken.of(oldRefreshToken.getMemberId(), newRefreshToken));
return newRefreshToken;
}

private TokenResponse rotateTokens(Long memberId, String appleRefreshToken, RefreshToken refreshToken) {
String newRefreshToken = rotateRefreshToken(refreshToken);

//RTR
refreshTokenRepository.delete(refreshToken);
return new TokenResponse(jwtProvider.accessToken(memberId), newRefreshToken, appleRefreshToken);
}

private TokenResponse createNewTokens(Long memberId, String appleRefreshToken) {
String newRefreshToken = jwtProvider.refreshToken();
refreshTokenRepository.save(RefreshToken.of(refreshToken.getMemberId(), newRefreshToken));
return new ReissueTokenResponse(jwtProvider.accessToken(refreshToken.getMemberId()), newRefreshToken);
refreshTokenRepository.save(RefreshToken.of(memberId, newRefreshToken)); // refreshToken은 DB에 저장

return new TokenResponse(jwtProvider.accessToken(memberId), newRefreshToken, appleRefreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.dnd.dndtravel.auth.service.dto.response;

public record AppleTokenResponse(
String sub,
String name,
String email,
String appleRefreshToken
) {
public static AppleTokenResponse of(AppleIdTokenPayload appleIdTokenPayload, String appleRefreshToken) {
return new AppleTokenResponse(
appleIdTokenPayload.sub(),
appleIdTokenPayload.name(),
appleIdTokenPayload.email(),
appleRefreshToken
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

public record TokenResponse(
String accessToken,
String refreshToken
String refreshToken,
String appleRefreshToken
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ public class MemberNotFoundException extends RuntimeException {
public MemberNotFoundException(long memberId) {
super(String.format(MESSAGE, memberId));
}

public MemberNotFoundException(String appleId) {
super(String.format(MESSAGE, appleId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.dnd.dndtravel.map.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

import com.dnd.dndtravel.member.domain.WithdrawMember;

public interface WithdrawMemberRepository extends JpaRepository<WithdrawMember, Long> {
Optional<WithdrawMember> findByAppleId(String sub);
}
Loading

0 comments on commit 35ae8f8

Please sign in to comment.