diff --git a/build.gradle b/build.gradle index ad29960..4e4fa15 100644 --- a/build.gradle +++ b/build.gradle @@ -21,11 +21,31 @@ repositories { mavenCentral() } +ext { + // springCloudVersion과 spring boot 버전 충돌나지 않도록 주의 + set('springCloudVersion', '2021.0.5') +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + + // 애플 로그인을 위한 FeignClient 연동 + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/main/java/trothly/trothcam/config/FeignClientConfig.java b/src/main/java/trothly/trothcam/config/FeignClientConfig.java new file mode 100644 index 0000000..b26f306 --- /dev/null +++ b/src/main/java/trothly/trothcam/config/FeignClientConfig.java @@ -0,0 +1,10 @@ +package trothly.trothcam.config; + +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; +import trothly.trothcam.TrothcamApplication; + +@Configuration +@EnableFeignClients(basePackageClasses = TrothcamApplication.class) +public class FeignClientConfig { +} diff --git a/src/main/java/trothly/trothcam/controller/auth/OAuthController.java b/src/main/java/trothly/trothcam/controller/auth/OAuthController.java new file mode 100644 index 0000000..bce7083 --- /dev/null +++ b/src/main/java/trothly/trothcam/controller/auth/OAuthController.java @@ -0,0 +1,32 @@ +package trothly.trothcam.controller.auth; + +import lombok.RequiredArgsConstructor; +import org.springframework.validation.BindingResult; +import org.springframework.validation.ObjectError; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import trothly.trothcam.exception.custom.base.BaseResponse; +import trothly.trothcam.dto.auth.apple.LoginReqDto; +import trothly.trothcam.dto.auth.apple.LoginResDto; +import trothly.trothcam.service.OAuthService; + +@RestController +@RequiredArgsConstructor +public class OAuthController { + private final OAuthService oauthService; + + // 애플 로그인 + @PostMapping("/appple/login") + public BaseResponse appleLogin(@RequestBody @Validated LoginReqDto loginReqDto, BindingResult bindingResult){ + // BindingResult = 검증 오류가 발생할 경우 오류 내용을 보관하는 객체 + if(bindingResult.hasErrors()) { + ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get(); + return new BaseResponse<>(400, objectError.getDefaultMessage(), null); + } + + LoginResDto result = oauthService.appleLogin(loginReqDto); + return new BaseResponse<>(result); + } +} diff --git a/src/main/java/trothly/trothcam/domain/core/BaseTimeEntity.java b/src/main/java/trothly/trothcam/domain/core/BaseTimeEntity.java new file mode 100644 index 0000000..bca000f --- /dev/null +++ b/src/main/java/trothly/trothcam/domain/core/BaseTimeEntity.java @@ -0,0 +1,24 @@ +package trothly.trothcam.domain.core; + +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseTimeEntity { + @CreatedDate + @Column(name = "created_at", updatable = false, nullable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "last_modified_at", nullable = false) + private LocalDateTime lastModifiedAt; +} diff --git a/src/main/java/trothly/trothcam/domain/member/Member.java b/src/main/java/trothly/trothcam/domain/member/Member.java new file mode 100644 index 0000000..41b89ed --- /dev/null +++ b/src/main/java/trothly/trothcam/domain/member/Member.java @@ -0,0 +1,52 @@ +package trothly.trothcam.domain.member; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import trothly.trothcam.domain.core.BaseTimeEntity; + +import javax.persistence.*; +import javax.validation.constraints.Email; +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@Table(name = "member") +public class Member extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "member_id") + private Long id; + + @Email + @Column(name = "email", length = 255, nullable = false) + private String email; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "imaage", length = 255, nullable = true) + private String image; + + @Column(name = "provider", nullable = false) + @Enumerated(EnumType.STRING) + private Provider provider; + + @Column(name = "refresh_token", nullable = false) + private String refreshToken; + + @Column(name = "refresh_token_expires_at", nullable = false) + private LocalDateTime refreshTokenExpiresAt; + + @Builder + private Member(String email, String name, String image, Provider provider) { + this.email = email; + this.name = name; + this.image = image; + this.provider = provider; + this.refreshToken = ""; + this.refreshTokenExpiresAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/trothly/trothcam/domain/member/Provider.java b/src/main/java/trothly/trothcam/domain/member/Provider.java new file mode 100644 index 0000000..a4b7693 --- /dev/null +++ b/src/main/java/trothly/trothcam/domain/member/Provider.java @@ -0,0 +1,5 @@ +package trothly.trothcam.domain.member; + +public enum Provider { + APPLE, GOOGLE +} diff --git a/src/main/java/trothly/trothcam/dto/auth/apple/ApplePublicKey.java b/src/main/java/trothly/trothcam/dto/auth/apple/ApplePublicKey.java new file mode 100644 index 0000000..8947da1 --- /dev/null +++ b/src/main/java/trothly/trothcam/dto/auth/apple/ApplePublicKey.java @@ -0,0 +1,18 @@ +package trothly.trothcam.dto.auth.apple; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +public class ApplePublicKey { + private String kty; + private String kid; + private String use; + private String alg; + private String n; + private String e; +} diff --git a/src/main/java/trothly/trothcam/dto/auth/apple/ApplePublicKeys.java b/src/main/java/trothly/trothcam/dto/auth/apple/ApplePublicKeys.java new file mode 100644 index 0000000..29b7ad3 --- /dev/null +++ b/src/main/java/trothly/trothcam/dto/auth/apple/ApplePublicKeys.java @@ -0,0 +1,23 @@ +package trothly.trothcam.dto.auth.apple; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +public class ApplePublicKeys { + private List keys; + + public ApplePublicKey getMatchesKey(String alg, String kid) { + return this.keys + .stream() + .filter(k -> k.getAlg().equals(alg) && k.getKid().equals(kid)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Apple id_token 값의 alg, kid 정보가 올바르지 않습니다.")); + } +} \ No newline at end of file diff --git a/src/main/java/trothly/trothcam/dto/auth/apple/LoginReqDto.java b/src/main/java/trothly/trothcam/dto/auth/apple/LoginReqDto.java new file mode 100644 index 0000000..333f098 --- /dev/null +++ b/src/main/java/trothly/trothcam/dto/auth/apple/LoginReqDto.java @@ -0,0 +1,16 @@ +package trothly.trothcam.dto.auth.apple; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LoginReqDto { + @NotBlank(message = "애플 토큰 값이 존재하지 않습니다.") + private String idToken; +} diff --git a/src/main/java/trothly/trothcam/dto/auth/apple/LoginResDto.java b/src/main/java/trothly/trothcam/dto/auth/apple/LoginResDto.java new file mode 100644 index 0000000..e45f51f --- /dev/null +++ b/src/main/java/trothly/trothcam/dto/auth/apple/LoginResDto.java @@ -0,0 +1,19 @@ +package trothly.trothcam.dto.auth.apple; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LoginResDto { + private String accessToken; + private String refreshToken; + + @Builder + public LoginResDto(String accessToken, String refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} diff --git a/src/main/java/trothly/trothcam/exception/base/BaseException.java b/src/main/java/trothly/trothcam/exception/base/BaseException.java new file mode 100644 index 0000000..df09062 --- /dev/null +++ b/src/main/java/trothly/trothcam/exception/base/BaseException.java @@ -0,0 +1,12 @@ +package trothly.trothcam.exception.custom.base; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class BaseException extends Exception { + private BaseResponseStatus status; //BaseResoinseStatus 객체에 매핑 +} diff --git a/src/main/java/trothly/trothcam/exception/base/BaseResponse.java b/src/main/java/trothly/trothcam/exception/base/BaseResponse.java new file mode 100644 index 0000000..0ca14a0 --- /dev/null +++ b/src/main/java/trothly/trothcam/exception/base/BaseResponse.java @@ -0,0 +1,45 @@ +package trothly.trothcam.exception.custom.base; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import static trothly.trothcam.exception.custom.base.BaseResponseStatus.SUCCESS; + + +@Getter +@AllArgsConstructor +@JsonPropertyOrder({"isSuccess", "code", "message", "result"}) +public class BaseResponse {//BaseResponse 객체를 사용할때 성공, 실패 경우 + @JsonProperty("isSuccess") + private Boolean isSuccess; + private final String message; + private final int code; + @JsonInclude(JsonInclude.Include.NON_NULL) + private T result; + + // 요청에 성공한 경우 + public BaseResponse(T result) { + this.isSuccess = SUCCESS.isSuccess(); + this.message = SUCCESS.getMessage(); + this.code = SUCCESS.getCode(); + this.result = result; + } + + // 요청에 실패한 경우 #1 + public BaseResponse(BaseResponseStatus status) { + this.isSuccess = status.isSuccess(); + this.message = status.getMessage(); + this.code = status.getCode(); + } + + // 요청에 실패한 경우 #2 + public BaseResponse(int code, String message, T result) { + this.code = code; + this.message = message; + this.result = result; + } +} + diff --git a/src/main/java/trothly/trothcam/exception/base/BaseResponseStatus.java b/src/main/java/trothly/trothcam/exception/base/BaseResponseStatus.java new file mode 100644 index 0000000..e06bbc6 --- /dev/null +++ b/src/main/java/trothly/trothcam/exception/base/BaseResponseStatus.java @@ -0,0 +1,59 @@ +package trothly.trothcam.exception.custom.base; + +import lombok.Getter; + +/** + * 에러 코드 관리 + */ +@Getter +public enum BaseResponseStatus { + /** + * 1000 : 요청 성공 + */ + SUCCESS(true, 1000, "요청에 성공하였습니다."), + + + /** + * 2000 : Request 오류 + */ + // Common + REQUEST_ERROR(false, 2000, "입력값을 확인해주세요."), + EMPTY_JWT(false, 2001, "JWT를 입력해주세요."), + INVALID_JWT(false, 2002, "유효하지 않은 JWT입니다."), + INVALID_USER_JWT(false,2003,"권한이 없는 유저의 접근입니다."), + + + /** + * 3000 : Response 오류 + */ + // Common + RESPONSE_ERROR(false, 3000, "값을 불러오는데 실패하였습니다."), + + + /** + * 4000 : Database, Server 오류 + */ + DATABASE_ERROR(false, 4000, "데이터베이스 연결에 실패하였습니다."), + SERVER_ERROR(false, 4001, "서버와의 연결에 실패하였습니다."); + + + /** + * 5000 : 필요시 만들어서 쓰세요 + */ + + + /** + * 6000 : 필요시 만들어서 쓰세요 + */ + + + private final boolean isSuccess; + private final int code; + private final String message; + + private BaseResponseStatus(boolean isSuccess, int code, String message) { //BaseResponseStatus 에서 각 해당하는 코드를 생성자로 맵핑 + this.isSuccess = isSuccess; + this.code = code; + this.message = message; + } +} \ No newline at end of file diff --git a/src/main/java/trothly/trothcam/exception/custom/InvalidTokenException.java b/src/main/java/trothly/trothcam/exception/custom/InvalidTokenException.java new file mode 100644 index 0000000..249adb9 --- /dev/null +++ b/src/main/java/trothly/trothcam/exception/custom/InvalidTokenException.java @@ -0,0 +1,7 @@ +package trothly.trothcam.exception.custom; + +public class InvalidTokenException extends RuntimeException { + public InvalidTokenException(String message) { + super(message); + } +} diff --git a/src/main/java/trothly/trothcam/exception/custom/TokenExpiredException.java b/src/main/java/trothly/trothcam/exception/custom/TokenExpiredException.java new file mode 100644 index 0000000..778f7c7 --- /dev/null +++ b/src/main/java/trothly/trothcam/exception/custom/TokenExpiredException.java @@ -0,0 +1,7 @@ +package trothly.trothcam.exception.custom; + +public class TokenExpiredException extends RuntimeException { + public TokenExpiredException(String message) { + super(message); + } +} diff --git a/src/main/java/trothly/trothcam/feign/AppleClient.java b/src/main/java/trothly/trothcam/feign/AppleClient.java new file mode 100644 index 0000000..339e23e --- /dev/null +++ b/src/main/java/trothly/trothcam/feign/AppleClient.java @@ -0,0 +1,16 @@ +package trothly.trothcam.feign; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import trothly.trothcam.config.FeignClientConfig; +import trothly.trothcam.dto.auth.apple.ApplePublicKeys; + +@FeignClient( + name = "apple-public-key-client", + url = "https://appleid.apple.com/auth", + configuration = FeignClientConfig.class +) +public interface AppleClient { + @GetMapping("/keys") + ApplePublicKeys getApplePublicKeys(); +} diff --git a/src/main/java/trothly/trothcam/service/OAuthService.java b/src/main/java/trothly/trothcam/service/OAuthService.java new file mode 100644 index 0000000..77a21c2 --- /dev/null +++ b/src/main/java/trothly/trothcam/service/OAuthService.java @@ -0,0 +1,16 @@ +package trothly.trothcam.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import trothly.trothcam.dto.auth.apple.LoginReqDto; +import trothly.trothcam.dto.auth.apple.LoginResDto; + +@RequiredArgsConstructor +@Service +public class OAuthService { + private static String APPLE_REQUEST_URL = "https://appleid.apple.com/auth/keys"; + public LoginResDto appleLogin(LoginReqDto loginReqDto) { + + } + +}