Skip to content
Open
Show file tree
Hide file tree
Changes from 80 commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
5a89792
refactor(MissionStepTest): [1단계] 일단계 테스트 코드 추가
developowl Dec 24, 2024
acd8813
feat(TokenRequest): [1단계] 사용자 로그인 요청 DTO 생성
developowl Dec 29, 2024
013c8a8
feat(TokenLoginController): [1단계] 사용자 인증 및 로그인 요청 처리 기능 추가(tokenLogin)
developowl Dec 29, 2024
3569c13
feat(AuthService): [1단계] 사용자 인증 처리 클래스 생성
developowl Dec 29, 2024
1ddb5a4
feat(JwtTokenProvider): [1단계] 토큰 생성 및 검증 클래스 생성
developowl Dec 29, 2024
6cf51d4
refactor(MemberDao): [1단계] email로 사용자를 찾는 메서드 추가(findByEmail)
developowl Dec 29, 2024
1f22ed7
feat(TokenResponse): [1단계] 로그인 응답 클래스 생성
developowl Dec 29, 2024
ee6dac0
refactor(TokenLoginController): [1단계] 사용자 인증 상태 확인 요청 기능 추가(checkLogin)
developowl Dec 29, 2024
91bd50f
feat(BearerAuthorizationExtractor): [1단계] Bearer 토큰 추출 클래스 추가
developowl Dec 29, 2024
e7c9b87
feat(AuthorizationExtractor): [1단계] 인증 정보 추출을 위한 인터페이스 생성
developowl Dec 29, 2024
7773f89
feat(AuthorizationExtractor): [1단계] 인증 관련 예외 처리 클래스 생성
developowl Dec 29, 2024
aa6384d
refactor(AuthService): [1단계] 인증 실패 예외처리 통일
developowl Dec 29, 2024
42c9296
refactor(application.properties): [1단계] jwt properties 추가
developowl Dec 29, 2024
e39fa6e
refactor(TokenLoginController): [1단계] 코드 문법 수정(로직 동일)
developowl Dec 30, 2024
12e4981
refactor(MissionStepTest): [2단계] 이단계 테스트 코드 추가 및 토큰 추출 메서드 생성
developowl Jan 1, 2025
084f339
feat(LoginMemberArgumentResolver): [2단계] 쿠키를 통해 멤버 정보를 조회하는 ArgumentR…
developowl Jan 1, 2025
a9d0815
feat(LoginMember): [2단계] LoginMemberArgumentResolver 를 통해 조회한 멤버 정보를 …
developowl Jan 1, 2025
75870e3
refactor(AuthService): [2단계] 이메일로 멤버를 조회하는 메서드의 반환형 변경(MemberResponse…
developowl Jan 1, 2025
1ad00b6
refactor(TokenLoginController): [2단계] loginMember 를 통해 전달된 정보를 응답으로 바…
developowl Jan 1, 2025
6562345
refactor(ReservationController): [2단계] name이 없는 경우 Cookie에 담긴 정보를 활용하…
developowl Jan 1, 2025
c9a7b9c
refactor(ReservationRequest): [2단계] DB에서 회원 데이터를 조회하기 위해 memberId 필드 추가
developowl Jan 1, 2025
96df506
refactor(MissionStepTest): [3단계] 삼단계 테스트 코드 추가
developowl Jan 1, 2025
1021263
feat(WebConfig): WebConfig 추가(Resolvers, interceptors)
developowl Jan 1, 2025
0dd51e8
feat(AdminAccessInterceptor): [3단계] Interceptor 생성
developowl Jan 1, 2025
59aecdc
[질문]: build.gradle
developowl Jan 1, 2025
7622e3f
주석 및 공백 정리
developowl Jan 1, 2025
d09b798
refactor(build.gradle): [4단계] jdbc -> jpa 의존성 변경
developowl Jan 8, 2025
999bca1
refactor(application.properties): [4단계] jpa 의존성 추가
developowl Jan 8, 2025
aad687f
refactor(Dao): [4단계] 전체 DAO 클래스 삭제
developowl Jan 8, 2025
7d9cead
feat(TimeRepository): [4단계] Dao -> TimeRepository 생성
developowl Jan 8, 2025
920de2e
refactor(TimeService): [4단계] Dao -> Repository 로직 변경
developowl Jan 8, 2025
c7b5651
refactor(TimeController): [4단계] 메서드명 변경(getValue -> getTime)
developowl Jan 8, 2025
664d873
refactor(Theme): [4단계] Theme 클래스 엔티티 설정 및 매핑
developowl Jan 8, 2025
4e6e0e2
feat(ThemeRepository): [4단계] Dao -> ThemeRepository 생성
developowl Jan 8, 2025
d2d7c24
refactor(ThemeController): [4단계] Dao -> Repository 방식으로 리팩토링
developowl Jan 8, 2025
880c053
feat(MemberRepository): [4단계] Dao -> MemberRepository 생성
developowl Jan 8, 2025
1694a87
refactor(Member): [4단계] Member 클래스 엔티티 설정 및 매핑
developowl Jan 8, 2025
2478292
refactor(MemberService): [4단계] Dao -> Repository 로직 변경
developowl Jan 8, 2025
b767ae7
refactor(AuthService): [4단계] Dao -> Repository 로직 변경
developowl Jan 8, 2025
357734e
feat(ReservationRepository): [4단계] Dao -> ReservationRepository 생성
developowl Jan 8, 2025
56d04ca
refactor(Reservation): [4단계] Reservation 클래스 엔티티 설정 및 매핑
developowl Jan 8, 2025
85c6191
refactor(Reservation): [4단계] Request, Response 필드 변수 자료형 변경(String ->…
developowl Jan 8, 2025
e74ba1b
refactor(Reservation): [4단계] Dao -> Repository 로직 변경
developowl Jan 8, 2025
3ccbabf
refactor(schema.sql): [4단계] 테이블 생성 구문 삭제(hibernate와 중복 생성 때문)
developowl Jan 8, 2025
f348d60
refactor(MissionStepTest): [4단계] 사단계 테스트 코드 추가
developowl Jan 8, 2025
ca6e81a
refactor(MissionStepTest): [5단계] 오단계 테스트 코드 추가
developowl Jan 8, 2025
6b03aaf
refactor(MissionStepTest): 테스트 코드 추가(사단계, 오단계)
developowl Jan 10, 2025
6f22cb2
1~4단계: 오류 정리 및 리팩토링(다시 시작..)
developowl Jan 11, 2025
8ede4af
1~4단계: 오류 정리 및 리팩토링 2(다시 시작..)
developowl Jan 11, 2025
595ad00
refactor(MissionStepTest): [5단계] 오단계 테스트 코드 추가
developowl Jan 11, 2025
68af541
refactor(schema): [5단계] 초기값 설정 쿼리 수정
developowl Jan 11, 2025
a5e4367
feat(MyReservationResponse): [5단계] 내 예약 조회 DTO 추가
developowl Jan 11, 2025
3244cf2
refactor(ReservationRepository): [5단계] findByName 메서드 등록
developowl Jan 11, 2025
abd6f42
refactor(ReservationService): [5단계] 내 예약 조회 로직 생성
developowl Jan 11, 2025
e194f03
refactor(ReservationController): [5단계] 내 예약 조회 API 생성
developowl Jan 11, 2025
d003cbc
refactor(MissionStepTest): [6단계] 육단계 테스트 코드 추가
developowl Jan 12, 2025
f973982
feat(DuplicateReservationException): [6단계] 중복 예약 커스텀 예외 생성
developowl Jan 12, 2025
aa3847d
feat(Waiting): [6단계] 예약 대기 엔티티 생성
developowl Jan 12, 2025
5360403
feat(WaitingWithRank): [6단계] 예약 대기(순번 추가) 객체 생성
developowl Jan 12, 2025
2735245
feat(WaitingRequest / Response): [6단계] 예약 대기 요청, 응답 생성
developowl Jan 12, 2025
6eb5491
feat(WaitingRepository): [6단계] WaitingRepository 생성
developowl Jan 12, 2025
664156b
feat(WaitingService): [6단계] 예약대기 서비스 생성
developowl Jan 12, 2025
c66e0cd
feat(WaitingController): [6단계] 예약대기 컨트롤러 생성
developowl Jan 12, 2025
f030098
refactor(ReservationRepository): [6단계] exist 메서드 추가
developowl Jan 12, 2025
7198357
refactor(ReservationService): [6단계] 예약 대기 목록 조회 추가(내 예약 목록 조회 시)
developowl Jan 12, 2025
14c6c64
refactor(ReservationController): [6단계] 컨트롤러 리팩토링
developowl Jan 12, 2025
d0c428b
EOL, 미사용 코드, 주석 정리
developowl Jan 12, 2025
77f696e
refactor(ReservationController, Service): 컨트롤러 내 서비스 로직 이동
developowl Jan 13, 2025
8678ade
refactor(AuthService): checkInvalidLogin을 void 함수로 변경
developowl Jan 13, 2025
302a34e
refactor(AuthService): verifyToken메서드를 void형으로 변경
developowl Jan 13, 2025
883e77e
refactor(MissionStep): [7단계] 칠단계 테스트 코드 추가
developowl Jan 14, 2025
771bf97
fix conflicts
developowl Jan 14, 2025
4a9786c
refactor(MissionStepTest): [7단계] 칠단계 테스트 코드 추가
developowl Jan 14, 2025
764196a
refactor: [7단계] 반영
developowl Jan 15, 2025
01d4875
refactor: 로그인 로직 리팩토링
developowl Jan 15, 2025
4554a5d
refactor(MissionStepTest): [8단계] 팔단계 테스트 코드 추가
developowl Jan 15, 2025
f434739
refactor(properties): [8단계] environment 분리(default, prod, test)
developowl Jan 15, 2025
2966054
feat(DataLoader): [8단계] schema.sql -> DataLoader 형식으로 변경
developowl Jan 15, 2025
631456a
refactor(JwtAuthManager): bean 충돌 해결
developowl Jan 15, 2025
abdef60
refactor: 기존 오류 및 로직 리팩토링
developowl Jan 17, 2025
869a52e
refactor: 미사용 코드 및 주석 삭제
developowl Jan 22, 2025
bc3e5f2
conflict fix
developowl Jan 23, 2025
86fed55
refactor(build.gradle): jar 블록 추가
developowl Feb 3, 2025
bcd7d76
refactor: prod.properties 수정
developowl Feb 3, 2025
9906c22
미사용 코드 삭제
developowl Feb 3, 2025
7db3c6a
refactor: DTO -> record 형으로 변경. ReservationRequest, WaitingRequest는 s…
developowl Feb 11, 2025
80bbdc7
refactor: 패키지 분리에 따른 클래스 이동 및 미사용 클래스 삭제(move: JwtAuthConfig, delete:…
developowl Feb 11, 2025
b3560ce
refactor: 미사용 클래스 삭제(AuthorizationExtractor)
developowl Feb 11, 2025
181153b
docs(README.md): 패키지 구분 및 역할 명시
developowl Feb 12, 2025
a8290d8
refactor(TestDataLoader): test 패키지로 이동
developowl Feb 14, 2025
701263e
refactor: ArgumentResolver에서 멤버 조회 후 id값만 갖는 객체를 반환하게끔 리팩토링
developowl Feb 14, 2025
5957d4e
refactor: 의존성이 한방향으로 흐르게끔 코드 수정
developowl Feb 17, 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
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
47 changes: 47 additions & 0 deletions src/main/java/auth/AdminAccessInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package auth;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import roomescape.exception.AuthorizationException;

import java.util.Arrays;

@Component
public class AdminAccessInterceptor implements HandlerInterceptor {
private final JwtAuthManager jwtAuthManager;

public AdminAccessInterceptor(JwtAuthManager jwtAuthManager) {
this.jwtAuthManager = jwtAuthManager;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = extractTokenFromCookies(request.getCookies());
jwtAuthManager.validateToken(token);

String role = jwtAuthManager.getRole(token);

if (!"ADMIN".equals(role)) {
response.setStatus(401);
response.getWriter().write("권한이 없습니다.");
return false;
}

return true;
}

private String extractTokenFromCookies(Cookie[] cookies) {
if (cookies == null) {
throw new AuthorizationException("쿠키가 존재하지 않습니다.");
}

return Arrays.stream(cookies)
.filter(cookie -> "token".equals(cookie.getName()))
.findFirst()
.map(Cookie::getValue)
.orElseThrow(() -> new AuthorizationException("토큰 쿠키가 없습니다."));
}
}
94 changes: 94 additions & 0 deletions src/main/java/auth/JwtAuthManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package auth;

import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import roomescape.domain.member.Member;
import roomescape.domain.member.MemberRepository;
import roomescape.exception.AuthorizationException;

import java.util.Date;

public class JwtAuthManager {

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

@Value("${roomescape.auth.jwt.expire-length}")
private long validityInMilliseconds;

private final MemberRepository memberRepository;

public JwtAuthManager(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}

public String createToken(String email, String password) {
Member member = memberRepository.findByEmailAndPassword(email, password)
.orElseThrow(() -> new AuthorizationException("유효한 이메일이 아닙니다."));

Long memberId = member.getId();
String role = member.getRole();

Claims claims = Jwts.claims().setSubject(String.valueOf(memberId));
claims.put("role", role);

Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);

return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}

public Long getId(String token) {
JwtParser parser = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build();

Claims claims = parser.parseClaimsJws(token).getBody();
return Long.parseLong(claims.getSubject());
}

public String getRole(String token) {
JwtParser parser = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build();

Claims claims = parser.parseClaimsJws(token).getBody();
return claims.get("role", String.class);
}

public void validateToken(String token) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);

if (claims.getBody().getExpiration().before(new Date())) {

throw new IllegalArgumentException("토큰이 만료되었습니다.");
}
} catch (JwtException | IllegalArgumentException e) {
throw new IllegalArgumentException("유효하지 않은 토큰입니다.", e);
}
}

