-
Notifications
You must be signed in to change notification settings - Fork 113
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
65 changed files
with
4,084 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,33 @@ | ||
# spring-gift-point | ||
# spring-gift-order | ||
# 1단계 - 카카오 로그인 | ||
|
||
## 요구사항 | ||
### 기능 요구사항 | ||
1. 카카오 로그인을 통해 인가 코드를 받는다. | ||
2. 인가 코드를 사용해 토큰을 받은 후 향후 카카오 API 사용을 준비한다. | ||
3. 카카오계정 로그인을 통해 인증 코드를 받는다. | ||
4. 토큰 받기를 읽고 액세스 토큰을 추출한다. | ||
5. 앱 키, 인가 코드가 절대 유출되지 않도록 한다. | ||
- 특히 시크릿 키는 GitHub나 클라이언트 코드 등 외부에서 볼 수 있는 곳에 추가하지 않는다. | ||
6. 오류 처리 | ||
레퍼런스와 문제 해결을 참고하여 발생할 수 있는 다양한 오류를 처리한다. | ||
|
||
# 2단계 - 주문하기 | ||
|
||
## 요구사항 | ||
### 기능 요구사항 | ||
1. 카카오톡 메시지 API를 사용하여 주문하기 기능을 구현한다. | ||
2. 주문할 때 수령인에게 보낼 메시지를 작성할 수 있다. | ||
3. 상품 옵션과 해당 수량을 선택하여 주문하면 해당 상품 옵션의 수량이 차감된다. | ||
4. 해당 상품이 위시 리스트에 있는 경우 위시 리스트에서 삭제한다. | ||
5. 나에게 보내기를 읽고 주문 내역을 카카오톡 메시지로 전송한다. | ||
6. 메시지는 메시지 템플릿의 기본 템플릿이나 사용자 정의 템플릿을 사용하여 자유롭게 작성한다. | ||
|
||
# 3단계 - API 문서 만들기 | ||
|
||
## 요구사항 | ||
### 기능 요구사항 | ||
API 사양에 관해 클라이언트와 어떻게 소통할 수 있을까? 어떻게 하면 편하게 소통할 수 있을지 고민해 보고 그 방법을 구현한다. | ||
1. API 문서를 만들어 클라이언트에게 제공한다. | ||
2. API 문서는 Swagger 또는 restDoc을 사용하여 만든다. | ||
3. API 문서에는 API 명세, 요청, 응답, 예외 처리 등이 포함되어야 한다. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
src/main/java/gift/Annotation/LoginMemberArgumentResolver.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package gift.Annotation; | ||
|
||
import gift.Model.MemberDto; | ||
import gift.Service.MemberService; | ||
import gift.Utils.JwtUtil; | ||
import gift.Validation.KakaoTokenValidator; | ||
import io.jsonwebtoken.Claims; | ||
import jakarta.servlet.http.Cookie; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.core.MethodParameter; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.bind.support.WebDataBinderFactory; | ||
import org.springframework.web.context.request.NativeWebRequest; | ||
import org.springframework.web.method.support.HandlerMethodArgumentResolver; | ||
import org.springframework.web.method.support.ModelAndViewContainer; | ||
|
||
import java.util.Optional; | ||
|
||
@Component | ||
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { | ||
private final MemberService memberService; | ||
private final JwtUtil jwtUtil; | ||
private boolean isKakaoToken = false; | ||
|
||
@Autowired | ||
public LoginMemberArgumentResolver(MemberService memberService, JwtUtil jwtUtil) { | ||
this.memberService = memberService; | ||
this.jwtUtil = jwtUtil; | ||
} | ||
|
||
@Override | ||
public boolean supportsParameter(MethodParameter methodParameter) { | ||
return methodParameter.getParameterType().equals(MemberDto.class); | ||
} | ||
|
||
@Override | ||
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { | ||
HttpServletRequest request = (HttpServletRequest) nativeWebRequest.getNativeRequest(); | ||
|
||
// 쿠키에서 JWT 토큰 추출 | ||
String token = null; | ||
if (request.getCookies() != null) { | ||
for (Cookie cookie : request.getCookies()) { | ||
if ("token".equals(cookie.getName())) { | ||
token = cookie.getValue(); | ||
break; | ||
} | ||
|
||
if ("accessToken".equals(cookie.getName())) { | ||
token = cookie.getValue(); | ||
isKakaoToken = true; | ||
break; | ||
} | ||
} | ||
} | ||
|
||
if (token == null) { | ||
throw new IllegalArgumentException("JWT token not found in cookies"); | ||
} | ||
|
||
if (isKakaoToken) { | ||
//카카오 토큰 검증 | ||
long kakaoId = KakaoTokenValidator.validateToken(token); | ||
Optional<MemberDto> memberDto = memberService.findByKakaoId(kakaoId); | ||
|
||
return memberDto.orElseThrow(() -> new IllegalArgumentException("User not found")); | ||
} | ||
|
||
Claims claims = jwtUtil.decodeToken(token); //decode | ||
String memberEmail = claims.getSubject(); // subject를 email로 설정했기 때문에 userEmail로 사용 | ||
Optional<MemberDto> memberDto = memberService.findByEmail(memberEmail); //null이라면 인증된 것이 아닐 것이고 null이 아니라면 인증된 것 | ||
if (memberDto.isEmpty()) { | ||
throw new IllegalArgumentException("User not found"); | ||
} | ||
return memberDto; | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package gift.Annotation; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
@Target(ElementType.PARAMETER) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
public @interface LoginMemberResolver { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package gift.Config; | ||
|
||
import gift.Annotation.LoginMemberArgumentResolver; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.web.method.support.HandlerMethodArgumentResolver; | ||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||
|
||
import java.util.List; | ||
|
||
@Configuration | ||
public class WebConfig implements WebMvcConfigurer { | ||
|
||
@Autowired | ||
private LoginMemberArgumentResolver loginMemberResolverHandlerMethodArgumentResolver; | ||
|
||
@Override | ||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { | ||
resolvers.add(loginMemberResolverHandlerMethodArgumentResolver); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package gift.Controller; | ||
|
||
import gift.Model.CategoryDto; | ||
import gift.Service.CategoryService; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.stereotype.Controller; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
|
||
import java.util.List; | ||
|
||
@Controller | ||
@Tag(name = "Category", description = "카테고리 관련 api") | ||
public class CategoryController { | ||
|
||
private final CategoryService categoryService; | ||
|
||
public CategoryController(CategoryService categoryService) { | ||
this.categoryService = categoryService; | ||
} | ||
|
||
@Operation(summary = "모든 카테고리 조회", description = "모든 카테고리를 조회합니다.") | ||
@GetMapping("/api/categories") | ||
public ResponseEntity<List<CategoryDto>> getAllCategories() { | ||
List<CategoryDto> categoryDtoList = categoryService.getAllCategories(); | ||
return ResponseEntity.ok(categoryDtoList); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package gift.Controller; | ||
|
||
import gift.Exception.ForbiddenException; | ||
import gift.Exception.UnauthorizedException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import org.springframework.ui.Model; | ||
import org.springframework.validation.FieldError; | ||
import org.springframework.web.bind.MethodArgumentNotValidException; | ||
import org.springframework.web.bind.annotation.ControllerAdvice; | ||
import org.springframework.web.bind.annotation.ExceptionHandler; | ||
|
||
@ControllerAdvice | ||
public class GlobalExceptionHandler { | ||
|
||
@ExceptionHandler(MethodArgumentNotValidException.class) | ||
public String handleMethodArgumentNotValid(MethodArgumentNotValidException ex, Model model, HttpServletRequest request) { | ||
FieldError fieldError = ex.getBindingResult().getFieldError(); | ||
String errorMessage = fieldError.getDefaultMessage(); | ||
|
||
model.addAttribute("error", errorMessage); | ||
model.addAttribute("product", ex.getBindingResult().getTarget()); | ||
|
||
// 에러가 발생한 URL에 따라 다시 해당 페이지로 돌아가기 | ||
String requestUrl = request.getRequestURI(); | ||
if (requestUrl.equals("/api/products/create") || requestUrl.startsWith("/api/products/update")) { | ||
return "product_form"; // product_form 뷰로 포워딩 | ||
} | ||
|
||
return "redirect:/api/products"; | ||
} | ||
|
||
@ExceptionHandler(UnauthorizedException.class) | ||
public ResponseEntity<String> handleUnauthorizedException(UnauthorizedException ex) { | ||
return new ResponseEntity<>(ex.getMessage(), HttpStatus.UNAUTHORIZED); | ||
} | ||
|
||
@ExceptionHandler(ForbiddenException.class) | ||
public ResponseEntity<String> handleForbiddenException(ForbiddenException ex) { | ||
return new ResponseEntity<>(ex.getMessage(), HttpStatus.FORBIDDEN); | ||
} | ||
|
||
} |
115 changes: 115 additions & 0 deletions
115
src/main/java/gift/Controller/KakaoOAuthController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
package gift.Controller; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import gift.Model.KakaoAccessTokenDto; | ||
import gift.Model.KakaoMemberDto; | ||
import gift.Model.MemberDto; | ||
import gift.Service.MemberService; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import jakarta.servlet.http.Cookie; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.http.*; | ||
import org.springframework.util.LinkedMultiValueMap; | ||
import org.springframework.util.MultiValueMap; | ||
import org.springframework.web.bind.annotation.*; | ||
import org.springframework.web.client.RestTemplate; | ||
|
||
import java.io.IOException; | ||
|
||
@RestController | ||
@Tag(name = "Kakao Login", description = "카카오 로그인 관련 api") | ||
public class KakaoOAuthController { | ||
|
||
@Value("${kakao.clientId}") | ||
private String clientId; | ||
|
||
@Autowired | ||
private MemberService memberService; | ||
|
||
@GetMapping("/oauth/authorize") | ||
@Operation(summary = "카카오 로그인 화면", description = "카카오 로그인 화면을 보여줍니다.") | ||
public void authorize(HttpServletResponse response) throws IOException { | ||
var url = "https://kauth.kakao.com/oauth/authorize"; | ||
var redirectUri = "http://localhost:8080/auth/kakao/callback"; | ||
var responseType = "code"; | ||
|
||
String redirectUrl = url + "?response_type=" + responseType + "&client_id=" + clientId + "&redirect_uri=" + redirectUri; | ||
response.sendRedirect(redirectUrl); | ||
} | ||
|
||
|
||
@GetMapping("/auth/kakao/callback") | ||
@Operation(summary = "카카오 로그인 수행", description = "인가 코드를 통해 로그인을 수행합니다.") | ||
public ResponseEntity<?> callBack(@RequestParam("code") String code, HttpServletResponse response) throws Exception { | ||
//인가 코드로 토큰 받아오기 | ||
String accessToken = getAccessToken(code); | ||
|
||
//토큰을 쿠키에 저장하기 | ||
Cookie cookie = new Cookie("accessToken", accessToken); | ||
cookie.setHttpOnly(true); | ||
cookie.setPath("/"); | ||
response.addCookie(cookie); | ||
|
||
//토큰을 통해 사용자 정보 받아오기 | ||
RestTemplate restTemplate = new RestTemplate(); | ||
HttpHeaders headers = new HttpHeaders(); | ||
headers.add("Authorization", "Bearer " + accessToken); | ||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); | ||
|
||
HttpEntity<String> entity = new HttpEntity<>(null, headers); | ||
ResponseEntity<String> responseEntity = restTemplate.exchange( | ||
"https://kapi.kakao.com/v2/user/me", | ||
HttpMethod.GET, | ||
entity, | ||
String.class | ||
); | ||
|
||
ObjectMapper objectMapper = new ObjectMapper(); | ||
//id는 가능한데 email을 가져오려고 하면 null이 되는 이유?????????? | ||
KakaoMemberDto kakaoMemberDto = objectMapper.readValue(responseEntity.getBody(), KakaoMemberDto.class); | ||
long id = kakaoMemberDto.getId(); | ||
|
||
//이미 가입된 회원인지 확인, 가입되지 않은 회원이라면 회원가입 진행 | ||
if (memberService.findByKakaoId(id).isEmpty()) { | ||
MemberDto memberDto = new MemberDto(); | ||
memberDto.setKakaoId(id); | ||
memberService.register(memberDto); | ||
} | ||
|
||
//로그인 처리 | ||
headers = new HttpHeaders(); | ||
headers.add("Location", "/products"); | ||
|
||
return new ResponseEntity<>("Successfully logged in", headers, HttpStatus.SEE_OTHER); | ||
|
||
} | ||
|
||
@Operation(summary = "토큰 발급", description = "토큰을 발급받습니다.") | ||
public String getAccessToken(String code) throws Exception { | ||
RestTemplate restTemplate = new RestTemplate(); | ||
HttpHeaders headers = new HttpHeaders(); | ||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); | ||
|
||
MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); | ||
body.add("grant_type", "authorization_code"); | ||
body.add("client_id", clientId); | ||
body.add("redirect_uri", "http://localhost:8080/auth/kakao/callback"); | ||
body.add("code", code); | ||
|
||
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers); //순서가 body가 먼저 | ||
|
||
ResponseEntity<String> response = restTemplate.exchange( | ||
"https://kauth.kakao.com/oauth/token", | ||
HttpMethod.POST, | ||
requestEntity, | ||
String.class | ||
); //Http 요청 보내고 받기 | ||
|
||
ObjectMapper objectMapper = new ObjectMapper(); | ||
KakaoAccessTokenDto kakaoAccessTokenDto = objectMapper.readValue(response.getBody(), KakaoAccessTokenDto.class); | ||
return kakaoAccessTokenDto.getAccess_token(); | ||
} | ||
} |
Oops, something went wrong.