Skip to content

Commit

Permalink
Feat: #1 애플로그인, 회원가입, 자동 로그인(토큰 refresh) 로직 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
yerim216 authored May 27, 2024
2 parents 09ef87a + da61e1c commit 74b4dc0
Show file tree
Hide file tree
Showing 31 changed files with 699 additions and 120 deletions.
14 changes: 14 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ repositories {
mavenCentral()
}

ext {
set('springCloudVersion', "2023.0.0")
}

dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
Expand All @@ -47,6 +57,10 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

implementation 'org.springframework.cloud:spring-cloud-starter-config'
//애플 인증 서버에 JSON Web Key(JWK)를 가져올 때 사용
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@OpenAPIDefinition(servers={
@Server(url ="/", description = "Default Server URL")
})
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
@EnableFeignClients
@EnableJpaAuditing
public class NzGenerationApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.nzgeneration.domain.auth;

import com.example.nzgeneration.domain.auth.dto.AuthResponseDto.OIDCPublicKeysResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;


@FeignClient(name = "AppleOAuthClient", url = "https://appleid.apple.com")
public interface AppleAuthApiClient {

@GetMapping("/auth/keys")
OIDCPublicKeysResponse getAppleOIDCOpenKeys();


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.example.nzgeneration.domain.auth;

import com.example.nzgeneration.domain.auth.dto.AuthResponseDto.OIDCPublicKeysResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.example.nzgeneration.domain.auth.dto.AuthResponseDto.OIDCDecodePayload;

@Component
@RequiredArgsConstructor
public class AppleOauthHelper {
private final AppleAuthApiClient appleAuthApiClient;
private final OauthOIDCHelper oauthOIDCHelper;

@Value("${social-login.provider.apple.client-id}")
private String aud;

@Value("${social-login.provider.apple.issuer}")
private String iss;

public OIDCDecodePayload getOIDCDecodePayload(String token){
OIDCPublicKeysResponse oidcPublicKeysResponse = appleAuthApiClient.getAppleOIDCOpenKeys();
return oauthOIDCHelper.getPayloadFromIdToken(
token, iss, aud, oidcPublicKeysResponse
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.example.nzgeneration.domain.auth;

import com.example.nzgeneration.domain.auth.dto.AuthRequestDto.CreateUserRequest;
import com.example.nzgeneration.domain.auth.dto.AuthResponseDto.LoginSimpleInfo;
import com.example.nzgeneration.domain.auth.enums.ResponseType;
import com.example.nzgeneration.global.common.response.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
@Tag(name="Auth", description = "인증(로그인, 회원가입) 관련")
public class AuthController {

private final AuthService authService;

@PostMapping("/login")
@Operation(summary = "로그인/회원가입 api", description = "code : Authorization code / 회원가입, 로그인 구분 없이 동일한 API 사용")
public ApiResponse<LoginSimpleInfo> login(@RequestHeader("Authorization") String idToken){
if (idToken != null && idToken.startsWith("Bearer ")) {
idToken = idToken.substring("Bearer ".length());
}
LoginSimpleInfo loginSimpleInfo = authService.login(idToken);
if(loginSimpleInfo.getResponseType()== ResponseType.SIGN_IN){
return ApiResponse.onSuccess(loginSimpleInfo);
}
return ApiResponse.onFailure(4003, "추가 정보 입력이 필요합니다", loginSimpleInfo);
}

@PostMapping("/signup/extra")
@Operation(summary = "회원가입 추가 정보 입력 api")
public ApiResponse<LoginSimpleInfo> signUp(@RequestHeader("Authorization") String token, @RequestBody CreateUserRequest createUserRequest){
if (token != null && token.startsWith("Bearer ")) {
token = token.substring("Bearer ".length());
}
return ApiResponse.onSuccess(authService.signUp(token, createUserRequest));
}

@PostMapping("/refresh-token")
@Operation(summary = "access token, refresh token 재발급")
public ApiResponse<LoginSimpleInfo> refreshToken(@RequestParam String refreshToken){
return ApiResponse.onSuccess(authService.updateUserToken(refreshToken));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.example.nzgeneration.domain.auth;

import com.example.nzgeneration.domain.auth.dto.AuthRequestDto.CreateUserRequest;
import com.example.nzgeneration.domain.auth.dto.AuthResponseDto.LoginSimpleInfo;
import com.example.nzgeneration.domain.auth.dto.AuthResponseDto.OIDCDecodePayload;
import com.example.nzgeneration.domain.auth.enums.ResponseType;
import com.example.nzgeneration.domain.user.User;
import com.example.nzgeneration.domain.user.UserRepository;
import com.example.nzgeneration.global.common.response.ApiResponse;
import com.example.nzgeneration.global.common.response.code.status.ErrorStatus;
import com.example.nzgeneration.global.common.response.exception.GeneralException;
import com.example.nzgeneration.global.security.JwtTokenProvider;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class AuthService {

private final AppleOauthHelper appleOauthHelper;
private final UserRepository userRepository;
private final JwtTokenProvider jwtTokenProvider;
@Transactional
public LoginSimpleInfo login(String idToken){
OIDCDecodePayload oidcDecodePayload = appleOauthHelper.getOIDCDecodePayload(idToken);
String email = oidcDecodePayload.email();
Optional<User> optionalMember = userRepository.findByEmail(email);
String accessToken, refreshToken;
if(optionalMember.isPresent()){ //로그인 로직
accessToken = jwtTokenProvider.createAccessToken(optionalMember.get().getPayload());
refreshToken = jwtTokenProvider.createRefreshToken(optionalMember.get().getId());
return LoginSimpleInfo.toDTO(accessToken, refreshToken, ResponseType.SIGN_IN);
}
accessToken = jwtTokenProvider.generateTempToken(email);
return LoginSimpleInfo.toDTO(accessToken, null, ResponseType.SIGN_UP);

}

@Transactional
public LoginSimpleInfo signUp(String token, CreateUserRequest createUserRequest){
String email = jwtTokenProvider.validateTempTokenAndGetEmail(token);
if(userRepository.findByEmail(email).isPresent())
throw new GeneralException(ErrorStatus._DUPLICATE_USER);
User user = User.toEntity(email, createUserRequest);
userRepository.save(user);
String accessToken = jwtTokenProvider.createAccessToken(user.getPayload());
String refreshToken = jwtTokenProvider.createRefreshToken(user.getId());
user.updateToken(accessToken, refreshToken);
return LoginSimpleInfo.toDTO(accessToken, refreshToken, ResponseType.SIGN_IN);
}

@Transactional
public LoginSimpleInfo updateUserToken(String refreshToken) {
if(userRepository.findByRefreshToken(refreshToken).isEmpty()){
throw new GeneralException(ErrorStatus._EXPIRED_JWT);
}
Long userId = Long.valueOf(jwtTokenProvider.getPayload(refreshToken));
User user = userRepository.findById(userId)
.orElseThrow(() -> new GeneralException(ErrorStatus._INVALID_JWT));
String newRefreshToken = jwtTokenProvider.createRefreshToken(userId);
String newAccesstoken = jwtTokenProvider.refreshAccessToken(refreshToken);
user.updateToken(newAccesstoken, newRefreshToken);
return new LoginSimpleInfo(newAccesstoken, newRefreshToken, ResponseType.TOKEN_REFRESH);

}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.example.nzgeneration.domain.auth;

import com.example.nzgeneration.domain.auth.dto.AuthResponseDto.OIDCDecodePayload;
import com.example.nzgeneration.domain.auth.dto.AuthResponseDto.OIDCPublicKey;
import com.example.nzgeneration.domain.auth.dto.AuthResponseDto.OIDCPublicKeysResponse;
import com.example.nzgeneration.global.security.JwtOIDCProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class OauthOIDCHelper {

private final JwtOIDCProvider jwtOIDCProvider;

//토큰에서 kid 가져온다 -> 가져온 kid는 공개키 결정에 사용
private String getKidFromUnsignedIdToken(String token, String iss, String aud){
return jwtOIDCProvider.getKidFromUnsignedTokenHeader(token, iss, aud);
}

public OIDCDecodePayload getPayloadFromIdToken(String token, String iss, String aud, OIDCPublicKeysResponse oidcPublicKeysResponse){
String kid = getKidFromUnsignedIdToken(token, iss, aud);

//같은 Kid인 공개키 불러와서 토큰 검증에 사용
OIDCPublicKey oidcPublicKey = oidcPublicKeysResponse.keys().stream()
.filter(o-> o.kid().equals(kid))
.findFirst()
.orElseThrow();

//검증 된 토큰에서 바디를 꺼내온다
return jwtOIDCProvider.getOIDCTokenBody(token, oidcPublicKey.n(), oidcPublicKey.e());
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.nzgeneration.domain.auth.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

public class AuthRequestDto {

@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class CreateUserRequest {
private String nickName;
private String walletAddress;
private String profileImgUrl;
private Boolean isAllowLocationInfo;
private Boolean isAllowAdInfo;

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.example.nzgeneration.domain.auth.dto;

import com.example.nzgeneration.domain.auth.enums.ResponseType;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

public class AuthResponseDto {
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class LoginSimpleInfo{
private String accessToken;
private String refreshToken;
private ResponseType responseType;

public static LoginSimpleInfo toDTO(String accessToken, String refreshToken, ResponseType responseType) {
return LoginSimpleInfo.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.responseType(responseType)
.build();

}

}
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record OIDCPublicKeysResponse(List<OIDCPublicKey> keys) {
}
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record OIDCPublicKey(
String kty,
String kid,
String use,
String alg,
String n,
String e
) {
}

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record OIDCDecodePayload(
String issuer,
String audience,
String subject,
String email

){
}




}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.nzgeneration.domain.auth.enums;

public enum ResponseType {
SIGN_IN, SIGN_UP, TOKEN_REFRESH
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.example.nzgeneration.global.utils.BaseTimeEntity;
import com.example.nzgeneration.domain.nft.Nft;
import com.example.nzgeneration.domain.member.Member;
import com.example.nzgeneration.domain.user.User;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
Expand All @@ -29,5 +29,5 @@ public class Board extends BaseTimeEntity {
private Nft nft;

@ManyToOne(fetch = FetchType.LAZY)
private Member uploadMember;
private User uploadUser;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.example.nzgeneration.global.utils.BaseTimeEntity;
import com.example.nzgeneration.domain.trashcan.Trashcan;
import com.example.nzgeneration.domain.member.Member;
import com.example.nzgeneration.domain.user.User;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
Expand All @@ -23,7 +23,7 @@ public class ErrorReport extends BaseTimeEntity {
private Long id;

@ManyToOne
private Member errorReportMember;
private User errorReportUser;

private String content;

Expand Down
Loading

0 comments on commit 74b4dc0

Please sign in to comment.