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 Core] 원태연, 이승용 미션 제출합니다. #63

Open
wants to merge 42 commits into
base: kokodak
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
6cf4a6d
test: [1단계] 테스트 적용
TaeyeonRoyce Jun 5, 2024
d0f6595
chore: [1단계] jwt config 값 설정
TaeyeonRoyce Jun 5, 2024
e21365b
feat: [1단계] payload에 따른 jwt 생성 구현
TaeyeonRoyce Jun 5, 2024
76666e2
refactor: [1단계] 회원 인증 구현 방식 추상화
TaeyeonRoyce Jun 5, 2024
3e1fb84
feat: [1단계] 로그인 API 개발
TaeyeonRoyce Jun 7, 2024
61b3924
feat: [1단계] 토큰 parsing 로직 구현
TaeyeonRoyce Jun 7, 2024
2a82b2f
refactor: [1단계] 로그인 검증 로직 수정
TaeyeonRoyce Jun 7, 2024
226f8c1
feat: [1단계] 로그인 검증 API 추가
TaeyeonRoyce Jun 7, 2024
c556cfe
test: [1단계] Test시 Context 마다 embedded DB를 갖도록 수정
TaeyeonRoyce Jun 23, 2024
563aedf
test: [2단계] 이단계 테스트 적용
TaeyeonRoyce Jun 23, 2024
99517ca
feat: [2단계] 로그인 회원 공통 처리 로직 추가
TaeyeonRoyce Jun 23, 2024
cd4bc3a
feat: [2단계] 인증 ArgumentResolver 등록
TaeyeonRoyce Jun 23, 2024
9710322
refactor: [2단계] Dto Record class로 변경
TaeyeonRoyce Jun 23, 2024
05a63f2
feat: [2단계] 회원 이름 여부에 따른 예약 생성 API 추가
TaeyeonRoyce Jun 23, 2024
3042ff5
test: [3단계] 삼단계 테스트 적용
TaeyeonRoyce Jun 23, 2024
e5219ad
feat: [3단계] 어드민 인가 인터셉터 추가
TaeyeonRoyce Jun 23, 2024
a9260e9
feat: [3단계] 어드민 권한 체크 인터셉터 등록
TaeyeonRoyce Jun 23, 2024
1a11963
build: Spring Data JPA 의존성 추가
kokodak Jul 1, 2024
c799603
chore: JPA 관련 properties 내용 수정
kokodak Jul 1, 2024
6672791
chore: sql 파일 이름 변경 및 DDL 삭제
kokodak Jul 1, 2024
56f3d96
feat: Time 도메인을 JPA로 마이그레이션
kokodak Jul 1, 2024
0695597
feat: Theme 도메인을 JPA로 마이그레이션
kokodak Jul 1, 2024
5615039
feat: Reservation 도메인을 JPA로 마이그레이션
kokodak Jul 1, 2024
9573c84
feat: Member 도메인을 JPA로 마이그레이션
kokodak Jul 1, 2024
e65df61
test: 4단계 테스트 코드 작성
kokodak Jul 1, 2024
396a3a6
feat: 단일 조회 메서드에서 default 로 공통 예외처리 하도록 수정
kokodak Jul 1, 2024
6003474
refactor: 단건 조회시 Optional 로 감싸서 반환하도록 수정
kokodak Jul 1, 2024
6867ee1
fix: save() 로직 오류 수정
kokodak Jul 1, 2024
44febba
feat: Reservation 도메인이 Member를 의존하도록 변경
kokodak Jul 1, 2024
990e69e
chore: 더미 데이터 삽입 구문 추가
kokodak Jul 1, 2024
30e7cad
feat: 내 예약 목록 조회 기능 구현
kokodak Jul 1, 2024
51f611a
test: 5단계 테스트 코드 추가
kokodak Jul 1, 2024
0f1c573
feat: 예약 대기 요청 기능 구현
kokodak Jul 1, 2024
a7b2ca2
feat: 중복 예약이 불가능하도록 구현
kokodak Jul 1, 2024
15a3998
feat: 내 예약 목록 조회 시에, 예약 대기 목록도 함께 조회하도록 구현
kokodak Jul 1, 2024
5368d09
test: 6단계 테스트 코드 추가
kokodak Jul 1, 2024
449eba0
refactor: API 스펙에 맞도록 변수명 조정
kokodak Jul 1, 2024
62d4372
refactor: 몇번째 예약 대기인지 확인할 수 있도록 로직 추가
kokodak Jul 1, 2024
1a0be81
feat: 예약 대기 취소 기능 구현
kokodak Jul 1, 2024
f0a357c
test: 깨지는 테스트 코드 일부 수정
kokodak Jul 1, 2024
468a0f5
refactor: 7단계 요구사항 구현
kokodak Jul 6, 2024
0c1e467
feat: 8단계 요구사항 구현
kokodak Jul 6, 2024
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 build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ 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-data-jpa'

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