// public String getName(String token) {
// JwtParser parser = Jwts.parserBuilder()
// .setSigningKey(secretKey)
// .build();
//
// Claims claims = parser.parseClaimsJws(token).getBody();
// return claims.get("name", String.class);
// }
//
// public String getEmail(String token) {
// JwtParser parser = Jwts.parserBuilder()
// .setSigningKey(secretKey)
// .build();
//
// Claims claims = parser.parseClaimsJws(token).getBody();
// return claims.get("email", String.class);
// }
}
67 changes: 67 additions & 0 deletions src/main/java/auth/LoginMemberArgumentResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package auth;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
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;
import roomescape.domain.member.MemberRepository;
import roomescape.exception.AuthorizationException;
import roomescape.domain.member.Member;

import java.util.Arrays;

public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
private final JwtAuthManager jwtAuthManager;
private final MemberRepository memberRepository;

public LoginMemberArgumentResolver(JwtAuthManager jwtAuthManager, MemberRepository memberRepository) {
this.jwtAuthManager = jwtAuthManager;
this.memberRepository = memberRepository;
}

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(Member.class);
}

@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();

String token = extractTokenFromCookies(request.getCookies());
jwtAuthManager.validateToken(token);

Long id = jwtAuthManager.getId(token);

Member member = memberRepository.findById(id)
.orElseThrow(() -> new AuthorizationException("Member not found"));

