Skip to content

Commit

Permalink
[2단계 - [장바구니 기능] 허브(방대의) 미션 제출합니다. (#300)
Browse files Browse the repository at this point in the history
* docs: 2단계 요구사항 정리

* test: 잘못된 테스트명 수정

* feat: data.sql 수정 및 schema.sql 파일 추가

* refactor: auditing sql의 default를 사용하도록 변경

* refactor: 사용자 엔티티 추가

* feat: 사용자 설정 페이지 연동 기능 추가

* feat: 장바구니 페이지 연동 기능 추가

* feat: Basic Auth로 오는 값을 파싱하는 클래스 추가

* feat: 사용자 정보에 대한 ArgumentResolver 추가

* test: 잘못 작성된 테스트 수정

* feat: 장바구니 등록 및 조회 기능 추가

* feat: 장바구니 삭제 기능 추가

* refactor: Cart -> CartProduct로 수정

* refactor: create URI 수정

* feat: 인증을 인터셉터가 담당하도록 설정

* docs: 요구사항 업데이트

* feat: argumentResolver에서 threadLocal clear하는 기능 추가

* feat: Credential에서 비밀번호가 다른지 확인하는 기능 추가

* refactor: 기존에 인증과정에서 Member를 반환하던 Dao를 Credential을 반환하도록 수정

- AuthMemberDao -> CredentialDao
- 추가로 Credential을 검증하는 과정에서 에러메시지를 조금 더 자세하게 출력하도록 변경

* feat: ExceptionHandler에 로깅 적용

* refactor: Auditing을 위한 필드 전부 제거

* refactor: BasicAuthorizationParser의 isNotValid를 제거하고 조금 더 응집력 있는 클래스로 변경

- 올바른 Basic 유형의 헤더(파싱 불가능한 경우) InvalidBasicCredentialException 예외 던지도록 변경

* refactor: Credential의 id를 memberId로 변경

* feat: getMemberId 메서드에 Nullable 애너테이션 추가

* refactor: 패키지 구조 변경

* feat: 도미노 페페로니 피자 추가

* remove: todo 주석 제거

* refactor: InvalidBasicCredentialException이 AuthenticationException을 상속 받도록 수정

* test: BaiscAuthorizationParser 테스트 출력 수정
  • Loading branch information
greeng00se authored May 6, 2023
1 parent f282f04 commit cbc8152
Show file tree
Hide file tree
Showing 48 changed files with 1,498 additions and 100 deletions.
52 changes: 37 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
# 📄 웹 장바구니 - 요구사항

## 🎁 상품
### 🎁 상품

- [x] ID, 이름, 이미지, 가격을 가진다.
- [x] 상품명은 100자 이하여야 한다.
- [x] 상품 가격은 0원 이상이어야 한다.
- [x] 상품 예외 사항
- [x] 존재하지 않는 상품에 대해서 수정/삭제 요청
- [x] 100자 이상의 상품명을 입력
- [x] 음수 가격을 입력

## ✔️ 상품 관리 CRUD API 작성
### 장바구니

- [x] ID, 사용자 ID, 상품 ID를 가진다.

### 사용자

- [x] ID, 이메일, 비밀번호를 가진다.

### ✔️ 상품 관리 CRUD API 작성

- [x] 생성 기능
- [x] 상품 조회 기능
Expand All @@ -15,21 +27,31 @@
- [x] 수정 기능
- [x] 삭제 기능

### 📚 API
### ✔️ 장바구니 CRUD API 작성

| 기능 | Method | URL |
|----|--------|----------------|
| 생성 | POST | /products |
| 수정 | PATCH | /products/{id} |
| 삭제 | DELETE | /products/{id} |
- [x] 장바구니에 상품 추가
- [x] 장바구니에 담긴 상품 제거
- [x] 장바구니 목록 조회

## ✔️ 페이지 연동
### 📚 상품 API

- [x] 상품 목록 페이지 연동
- [x] 관리자 도구 페이지 연동
| 기능 | Method | URL |
|-----|--------|----------------|
| 생성 | POST | /products |
| 수정 | PATCH | /products/{id} |
| 삭제 | DELETE | /products/{id} |

### 📚 장바구니 API

| 기능 | Method | URL |
|-----|--------|---------------------|
| 생성 | POST | /cart-products |
| 삭제 | DELETE | /cart-products/{id} |
| 조회 | GET | /cart-products |

## 🛡️ 예외 사항
### ✔️ 페이지 연동

- [x] 존재하지 않는 상품에 대해서 수정/삭제 요청
- [x] 100자 이상의 상품명을 입력
- [x] 음수 가격을 입력
- [x] 상품 목록 페이지 연동
- [x] 관리자 도구 페이지 연동
- [x] 사용자 설정 페이지 연동
- [x] 장바구니 페이지 연동
18 changes: 18 additions & 0 deletions http-request.http
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
### 상품 추가

POST http://localhost:8080/products/
Content-Type: application/json

Expand All @@ -9,6 +10,7 @@ Content-Type: application/json
}

### 상품 수정

PUT http://localhost:8080/products/1
Content-Type: application/json

Expand All @@ -21,3 +23,19 @@ Content-Type: application/json
### 상품 삭제

DELETE http://localhost:8080/products/1

### 장바구니 추가

POST http://localhost:8080/cart-products/
Authorization: Basic cGl6emExQHBpenphLmNvbTpwaXp6YQ==
Content-Type: application/json

{
"productId": 1
}

### 장바구니 조회

GET http://localhost:8080/cart-products/
Authorization: Basic cGl6emExQHBpenphLmNvbTpwaXp6YQ==
Content-Type: application/json
11 changes: 11 additions & 0 deletions src/main/java/cart/auth/Auth.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package cart.auth;

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 Auth {
}
35 changes: 35 additions & 0 deletions src/main/java/cart/auth/AuthArgumentResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package cart.auth;

import org.springframework.core.MethodParameter;
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;

public class AuthArgumentResolver implements HandlerMethodArgumentResolver {

private final CredentialThreadLocal credentialThreadLocal;

public AuthArgumentResolver(final CredentialThreadLocal credentialThreadLocal) {
this.credentialThreadLocal = credentialThreadLocal;
}

@Override
public boolean supportsParameter(final MethodParameter parameter) {
final boolean hasParameterAnnotation = parameter.hasParameterAnnotation(Auth.class);
final boolean hasCredentialType = Credential.class.isAssignableFrom(parameter.getParameterType());
return hasParameterAnnotation && hasCredentialType;
}

@Override
public Object resolveArgument(
final MethodParameter parameter,
final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest,
final WebDataBinderFactory binderFactory
) {
final Credential credential = credentialThreadLocal.get();
credentialThreadLocal.clear();
return credential;
}
}
46 changes: 46 additions & 0 deletions src/main/java/cart/auth/AuthInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package cart.auth;

import cart.dao.CredentialDao;
import cart.exception.AuthenticationException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;

public class AuthInterceptor implements HandlerInterceptor {

private static final String AUTHORIZATION_HEADER_NAME = "Authorization";

private final CredentialDao credentialDao;
private final BasicAuthorizationParser basicAuthorizationParser;
private final CredentialThreadLocal credentialThreadLocal;

public AuthInterceptor(
final CredentialDao credentialDao,
final BasicAuthorizationParser basicAuthorizationParser,
final CredentialThreadLocal credentialThreadLocal
) {
this.credentialDao = credentialDao;
this.basicAuthorizationParser = basicAuthorizationParser;
this.credentialThreadLocal = credentialThreadLocal;
}

@Override
public boolean preHandle(
final HttpServletRequest request,
final HttpServletResponse response,
final Object handler
) {
final String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER_NAME);

final Credential credential = basicAuthorizationParser.parse(authorizationHeader);
final Credential savedCredential = credentialDao.findByEmail(credential.getEmail())
.orElseThrow(() -> new AuthenticationException("올바르지 않은 이메일입니다. 입력값: " + credential.getEmail()));

if (credential.isNotSamePassword(savedCredential)) {
throw new AuthenticationException();
}

credentialThreadLocal.set(savedCredential);
return true;
}
}
43 changes: 43 additions & 0 deletions src/main/java/cart/auth/BasicAuthorizationParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cart.auth;

