Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Spring Data JPA] 정상희 미션 제출합니다. #109

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
eac6692
[FEAT] 리드미 작성
SANGHEEJEONG Dec 24, 2024
e1b4ce3
[FEAT] 로그인, 인증 정보 조회 (1단계)
SANGHEEJEONG Dec 25, 2024
0186fcf
[FEAT] 리드미 작성 (2단계)
SANGHEEJEONG Dec 26, 2024
769b6bf
[FEAT] ArgumentResolver 구현 + 예약 리팩터링 (2단계)
SANGHEEJEONG Dec 26, 2024
9b180fb
[FEAT] 리드미 작성 (3단계)
SANGHEEJEONG Dec 26, 2024
2ebef37
[FEAT] 어드민 페이지 권한 (3단계)
SANGHEEJEONG Dec 26, 2024
f9c1a7d
[REFACTOR] 클래스명 변경 및 코드 정리
SANGHEEJEONG Dec 26, 2024
c83b84b
[FEAT] 리드미 작성 (4단계)
SANGHEEJEONG Jan 7, 2025
36b7dd3
[FEAT] Dao -> Repository (4단계)
SANGHEEJEONG Jan 8, 2025
9cfe2f7
[FEAT] 리드미 작성 (5단계)
SANGHEEJEONG Jan 8, 2025
112420c
[FEAT] 내 예약 목록 조회 (5단계)
SANGHEEJEONG Jan 9, 2025
2b497bf
[FEAT] 중복 예약 방지 및 예약 대기 기능 (6단계)
SANGHEEJEONG Jan 9, 2025
2e0884e
[REFACTOR] AuthClaims 와 LoginResponse 분리
SANGHEEJEONG Jan 13, 2025
0cfc7eb
[REFACTOR] 토큰 추출 메서드 중복 제거
SANGHEEJEONG Jan 13, 2025
ff3ad6a
[REFACTOR] Role enum 타입으로 변경
SANGHEEJEONG Jan 13, 2025
75132bc
[REFACTOR] 예외처리 핸들러 작성
SANGHEEJEONG Jan 13, 2025
fde3d1d
[REFACTOR] 커스텀 어노테이션 적용
SANGHEEJEONG Jan 13, 2025
1f2e385
[REBASE] mvc 리베이스 후 충돌 해결
SANGHEEJEONG Jan 15, 2025
1d62e9a
[REBASE] mvc 리베이스 충돌 해결 2
SANGHEEJEONG Jan 15, 2025
d5e2ad0
[FEAT] 리드미 작성 (5단계)
SANGHEEJEONG Jan 8, 2025
ae765ec
[REBASE] mvc 리베이스 충돌 해결 3
SANGHEEJEONG Jan 15, 2025
71cd274
[REBASE] mvc 리베이스 충돌 해결 4
SANGHEEJEONG Jan 15, 2025
9bcfcbf
[REFACTOR] service 웹 기술 제거
SANGHEEJEONG Jan 15, 2025
ec05e2c
[REFACTOR] DTO 변환 로직 분리
SANGHEEJEONG Jan 15, 2025
4b0e950
[REFACTOR] jpa를 위한 생성자 오용 방지
SANGHEEJEONG Jan 15, 2025
d47ce84
[REFACTOR] name -> id 조회로 변경 및 권한 추가
SANGHEEJEONG Jan 15, 2025
43f3c71
[REFACTOR] joinColumn 추가
SANGHEEJEONG Jan 15, 2025
b8b073f
[REFACTOR] joinColumn 추가 (Waiting 엔티티)
SANGHEEJEONG Jan 15, 2025
ebfb5d9
[REFACTOR] WaitingService 조회 수정
SANGHEEJEONG Jan 15, 2025
a89b961
[REFACTOR] 코드 정렬
SANGHEEJEONG Jan 15, 2025
0b1819c
Merge branch 'sanghee-jpa' of https://github.com/SANGHEEJEONG/spring-…
SANGHEEJEONG Jan 15, 2025
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
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# 🌀 Spring MVC (인증)