String name = member.getName();

String email = member.getEmail();

String password = member.getPassword();

String role = jwtAuthManager.getRole(token);

return new Member(id, name, email, password, role);
}

private String extractTokenFromCookies(Cookie[] cookies) {
if (cookies == null) {
throw new AuthorizationException("쿠키가 존재하지 않습니다.");
}

return Arrays.stream(cookies)
.filter(cookie -> "token".equals(cookie.getName()))
.findFirst()
.map(Cookie::getValue)
.orElseThrow(() -> new AuthorizationException("토큰 쿠키가 없습니다."));
}
}
28 changes: 28 additions & 0 deletions src/main/java/roomescape/DataLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package roomescape;

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

@Profile("default") // 배포 환경 -> "prod", 로컬 환경 -> "default"
@Component
public class DataLoader implements CommandLineRunner {
private final MemberRepository memberRepository;

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

@Override
public void run(String... args) throws Exception {
if (memberRepository.count() == 0) {
Member admin = new Member("어드민", "[email protected]", "password", "ADMIN");
Member brown = new Member("브라운", "[email protected]", "password", "USER");
memberRepository.save(admin);
memberRepository.save(brown);
System.out.println("초기 사용자 정보가 등록되었습니다.");
}
}
}
57 changes: 57 additions & 0 deletions src/main/java/roomescape/TestDataLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package roomescape;

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

