Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ out/
/.nb-gradle/

### VS Code ###
.vscode/
.vscode/
18 changes: 18 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

//security
implementation 'org.springframework.boot:spring-boot-starter-security'


//JWT
implementation 'io.jsonwebtoken:jjwt:0.11.5'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

//유효성 검사 라이브러리
implementation 'commons-validator:commons-validator:1.7'

// cache memory
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'

}

tasks.named('test') {
Expand Down
28 changes: 23 additions & 5 deletions src/main/java/com/example/PING/WebConfig.java
Original file line number Diff line number Diff line change
@@ -1,24 +1,42 @@
package com.example.PING;

import com.example.PING.global.annotation.AuthenticationArgumentResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
@EnableScheduling
@RequiredArgsConstructor
@EnableTransactionManagement
public class WebConfig implements WebMvcConfigurer {

// private final String ipAddress = "localhost"; // 로컬 테스트용 IP
// private final String ipAddress = "localhost:8080"; // 로컬 테스트용 IP
// private final String ipAddress = "43.203.51.237"; // CI/CD 배포를 위한 IP 주소
// private final String frontEndPort = "5173"; // React 앱이 실행되는 포트
private final AuthenticationArgumentResolver authenticationArgumentResolver;

@Value("${cors-allowed-origins}")
private List<String> corsAllowedOrigins;

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowCredentials(true)
.allowedHeaders("*")
.allowedOrigins(corsAllowedOrigins.toArray(new String[0]))
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
// .allowedOrigins("http://" + this.ipAddress + ":" + this.frontEndPort);
.allowedOrigins("http://ping-deploy-bucket.s3-website.ap-northeast-2.amazonaws.com"); // S3 배포로 제공받은 도메인 주소
.allowedHeaders("*")
.allowCredentials(true);
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(authenticationArgumentResolver);
}
}
64 changes: 64 additions & 0 deletions src/main/java/com/example/PING/auth/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.example.PING.auth.controller;

import com.example.PING.auth.dto.request.SocialSignUpRequest;
import com.example.PING.auth.dto.response.LoginResponse;
import com.example.PING.auth.dto.response.SocialLoginResponse;
import com.example.PING.auth.dto.response.SocialSignUpResponse;
import com.example.PING.auth.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthService authService;

@Operation(
summary = "소셜 로그인",
description = "소셜 로그인 후 Authorization 토큰과 회원 정보를 발급합니다."
)
@PostMapping("/oauth/social-login")
public ResponseEntity<SocialLoginResponse> socialLogin(
@RequestHeader("social_access_token") String accessToken,
@RequestParam("provider")
@Parameter(example = "kakao", description = "OAuth 제공자")
String provider
) {
LoginResponse response = authService.socialLogin(accessToken, provider);
// Todo 정상토큰이면 로그인 완료
HttpHeaders headers = new HttpHeaders();
headers.set("AccessToken", response.accessToken());
headers.set("RefreshToken", response.refreshToken());
headers.set("TemporaryToken", response.temporaryToken());

return new ResponseEntity<>(SocialLoginResponse.of(response), headers,
HttpStatus.OK);
}