import cart.exception.InvalidBasicCredentialException;
import java.util.Base64;
import org.springframework.stereotype.Component;

@Component
public class BasicAuthorizationParser {

private static final String KEYWORD = "Basic ";
private static final String DELIMITER = ":";
private static final int VALID_CREDENTIAL_SIZE = 2;
private static final int EMAIL_INDEX = 0;
private static final int PASSWORD_INDEX = 1;
private static final String EMPTY = "";

public Credential parse(final String authorizationHeader) {
final String[] credential = parseCredential(authorizationHeader);
if (isInvalidBasicCredential(authorizationHeader)) {
throw new InvalidBasicCredentialException(authorizationHeader);
}
return new Credential(credential[EMAIL_INDEX], credential[PASSWORD_INDEX]);
}

private String[] parseCredential(final String authorizationHeader) {
final String credential = authorizationHeader.replace(KEYWORD, EMPTY);
return decodeBase64(credential).split(DELIMITER);
}

private String decodeBase64(final String credential) {
try {
return new String(Base64.getDecoder().decode(credential));
} catch (final IllegalArgumentException e) {
throw new InvalidBasicCredentialException(credential);
}
}

private boolean isInvalidBasicCredential(final String authorizationHeader) {
return !authorizationHeader.startsWith(KEYWORD) ||
parseCredential(authorizationHeader).length != VALID_CREDENTIAL_SIZE;
}
}

