diff --git "a/4Week_Record/4Week_\355\225\234\354\212\271\354\227\260.md" "b/4Week_Record/4Week_\355\225\234\354\212\271\354\227\260.md"
index 1d7151b..bb261e8 100644
--- "a/4Week_Record/4Week_\355\225\234\354\212\271\354\227\260.md"
+++ "b/4Week_Record/4Week_\355\225\234\354\212\271\354\227\260.md"
@@ -1,18 +1,240 @@
## 4Week_Mission
+*표시: 개인적으로 추가한 기능
+
+### ⭐️ 3Week 추가과제 ⭐️
+- [x] withdrawApply 엔티티 설계
+- [x] MemberExtra 엔티티 설계
+ - 회원의 출금 계좌 정보(은행명, 계좌번호) 관리 목적
+- [x] 출금 신청
+ - POST /withdraw/apply
+ - Form: price
+- [x] 출금 신청 내역 리스트(사용자 기능)*
+ - GET /withdraw/applyList
+- [x] 출금 신청 내역 리스트(관리자 기능)
+ - GET /adm/withdraw/applyList
+- [x] 출금 처리
+ - POST /adm/withdraw/{withdrawApplyId}
+- [x] 출금 취소(사용자 기능)*
+ - POST /withdraw/cancel/{withdrawApplyId}
+- [x] 출금 취소(관리자 기능)*
+ - POST /adm/withdraw/cancel/{withdrawApplyId}
### ⭐️ 4Week 필수과제 ⭐️
+- [x] JWT 로그인 구현(POST /api/v1/member/login)
+- [x] 로그인 한 회원의 정보 구현(GET /api/v1/member/me)
+ - [x] MemberDto 추가
+- [x] 내 도서 리스트 구현(GET /api/v1/myBooks)
+ - [x] MyBookDto 추가
+ - [x] ProductDto 추가
+- [x] 내 도서 상세정보 구현(GET /api/v1/myBooks/{myBookId})
+ - [x] MyBookDetailDto 추가
+ - [x] ProductDetailDto 추가
+ - [x] PostDetailDto 추가
+- [x] Srping Doc 으로 API 문서화(/swagger-ui/index.html )
+ - [x] SpringDocConfig 추가
+ - [x] 관리자 회원만 spring doc 접근가능하도록 SecurityConfig 설정
### 👍🏻 4Week 추가과제 👍🏻
+- [x] ERD 완성*
+- [ ] 엑세스 토큰 화이트리스트 구현(Member 엔티티에 accessToken 필드 추가)
+- [x] 리액트 코드 작동 확인
### 🙈 요구사항 및 접근방법 정리 🙈
+### JWT 프로세스
+1. 사용자가 `username, password` 를 입력하고 서버로 로그인 요청을 보낸다.
+2. 로그인 성공시 서버는 비밀키로 서명을 하고 공개키로 암호화 하여 `Access Token` 을 발급한다.
+3. `Authorization Header` 에 `Access Token` 을 담아 클라이언트에게 응답을 보낸다.
+4. 클라이언트는 API를 요청할 때 `Authorization Header` 에 `Access Token` 을 담아 요청을 보낸다.
+5. 서버에서는 `Access Token` 을 검증하고 사용자를 인증한다.
+6. 서버가 요청에 대한 응답을 클라이언트에게 전달한다.
+- https://velog.io/@junghyeonsu/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%84-%EC%B2%98%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95
+---
+### Spring Security + JWT 로그인 구현
+- https://samtao.tistory.com/65
-### ❗️ 특이사항 ❗️
+**1. JWT dependency 추가**
+- `build.gradle` 파일에 jwt 구현을 위해 필요한 의존성을 추가한다.
+```bash
+implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
+runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
+runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
+```
+**2. JWT secretKey 관리**
+- ``JwtConfig`` 는 JWT AccessToken 발급에 사용되는 비밀키를 싱글톤 빈으로 등록하여 관리한다.
+```java
+@Configuration
+public class JwtConfig {
+ @Value("${custom.jwt.secretKey}")
+ private String secretKeyPlain; // 비밀키 원문
+
+ // JWT 비밀키 싱글톤 빈 관리
+ @Bean
+ public SecretKey jwtSecretKey() {
+ String keyBase64Encoded = Base64.getEncoder().encodeToString(secretKeyPlain.getBytes());
+ return Keys.hmacShaKeyFor(keyBase64Encoded.getBytes());
+ }
+}
+```
+- secretKey(원문)은 ``application.yml`` 파일에서 관리한다.
-아쉬웠던 점
+**3. JWT 토큰(AccessToken) 발급**
+- ``JwtProvider`` 은 JWT 토큰을 발급하는 역할을 하고 비밀키를 이용해 토큰을 생성한다.
+```java
+// JWT Access Token 발급
+public String generateAccessToken(Map claims, int seconds) {
+ long now = new Date().getTime();
+ Date accessTokenExpiresIn = new Date(now + 1000L * seconds);
+
+ return Jwts.builder()
+ .claim("body", Ut.json.toStr(claims)) // Claims 정보 설정
+ .setExpiration(accessTokenExpiresIn) // accessToken 만료 시간 설정
+ .signWith(getSecretKey(), SignatureAlgorithm.HS512) // HS512, 비밀키로 서명
+ .compact(); // 토큰 생성
+}
+```
+1. `Jwts.builder()` 를 이용해 `JwtBuilder` 객체를 생성한다.
+2. Headers, Claims, 토큰 용도, 토큰 만료 시간 등을 설정한다.
+3. `HS512(서명 알고리즘), 비밀키` 로 서명한다.
+4. `compact()` 로 토큰을 생성한다.
+**4. JWT 토큰(AccessToken) 검증**
+- ``JwtProvider`` 은 JWT 토큰을 검증하는 역할을 하고 공개키를 이용해 토큰을 검증한다.
+```java
+// JWT Access Token 검증
+public boolean verify(String accessToken) {
+ try {
+ Jwts.parserBuilder()
+ .setSigningKey(getSecretKey()) // 비밀키
+ .build()
+ .parseClaimsJws(accessToken); // 파싱 및 검증(실패시 에러)
+ } catch (ExpiredJwtException e) {
+ // 토큰이 만료되었을 경우
+ return false;
+ }
+ catch (Exception e) {
+ // 그 외 에러
+ return false;
+ }
+ return true;
+}
+```
+1. ``Jwts.parserBuilder()`` 를 이용해 ``JwtParserBuilder`` 객체를 생성한다.
+2. 서명 검증을 위한 비밀키를 지정한다.
+3. `parseClaimsJws()` 로 파싱 및 서명 검증을 한다. (예외처리를 위해 try-catch)
-궁금했던 점
+**5. JWT 토큰(AccessToken) 으로부터 Claim 정보 가져오기**
+- ``JwtProvider`` 은 JWT 토큰으로부터 Claim 정보를 가져오는 역할을 한다.
+```java
+// accessToken 으로부터 Claim 정보 얻기
+public Map getClaims(String accessToken) {
+ String body = Jwts.parserBuilder()
+ .setSigningKey(getSecretKey())
+ .build()
+ .parseClaimsJws(accessToken)
+ .getBody()
+ .get("body", String.class);
+
+ return Ut.json.toMap(body);
+}
+```
+1~3. 위와 동일
+4. `getBody()` 로 claim 정보를 가져온다.
+
+**6. REST API 요청 Security 설정**
+- REST API 요청이 들어왔을 때는 JWT 방식으로 인증을 수행해야하므로 `ApiSecurityConfig` 에 관련 설정을 추가한다.
+```java
+ @Bean
+ public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
+ http
+ ...
+ .authorizeRequests(
+ authorizeRequests -> authorizeRequests
+ // 로그인 요청 외 모든 요청은 로그인 필수
+ .antMatchers("/api/*/member/login").permitAll()
+ .anyRequest()
+ .authenticated() // 최소자격 : 로그인
+ )
+ .sessionManagement(sessionManagement -> sessionManagement
+ .sessionCreationPolicy(STATELESS)
+ )
+ .addFilterBefore(
+ jwtAuthorizationFilter,
+ UsernamePasswordAuthenticationFilter.class
+ )
+ ...
+ return http.build();
+ }
+```
+- `/api/*/member/login` 요청 외 모든 `/api/**` 요청은 인증된 사용자여야 한다.
+- 지정된 필터보다 먼저 실행되도록 `jwtAuthorizationFilter` (커스텀 필터) 를 추가한다.
+
+**7. jwtAuthorizationFilter 추가**
+- REST API 요청이 Controller 에 도달하기 이전에 앞 단(Filter 혹은 Interceptor)에서 인증/인가를 수행한다.
+```java
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class JwtAuthorizationFilter extends OncePerRequestFilter {
+ private final JwtProvider jwtProvider;
+ private final MemberService memberService;
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+ String barerToken = request.getHeader("Authorization");
+ // 토큰 유효성 검증
+ if(barerToken != null) {
+ String token = barerToken.substring("Barer ".length());
+ // 토큰이 유효하면 회원 정보 얻어서 강제 로그인 처리
+ if(jwtProvider.verify(token)) {
+ Map claims = jwtProvider.getClaims(token);
+ String username = (String) claims.get("username");
+ Member member = memberService.findByUsername(username);
+
+ if(member != null) {
+ forceAuthentication(member);
+ }
+ }
+ }
+ filterChain.doFilter(request, response);
+ }
+
+ // 강제 로그인 처리
+ private void forceAuthentication(Member member) {
+ MemberContext memberContext = new MemberContext(member);
+
+ UsernamePasswordAuthenticationToken authentication =
+ UsernamePasswordAuthenticationToken.authenticated(
+ memberContext,
+ null, member.getAuthorities()
+ );
+
+ // 이후 컨트롤러 단에서 MemberContext 객체 사용O
+ SecurityContext context = SecurityContextHolder.createEmptyContext();
+ context.setAuthentication(authentication);
+ SecurityContextHolder.setContext(context);
+ }
+}
+```
+1. 요청 헤더의 `Access Token` 을 검증한다.
+2. 토큰으로부터 `claim(회원 정보)` 를 얻어 DB에서 Member 객체 조회한다.
+3. 해당 회원 강제 로그인 처리한다.(`MemberContext` 세션 등록)
+---
+### Spring Doc API 문서화
+1. spring doc dependency 추가(build.gradle)
+```bash
+implementation 'org.springdoc:springdoc-openapi-ui:1.6.11'
+```
+2. SpringDocConfig 추가
+
+---
+궁금했던 점
+- api 패키지 위치 어디에 해야하는가?
+- AuthLevel 같은 enum 클래스는 어느 패키지의 하위에 생성해야는가?
Refactoring
+- 토스 카드 결제 환불 처리 구현
+- 관리자 회원으로 로그인하면 관리자 회원 메인페이지로 리다이렉트하기
+- 장바구니 단건 삭제에서는 productId 를 넘기고 선택 삭제에서는 cartItemId 를 넘기는 부분을 모두 cartItemId를 넘기도록 통일
+- 금액 콤마 표시(프론트)