# 1단계
___
## 로그인 페이지
+ 이메일, 비밀번호 입력
## 로그인 요청
+ 이메일, 비밀번호 -> 멤버 조회
+ 조회한 멤버로 토큰 발급
+ Cookie를 만들어 응답
## 인증 정보 조회
+ Cookie -> 토큰 정보 추출
+ 멤버를 찾아서 응답
## API
+ GET/login : 로그인 페이지 호출
+ POST/login : 로그인 요청
+ GET/login/check : 인증 정보 조회

# 2단계
___
## 로그인 리팩터링
#### HandlerMethodArgumentResolver
+ 컨트롤러 메서드 파라미터로 자동 주입

## 예약 생성 기능 변경
+ 예약 : ReservationRequest(요청 DTO)
-> name이 있으면 name으로 Member 찾기
-> name이 없으면 Cookie에 담긴 정보 활용

# 3단계
___
## 관리자 기능
+ admin 페이지 진입 (HandlerInterceptor 이용)
-> 관리자 : 진입 가능
-> 관리자 X : 401 코드 응답

# 🌀 Spring JPA (인증)

# 4단계
___
## JPA 전환
+ 엔티티 & 연관 관계 매핑
+ DAO -> JpaRepository를 상속받는 Repository로 대체

# 5단계
___
## 내 예약 목록 조회
+ 응답 DTO -> 예약 아이디, 테마, 날짜, 시간, 상태를 포함
## 예약 테이블 수정
+ 관리자 예약 (어드민 화면) : name을 string으로 전달
+ 사용자 예약 (예약 화면) : 로그인 정보를 이용해 Member ID 저장
## API
+ GET/reservation-mine : reservation-mine 페이지 응답
+ GET/reservations-mine : 내 예약 목록 조회

# 6단계
___
## 예약 대기
+ 요청
+ 취소
+ 조회 : N번 째 예약 대기인지 표시
## API
+ POST/waitings : 예약 대기 생성
+ DELETE/waitings/{id} : 예약 삭제
SANGHEEJEONG marked this conversation as resolved.
Show resolved Hide resolved

8 changes: 7 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,20 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
// implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.0.0'

implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.2'
implementation 'io.jsonwebtoken:jjwt-gson:0.11.2'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'

Expand Down
14 changes: 0 additions & 14 deletions src/main/java/roomescape/ExceptionController.java

This file was deleted.

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

public record AuthClaims(
Long id,
String name,
String role
) {
}
33 changes: 33 additions & 0 deletions src/main/java/roomescape/auth/AuthClaimsArgumentResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package roomescape.auth;

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
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.context.request.ServletWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@Component
@RequiredArgsConstructor
public class AuthClaimsArgumentResolver implements HandlerMethodArgumentResolver {

private final JWTUtils jwtUtils;

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(AuthCustomAnnotation.class) &&
parameter.getParameterType().equals(AuthClaims.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest();
String token = jwtUtils.extractToken(request).token();

return jwtUtils.getClaimsFromToken(token);
}
}

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

import java.lang.annotation.Documented;
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)
@Documented
public @interface AuthCustomAnnotation {
}
26 changes: 26 additions & 0 deletions src/main/java/roomescape/auth/AuthRoleInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package roomescape.auth;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
@RequiredArgsConstructor
public class AuthRoleInterceptor implements HandlerInterceptor {

private final JWTUtils jwtUtils;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = jwtUtils.extractToken(request).token();

if (!jwtUtils.getClaimsFromToken(token).role().equals("ADMIN")) {
response.setStatus(401);
return false;
}

return true;
}
}
6 changes: 6 additions & 0 deletions src/main/java/roomescape/auth/AuthToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package roomescape.auth;

public record AuthToken(
String token
) {
}
27 changes: 27 additions & 0 deletions src/main/java/roomescape/auth/AuthWebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package roomescape.auth;

import lombok.RequiredArgsConstructor;
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;

import java.util.List;