37 changes: 37 additions & 0 deletions src/main/java/cart/auth/Credential.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package cart.auth;

import org.springframework.lang.Nullable;

public class Credential {

private final Long memberId;
private final String email;
private final String password;

public Credential(final String email, final String password) {
this(null, email, password);
}

public Credential(final Long memberId, final String email, final String password) {
this.memberId = memberId;
this.email = email;
this.password = password;
}

public boolean isNotSamePassword(final Credential credential) {
return !this.password.equals(credential.getPassword());
}

@Nullable
public Long getMemberId() {
return memberId;
}

public String getEmail() {
return email;
}

public String getPassword() {
return password;
}
}
21 changes: 21 additions & 0 deletions src/main/java/cart/auth/CredentialThreadLocal.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package cart.auth;

import org.springframework.stereotype.Component;

@Component
public class CredentialThreadLocal {

private final ThreadLocal<Credential> local = new ThreadLocal<>();

public void set(final Credential credential) {
local.set(credential);
}

public Credential get() {
return local.get();
}

public void clear() {
local.remove();
}
}
45 changes: 45 additions & 0 deletions src/main/java/cart/config/WebConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package cart.config;

import cart.auth.AuthArgumentResolver;
import cart.auth.AuthInterceptor;
import cart.auth.BasicAuthorizationParser;
import cart.auth.CredentialThreadLocal;
import cart.dao.CredentialDao;
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

private final CredentialDao credentialDao;
private final BasicAuthorizationParser basicAuthorizationParser;
private final CredentialThreadLocal credentialThreadLocal;

public WebConfiguration(
final CredentialDao credentialDao,
final BasicAuthorizationParser basicAuthorizationParser,
final CredentialThreadLocal credentialThreadLocal
) {
this.credentialDao = credentialDao;
this.basicAuthorizationParser = basicAuthorizationParser;
this.credentialThreadLocal = credentialThreadLocal;
}

@Override
public void addInterceptors(final InterceptorRegistry registry) {
final AuthInterceptor authInterceptor = new AuthInterceptor(
credentialDao,
basicAuthorizationParser,
credentialThreadLocal
);
registry.addInterceptor(authInterceptor).addPathPatterns("/cart-products/**");
}

@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthArgumentResolver(credentialThreadLocal));
}
}
Loading

0 comments on commit cbc8152

Please sign in to comment.