Expand Down
24 changes: 24 additions & 0 deletions src/main/java/roomescape/DataLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package roomescape;

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import roomescape.member.Member;
import roomescape.member.MemberRepository;

@Profile("default")
@Component
public class DataLoader implements CommandLineRunner {

private final MemberRepository memberRepository;

public DataLoader(final MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}

@Override
public void run(final String... args) throws Exception {
final Member member1 = memberRepository.save(new Member("어드민", "admin@email.com", "password", "ADMIN"));
final Member member2 = memberRepository.save(new Member("브라운", "brown@email.com", "password", "USER"));
}
}
59 changes: 59 additions & 0 deletions src/main/java/roomescape/TestDataLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package roomescape;

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import roomescape.member.Member;
import roomescape.member.MemberRepository;
import roomescape.reservation.Reservation;
import roomescape.reservation.ReservationRepository;
import roomescape.theme.Theme;
import roomescape.theme.ThemeRepository;
import roomescape.time.Time;
import roomescape.time.TimeRepository;

@Profile("test")
@Component
public class TestDataLoader implements CommandLineRunner {

private final MemberRepository memberRepository;

private final ThemeRepository themeRepository;

private final TimeRepository timeRepository;

private final ReservationRepository reservationRepository;

public TestDataLoader(final MemberRepository memberRepository,
final ThemeRepository themeRepository,
final TimeRepository timeRepository,
final ReservationRepository reservationRepository) {
this.memberRepository = memberRepository;
this.themeRepository = themeRepository;
this.timeRepository = timeRepository;
this.reservationRepository = reservationRepository;
}

@Override
public void run(final String... args) throws Exception {
final Member member1 = memberRepository.save(new Member("어드민", "admin@email.com", "password", "ADMIN"));
final Member member2 = memberRepository.save(new Member("브라운", "brown@email.com", "password", "USER"));

final Theme theme1 = themeRepository.save(new Theme("테마1", "테마1입니다."));
final Theme theme2 = themeRepository.save(new Theme("테마2", "테마2입니다."));
final Theme theme3 = themeRepository.save(new Theme("테마3", "테마3입니다."));

final Time time1 = timeRepository.save(new Time("10:00"));
final Time time2 = timeRepository.save(new Time("12:00"));
final Time time3 = timeRepository.save(new Time("14:00"));
final Time time4 = timeRepository.save(new Time("16:00"));
final Time time5 = timeRepository.save(new Time("18:00"));
final Time time6 = timeRepository.save(new Time("20:00"));

reservationRepository.save(new Reservation("어드민", "2024-03-01", time1, theme1, member1));
reservationRepository.save(new Reservation("어드민", "2024-03-01", time2, theme2, member1));
reservationRepository.save(new Reservation("어드민", "2024-03-01", time3, theme3, member1));

reservationRepository.save(new Reservation("브라운", "2024-03-01", time4, theme1, member2));
}
}
44 changes: 44 additions & 0 deletions src/main/java/roomescape/auth/AdminInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package roomescape.auth;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Optional;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class AdminInterceptor implements HandlerInterceptor {

private final AuthorizationProvider authorizationProvider;

public AdminInterceptor(AuthorizationProvider authorizationProvider) {
this.authorizationProvider = authorizationProvider;
}

@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler
) {
Optional<String> token = Arrays.stream(request.getCookies())
.filter(cookie -> cookie.getName().equals("token"))
.findFirst()
.map(Cookie::getValue);
if (token.isEmpty()) {
response.setStatus(401);
return false;
}

MemberCredential memberCredential = new MemberCredential(token.get());
MemberAuthContext memberAuthContext = authorizationProvider.parseCredential(memberCredential);
if (!memberAuthContext.role().equalsIgnoreCase("admin")) {
response.setStatus(401);
return false;
}

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

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import roomescape.auth.jwt.JwtUtils;

@Configuration
public class AuthConfig {

@Bean
public JwtUtils jwtUtils(@Value("${roomescape.auth.jwt.secret}") String jwtSecret,
@Value("${roomescape.auth.jwt.expiration}") Long expireMilliseconds) {
return new JwtUtils(jwtSecret, expireMilliseconds);
}
}
49 changes: 49 additions & 0 deletions src/main/java/roomescape/auth/AuthMemberArgumentResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package roomescape.auth;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
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
public class AuthMemberArgumentResolver implements HandlerMethodArgumentResolver {

private final AuthorizationProvider authorizationProvider;

public AuthMemberArgumentResolver(AuthorizationProvider authorizationProvider) {
this.authorizationProvider = authorizationProvider;
}

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

@Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory
) {
String token = parseTokenFromNativeRequest((ServletWebRequest) webRequest);
MemberCredential memberCredential = new MemberCredential(token);
return authorizationProvider.parseCredential(memberCredential);
}

private String parseTokenFromNativeRequest(ServletWebRequest webRequest) {
HttpServletRequest httpServletRequest = webRequest.getRequest();
return Arrays.stream(httpServletRequest.getCookies())
.filter(cookie -> cookie.getName().equals("token"))
.findFirst()
.map(Cookie::getValue)
.orElseThrow(() -> new IllegalArgumentException("로그인이 필요합니다."));
}
}
33 changes: 33 additions & 0 deletions src/main/java/roomescape/auth/AuthWebConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package roomescape.auth;

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 AuthWebConfiguration implements WebMvcConfigurer {

private final AuthMemberArgumentResolver authMemberArgumentResolver;
private final AdminInterceptor adminInterceptor;

public AuthWebConfiguration(
AuthMemberArgumentResolver authMemberArgumentResolver,
AdminInterceptor adminInterceptor
) {
this.authMemberArgumentResolver = authMemberArgumentResolver;
this.adminInterceptor = adminInterceptor;
}

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

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

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Authentication {
}
8 changes: 8 additions & 0 deletions src/main/java/roomescape/auth/AuthorizationProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package roomescape.auth;

public interface AuthorizationProvider {

MemberCredential create(MemberAuthContext member);

MemberAuthContext parseCredential(MemberCredential token);
}
7 changes: 7 additions & 0 deletions src/main/java/roomescape/auth/MemberAuthContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.auth;

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

public record MemberCredential(
String authorization
) {
}
6 changes: 6 additions & 0 deletions src/main/java/roomescape/auth/jwt/JwtTokenInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package roomescape.auth.jwt;

public record JwtTokenInfo(
String accessToken
) {
}
52 changes: 52 additions & 0 deletions src/main/java/roomescape/auth/jwt/JwtUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package roomescape.auth.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import java.util.Date;
import roomescape.auth.AuthorizationProvider;
import roomescape.auth.MemberAuthContext;
import roomescape.auth.MemberCredential;

public class JwtUtils implements AuthorizationProvider {

private static final String USER_NAME = "name";
private static final String USER_ROLE = "role";

private final String jwtSecret;
private final Long validityInMilliseconds;

public JwtUtils(String jwtSecret,
Long expireMilliseconds) {
this.jwtSecret = jwtSecret;
this.validityInMilliseconds = expireMilliseconds;
}

public MemberCredential create(MemberAuthContext context) {
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
String tokenValue = Jwts.builder()
.claim(USER_NAME, context.name())
.claim(USER_ROLE, context.role())
.setIssuedAt(now)
.setExpiration(validity)
.signWith(Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(jwtSecret)))
.compact();
return new MemberCredential(tokenValue);
}

@Override
public MemberAuthContext parseCredential(MemberCredential token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(jwtSecret)))
.build()
.parseClaimsJws(token.authorization())
.getBody();

return new MemberAuthContext(
claims.get(USER_NAME, String.class),
claims.get(USER_ROLE, String.class)
);
}
}
17 changes: 17 additions & 0 deletions src/main/java/roomescape/member/Member.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
package roomescape.member;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Member {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

private String email;

private String password;

private String role;

public Member(Long id, String name, String email, String role) {
Expand All @@ -21,6 +34,10 @@ public Member(String name, String email, String password, String role) {
this.role = role;
}

public Member() {

}

public Long getId() {
return id;
}
Expand Down
Loading