@Configuration
@RequiredArgsConstructor
public class AuthWebConfig implements WebMvcConfigurer {

private final AuthClaimsArgumentResolver loginMemberArgumentResolver;
private final AuthRoleInterceptor roleCheckInterceptor;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginMemberArgumentResolver);
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(roleCheckInterceptor).addPathPatterns("/admin/**");
}
}
70 changes: 70 additions & 0 deletions src/main/java/roomescape/auth/JWTUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package roomescape.auth;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import roomescape.exception.InvalidTokenException;
import roomescape.exception.MissingTokenException;
import roomescape.member.Member;

import java.util.Arrays;

@Component
public class JWTUtils {

@Value("${roomescape.auth.jwt.secret}")
private String secretKey;

public AuthToken createToken(Member member) {
String accessToken = Jwts.builder()
.setSubject(member.getId().toString())
.claim("name", member.getName())
.claim("role", member.getRole())
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
.compact();

return new AuthToken(accessToken);
}

public AuthToken extractToken(HttpServletRequest request){
String token = Arrays.stream(request.getCookies())
.filter(cookie -> "token".equals(cookie.getName()))
.findFirst()
.map(Cookie::getValue)
.orElseThrow(() -> new IllegalArgumentException("토큰을 찾을 수 없습니다."));

return new AuthToken(token);
}

public AuthClaims getClaimsFromToken(String token) {
if (token == null || token.isBlank()) {
throw new MissingTokenException("토큰이 비어있습니다.");
}

try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes()))
.build()
.parseClaimsJws(token)
.getBody();

Long id = claims.get("id", Long.class);
String name = claims.get("name", String.class);
String role = claims.get("role", String.class);

if (id == null || name == null || role == null) {
throw new InvalidTokenException("필수 클레임이 누락되었습니다.");
}

return new AuthClaims(id, name, role);

} catch (JwtException e) {
throw new InvalidTokenException("유효하지 않은 토큰입니다.");
}
}
}
39 changes: 39 additions & 0 deletions src/main/java/roomescape/exception/ExceptionController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package roomescape.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class ExceptionController {
@ExceptionHandler(InvalidTokenException.class)
public ResponseEntity<?> handleInvalidTokenException(InvalidLoginException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage());
}

@ExceptionHandler(MissingTokenException.class)
public ResponseEntity<String> handleMissingTokenException(InvalidLoginException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage());
}

@ExceptionHandler(InvalidLoginException.class)
public ResponseEntity<String> handleInvalidLoginException(InvalidLoginException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
}

@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<String> handleMemberNotFoundException(MemberNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
}

@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
return ResponseEntity.internalServerError().body("의도되지 않은 에러가 발생했습니다");
}

@ExceptionHandler(RoomescapeException.class)
public ResponseEntity<String> handleExceptions(RoomescapeException e) {
return ResponseEntity.internalServerError().body("프로그램 내 에러가 발생했습니다.");
}
}
7 changes: 7 additions & 0 deletions src/main/java/roomescape/exception/InvalidLoginException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.exception;

public class InvalidLoginException extends RoomescapeException {
public InvalidLoginException(String message) {
super(message);
}
}
7 changes: 7 additions & 0 deletions src/main/java/roomescape/exception/InvalidTokenException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.exception;

public class InvalidTokenException extends RoomescapeException{
public InvalidTokenException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.exception;

public class MemberNotFoundException extends RoomescapeException {
public MemberNotFoundException(String message) {
super(message);
}
}
7 changes: 7 additions & 0 deletions src/main/java/roomescape/exception/MissingTokenException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.exception;

public class MissingTokenException extends RoomescapeException{
public MissingTokenException(String message) {
super(message);
}
}
7 changes: 7 additions & 0 deletions src/main/java/roomescape/exception/RoomescapeException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.exception;

public class RoomescapeException extends RuntimeException {
public RoomescapeException(String message) {
super(message);
}
}
7 changes: 7 additions & 0 deletions src/main/java/roomescape/member/LoginRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.member;

public record LoginRequest(
String email,
String password
) {
}
6 changes: 6 additions & 0 deletions src/main/java/roomescape/member/LoginResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package roomescape.member;

public record LoginResponse (
String name
){
}
Loading