@Operation(
summary = "소셜 회원가입",
description = "Temporary 토큰을 통해 확인 후, 정식 회원으로 등록합니다."
)
@PostMapping("/oauth/social-signUp") // 닉네임 받아서 유저 등록 및 회원 가입 완료
public ResponseEntity<SocialSignUpResponse> socialSignUp(
@RequestHeader("temporary_token") String temporaryToken,
@RequestBody SocialSignUpRequest request // 바디에 닉네임 들어있도록
){
// 받은 닉네임이랑 캐시메모리에 있는 정보 넣어서 정식 user 저장.
// 정상토큰 발급
LoginResponse response = authService.registerSocialSignUpUser(temporaryToken, request.nickname());

HttpHeaders headers = new HttpHeaders();
headers.set("AccessToken", response.accessToken());
headers.set("RefreshToken", response.refreshToken());

return new ResponseEntity<>(SocialSignUpResponse.of(response), headers, HttpStatus.OK);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.PING.auth.dto.request;

public record SocialSignUpRequest(
String nickname
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.PING.auth.dto.response;

import com.example.PING.user.entity.User;

public record LoginResponse(
Long userId,
String nickname,
String email,
String accessToken,
String refreshToken,
String temporaryToken
) {
public static LoginResponse of(User user, TokenSetResponse tokenSetResponse) {
return new LoginResponse(user.getUserId(), user.getNickname(), user.getOauthInfo().getOauthEmail(), tokenSetResponse.accessToken(), tokenSetResponse.refreshToken(), tokenSetResponse.temporaryToken());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.PING.auth.dto.response;

public record SocialLoginResponse(Long userId,
String nickname,
String email) {
public static SocialLoginResponse of(LoginResponse loginResponse) {
return new SocialLoginResponse(loginResponse.userId(), loginResponse.nickname(), loginResponse.email());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.PING.auth.dto.response;

public record SocialSignUpResponse(Long userId,
String nickname) {
public static SocialSignUpResponse of(LoginResponse loginResponse) {
return new SocialSignUpResponse(loginResponse.userId(), loginResponse.nickname());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.PING.auth.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;

public record TemporaryTokenResponse(
@Schema(description = "임시 토큰", defaultValue = "temporaryToken") String temporaryToken) {

public static TemporaryTokenResponse of(String temporaryToken) {
return new TemporaryTokenResponse(temporaryToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.PING.auth.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;

public record TokenSetResponse(
@Schema(description = "액세스 토큰", defaultValue = "accessToken") String accessToken,
@Schema(description = "리프레시 토큰", defaultValue = "refreshToken") String refreshToken,
@Schema(description = "임시 토큰", defaultValue = "temporaryToken") String temporaryToken) {

public static TokenSetResponse of(String accessToken, String refreshToken, String temporaryToken) {
return new TokenSetResponse(accessToken, refreshToken, temporaryToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.PING.auth.dto.response.auth;

import com.fasterxml.jackson.annotation.JsonProperty;

public record KakaoAuthResponse(
Long id,
@JsonProperty("kakao_account") KakaoAccountResponse kakaoAccount,
@JsonProperty("properties") PropertiesResponse properties) {
public record KakaoAccountResponse(String email) {}

public record PropertiesResponse(
String nickname, String profile_image, String thumbnail_image) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.PING.auth.dto.response.auth;

import com.example.PING.auth.service.OAuthProvider;
import com.example.PING.user.entity.OauthInfo;
import lombok.Builder;

@Builder
public record OAuthUserInfoResponse( // 받은 json 타입을 자바로 변환하기 위한 dto
// 카카오 말고 다른 거에서도 쓸 수 있게 하기 위해 공통 타입으로 변환하려고 이거 dto로 변환하는 거임
String oauthId,
String email,
String name,
OAuthProvider provider

) {
public OauthInfo toEntity() {
return OauthInfo.builder()
.oauthId(this.oauthId())
.oauthEmail(this.email())
.oauthName(this.name())
.oauthProvider(this.provider().name())
.build();
}
}
37 changes: 37 additions & 0 deletions src/main/java/com/example/PING/auth/entity/RefreshToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.example.PING.auth.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;

import java.time.LocalDateTime;

@Entity
@NoArgsConstructor
@Getter
public class RefreshToken{
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdDate;

@LastModifiedDate
@Column(name = "update_at")
private LocalDateTime updatedDate;

@Id
@Column(nullable = false, name = "refresh_token_id")
private Long memberId;
@Column(nullable = false)
private String token;

@Builder
public RefreshToken(Long memberId, String token) {
this.memberId = memberId;
this.token = token;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.PING.auth.repository;

import com.example.PING.auth.entity.RefreshToken;
import org.springframework.data.jpa.repository.JpaRepository;

public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {

boolean existsByMemberId(Long memberId);

void deleteByMemberId(Long memberId);
}
62 changes: 62 additions & 0 deletions src/main/java/com/example/PING/auth/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.example.PING.auth.service;

import com.example.PING.auth.dto.response.LoginResponse;
import com.example.PING.auth.dto.response.TokenSetResponse;
import com.example.PING.auth.dto.response.auth.OAuthUserInfoResponse;
import com.example.PING.auth.service.kakao.KakaoClient;
import com.example.PING.global.security.utils.JwtTokenGenerator;
import com.example.PING.user.entity.OauthInfo;
import com.example.PING.user.entity.User;
import com.example.PING.user.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class AuthService {

private final KakaoClient kakaoClient;
private final UserService userService;
private final JwtTokenGenerator jwtTokenGenerator;
private final TemporalUserCacheService temporalUserCacheService;

@Transactional
public LoginResponse socialLogin(String socialAccessToken, String provider) {
OAuthUserInfoResponse oauthUserInfoResponse = getOauthUserInfo(socialAccessToken, OAuthProvider.from(provider));
OauthInfo oauthInfo = oauthUserInfoResponse.toEntity();

User user = userService.getUserByOAuthInfo(oauthInfo);
// 만약 소셜로그인으로 새로이 회원가입을 한 사람이라면, 여기서 nickname 필드가 null로 되어 있음.

if(user.getNickname() == null) { // 임시 토큰을 발급해야 하는 경우
TokenSetResponse tokenSetResponse = jwtTokenGenerator.generateTemporaryToken(user);
temporalUserCacheService.set(tokenSetResponse.temporaryToken(), user);
return LoginResponse.of(user, tokenSetResponse);
}

TokenSetResponse tokenSetResponse = jwtTokenGenerator.generateTokenPair(user);
return LoginResponse.of(user, tokenSetResponse);
}

public LoginResponse registerSocialSignUpUser(String tempToken, String nickname) {
User tempUser = temporalUserCacheService.get(tempToken, User.class);
if (tempUser == null) {
throw new RuntimeException("인증 정보가 만료되었습니다.");
}

tempUser.setNickname(nickname); // 닉네임 설정
User savedNewUser = userService.saveTempUser(tempUser); // DB에 새로운 회원으로 저장

temporalUserCacheService.delete(tempToken); // 캐시에서 삭제
// Todo 임시토큰 무효화해야 하나?
return LoginResponse.of(savedNewUser, jwtTokenGenerator.generateTokenPair(savedNewUser));
}

private OAuthUserInfoResponse getOauthUserInfo(String socialAccessToken, OAuthProvider provider) {
return switch (provider) {
case APPLE -> null; // 나중에 다른 것도 하면 이런 식으로~^^
case KAKAO -> kakaoClient.getOauthUserInfo(socialAccessToken);
};
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/example/PING/auth/service/OAuthProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.PING.auth.service;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum OAuthProvider {
KAKAO("KAKAO"),
APPLE("APPLE"),
;
private final String value;

public static OAuthProvider from(String provider) {
return switch (provider.toUpperCase()) {
case "APPLE" -> APPLE;
case "KAKAO" -> KAKAO;
default -> throw new IllegalArgumentException("Unknown provider: " + provider); // Todo 에러코드 이넘화 해놓기
};
}
}
Loading
Loading