@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(MemberRepository memberRepository, ThemeRepository themeRepository,
TimeRepository timeRepository, ReservationRepository reservationRepository) {
this.memberRepository = memberRepository;
this.themeRepository = themeRepository;
this.timeRepository = timeRepository;
this.reservationRepository = reservationRepository;
}

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

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

Time time1 = new Time("10:00");
Time time2 = new Time("12:00");
timeRepository.save(time1);
timeRepository.save(time2);

Reservation reservation1 = new Reservation("어드민", "2024-03-01", time1, theme1, admin);
Reservation reservation2 = new Reservation("브라운", "2024-03-01", time2, theme2, brown);
reservationRepository.save(reservation1);
reservationRepository.save(reservation2);

System.out.println("테스트 데이터가 초기화되었습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package roomescape.authentication;

import jakarta.servlet.http.HttpServletRequest;

public interface AuthorizationExtractor<T> {
String AUTHORIZATION = "Authorization";

T extract(HttpServletRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package roomescape.authentication;

import jakarta.servlet.http.HttpServletRequest;

import java.util.Enumeration;

public class BearerAuthorizationExtractor implements AuthorizationExtractor<String> {
private static final String BEARER_TYPE = "Bearer";
private static final String ACCESS_TOKEN_TYPE = BearerAuthorizationExtractor.class.getSimpleName() + ".ACCESS_TOKEN_TYPE";

@Override
public String extract(HttpServletRequest request) {
Enumeration<String> headers = request.getHeaders(AUTHORIZATION);
while (headers.hasMoreElements()) {
String value = headers.nextElement();
if ((value.toLowerCase().startsWith(BEARER_TYPE.toLowerCase()))) {
String authHeaderValue = value.substring(BEARER_TYPE.length()).trim();
request.setAttribute(ACCESS_TOKEN_TYPE, value.substring(0, BEARER_TYPE.length()).trim());
int commaIndex = authHeaderValue.indexOf(',');
if (commaIndex > 0) {
authHeaderValue = authHeaderValue.substring(0, commaIndex);
}
return authHeaderValue;
}
}
return null;
}
}
22 changes: 22 additions & 0 deletions src/main/java/roomescape/config/JwtAuthConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package roomescape.config;

import auth.JwtAuthManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import roomescape.domain.member.MemberRepository;

@Configuration
@ComponentScan(basePackages = {"roomescape", "auth"})
public class JwtAuthConfig {
private final MemberRepository memberRepository;

public JwtAuthConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}

@Bean
public JwtAuthManager jwtAuthManager() {
return new JwtAuthManager(memberRepository);
}
}
Loading