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] (배포) 신혜빈 미션 제출합니다. #113

Open
wants to merge 89 commits into
base: shin378378
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
e233cc2
feat : 로그인 기능 추가
shin378378 Dec 25, 2024
b043419
feat : 1단계 통과
shin378378 Dec 25, 2024
f7a3a4b
refactor : 로그인 리팩토링
shin378378 Dec 25, 2024
7a17aa9
feat : 이름없이 회원정보 조회하기
shin378378 Dec 25, 2024
aa8ee73
refactor : reservationController 주소 리팩토링하기
shin378378 Dec 25, 2024
9cff672
feat : 권한이 있는 자만 접근하도록 설정
shin378378 Dec 25, 2024
6be6af3
feat : 권한 확인 리팩토링
shin378378 Dec 25, 2024
9f11631
refactor : 안 쓰는 파일 지우기
shin378378 Dec 25, 2024
56723c7
refactor : 안 쓰는 파일 지우기
shin378378 Jan 1, 2025
9db83a8
refactor : 안 쓰는 예외처리 코드 지우기
shin378378 Jan 7, 2025
3cb4153
refactor : key를 지역변수로 바꾸기
shin378378 Jan 7, 2025
7fbcc72
refactor : jwtService로 userId 찾는 부분 옮기기
shin378378 Jan 7, 2025
8b84763
refactor : 안 쓰는 어노테이션 삭제
shin378378 Jan 7, 2025
c57fbfb
refactor : @Autowired 대신 생성자로 주입해주기
shin378378 Jan 7, 2025
0dbb542
refactor : 코드 indent depth 줄이기
shin378378 Jan 7, 2025
903343e
refactor : boolean대신 에러코드 반환하기
shin378378 Jan 7, 2025
fa6d70c
refactor : try-catch를 ExceptionHandler로 처리하기
shin378378 Jan 7, 2025
787dd03
refactor : 커스텀 어노테이션 만들기
shin378378 Jan 7, 2025
6591dba
refactor : loginController의 멤버 찾는 코드를 service계층으로 옮기기
shin378378 Jan 7, 2025
d1d0e04
refactor : LoginController 필요없는 의존성 제거하기
shin378378 Jan 7, 2025
7abea53
refactor : 멤버찾는 코드 중복해결
shin378378 Jan 7, 2025
2ce4d0d
feat : gradle에 의존성 추가하기
shin378378 Jan 8, 2025
62ebd1c
feat : 엔티티 설정하기
shin378378 Jan 8, 2025
c4707f9
feat : test 추가하기
shin378378 Jan 8, 2025
41881e7
feat : @repository 어노테이션 추가하기
shin378378 Jan 8, 2025
8e8c37b
feat : memberRepository 만들기
shin378378 Jan 8, 2025
552ebaf
feat : themeRepository 만들기
shin378378 Jan 8, 2025
cde7be6
feat : reservationRepository 만들기
shin378378 Jan 8, 2025
a8c2c8c
feat : themeRepository 만들기
shin378378 Jan 8, 2025
1672171
refactor : 스키마 삭제
shin378378 Jan 8, 2025
19c1290
feat : jwt에서 memberDao 삭제
shin378378 Jan 8, 2025
60751e7
feat : login에서 dao삭제
shin378378 Jan 8, 2025
b2f3fea
feat : timeDao 삭제
shin378378 Jan 8, 2025
9feb8f4
feat : 주석처리
shin378378 Jan 8, 2025
0676f86
feat : Time 도메인을 JPA로
shin378378 Jan 8, 2025
efd3428
feat : Theme 도메인을 JPA로
shin378378 Jan 8, 2025
754949c
feat : Reservation 도메인을 JPA로
shin378378 Jan 8, 2025
78a0613
feat : member 도메인을 JPA로
shin378378 Jan 8, 2025
0b9afee
feat : 4단계 test 코드 추가
shin378378 Jan 8, 2025
fdb3a20
feat : sql 데이터 수정
shin378378 Jan 8, 2025
880c550
feat : sql 데이터 수정
shin378378 Jan 8, 2025
23e3a4d
feat : 5단계 통과
shin378378 Jan 8, 2025
c5fe67f
feat : 예약초반에 이름 설정해버리기
shin378378 Jan 8, 2025
ec96693
refactor : 정렬하기
shin378378 Jan 8, 2025
0c0a96f
feat : 6단계 테스트 작성
shin378378 Jan 8, 2025
611bcd0
feat : 예약 변경하기
shin378378 Jan 8, 2025
1868459
feat : 예약 대기 추가하기
shin378378 Jan 8, 2025
d416182
feat : 6단계 통과
shin378378 Jan 8, 2025
842d0ed
refactor : 안 쓰는 코드 삭제하기
shin378378 Jan 9, 2025
e5688a0
refactor : 안 쓰는 setter 삭제하기
shin378378 Jan 9, 2025
a87cfc5
feat : @Repository어노테이션 붙이기
shin378378 Jan 9, 2025
37d5690
feat : int 타입으로 변경하기
shin378378 Jan 9, 2025
c09cf97
feat : 다시 Long 타입으로 변경하기
shin378378 Jan 9, 2025
9e59f50
feat : int 타입도 받을 수 있게 변경
shin378378 Jan 9, 2025
ed03913
feat : 7단계 초기설정
shin378378 Jan 13, 2025
ee7409f
feat : cookieUtil 위치 바꾸기
shin378378 Jan 14, 2025
68dd573
feat : 미션 7 사전작업
shin378378 Jan 14, 2025
230c9c3
feat : test2 변경
shin378378 Jan 14, 2025
ce30c3e
feat : 중간 점검
shin378378 Jan 14, 2025
f05d599
feat : 7단계 통과
shin378378 Jan 14, 2025
4eb2fab
feat : 8단계 통과
shin378378 Jan 14, 2025
afb1fdb
feat : sql 삭제하기
shin378378 Jan 15, 2025
702d228
feat : swagger 만들기
shin378378 Jan 15, 2025
35a8c29
feat : test환경 설정하기
shin378378 Jan 15, 2025
b84f83d
feat : 안 쓰는 의존성 없애기
shin378378 Jan 15, 2025
3c2f7c4
feat : enum 만들기
shin378378 Jan 17, 2025
c6babe6
test : 안 쓰는 거 지우기
shin378378 Jan 20, 2025
280fc87
feat : enum 만들기
shin378378 Jan 22, 2025
298e3d7
feat : enum으로 상수 대체하기
shin378378 Jan 22, 2025
a4df411
feat : role을 enum으로 대체하기
shin378378 Jan 22, 2025
f2175e6
feat : 안 쓰는 코드 삭제하기
shin378378 Jan 22, 2025
f494ac1
feat : 빠진 코드 작성하기
shin378378 Jan 25, 2025
18b5cfd
feat : String을 LocalTime으로 바꾸기
shin378378 Jan 25, 2025
f8f1e8d
feat : String을 LocalDate으로 바꾸기
shin378378 Jan 25, 2025
fd35d9f
feat : String을 LocalDate으로 바꾸기 - 오류잡기
shin378378 Jan 25, 2025
dd89d39
feat : time 오류 바로잡기
shin378378 Jan 25, 2025
e3ba85e
feat : waiting의 fetch모드 설정하기
shin378378 Jan 25, 2025
0e8ba26
feat : N+1문제 없애기
shin378378 Jan 25, 2025
f40db21
feat : JPA 조합 제약조건 설정하기
shin378378 Jan 26, 2025
fbf852f
feat : /admin 하위페이지도 접근 못 하게 하기
shin378378 Jan 26, 2025
89f1cf7
refactor : 코드 리팩토링 하기
shin378378 Jan 22, 2025
0eda78e
feat : @ConfigurationProperties 사용하기
shin378378 Feb 3, 2025
a55c84d
feat : jwt 패키지의 빈들을 다 수동등록
shin378378 Feb 3, 2025
a35f97c
feat : jwt 패키지의 빈들을 다 자동등록
shin378378 Feb 3, 2025
908fcaa
feat : test데이터를 test 패키지로 빼기
shin378378 Feb 3, 2025
6aa6fe4
feat : DB접근 없애기
shin378378 Feb 3, 2025
37fa9cb
refactor : 코드 정리하기
shin378378 Feb 3, 2025
924562d
Merge branch 'shin3783783' into shin3783784
shin378378 Feb 3, 2025
0b02227
feat : admin페이지 로그아웃 시 오류 잡기
shin378378 Feb 7, 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
31 changes: 31 additions & 0 deletions .run/MissionStepTest.삼단계.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="MissionStepTest.삼단계" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="env">
<map>
<entry key="JWT_SECRET_KEY" value="bOjgcSvj4CvncQT6+XFS7IJSdTCvsSdIjLaVBAyKG6g=" />
</map>
</option>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":test" />
<option value="--tests" />
<option value="&quot;roomescape.MissionStepTest.삼단계&quot;" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>true</RunAsTest>
<method v="2" />
</configuration>
</component>
7 changes: 6 additions & 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 All @@ -26,7 +26,12 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'

implementation 'com.h2database:h2'
runtimeOnly 'com.h2database:h2'

implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
implementation 'jakarta.servlet:jakarta.servlet-api:6.0.0'

}

test {
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/jwt/JwtProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package jwt;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {

private final String secret;
private final long EXPIRATION_TIME_MILLIS = 1000 * 60 * 60 * 24;

public JwtProperties(String secret) {
this.secret = secret;
}

public String getSecret() {
return secret;
}

public long getEXPIRATION_TIME_MILLIS() {
return EXPIRATION_TIME_MILLIS;
}
}

32 changes: 32 additions & 0 deletions src/main/java/jwt/JwtService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package jwt;

import io.jsonwebtoken.Claims;
import org.springframework.stereotype.Service;
import roomescape.member.Member;
import roomescape.member.MemberRepository;

@Service
public class JwtService {
private final JwtUtils jwtUtils;
private final MemberRepository memberRepository;

public JwtService(JwtUtils jwtUtils, MemberRepository memberRepository) {
this.jwtUtils = jwtUtils;
this.memberRepository = memberRepository;
}

public Long getUserIdFromToken(String token) {
Claims claims = jwtUtils.getClaimsFromToken(token);
try {
return Long.valueOf(claims.getSubject());
} catch (Exception e) {
throw new IllegalArgumentException("Claims에서 User ID를 추출할 수 없습니다.", e);
}
}

public Member getMemberFromToken(String token) {
Long userId = getUserIdFromToken(token);
return memberRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("토큰으로부터 유저를 찾을 수 없습니다."));
}
}
48 changes: 48 additions & 0 deletions src/main/java/jwt/JwtUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import roomescape.member.Role;

import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Date;

@Component
public class JwtUtils {
private final JwtProperties jwtProperties;

public JwtUtils(JwtProperties jwtProperties) {
this.jwtProperties = jwtProperties;
}

private Key generateKey() {
return new SecretKeySpec(jwtProperties.getSecret().getBytes(), SignatureAlgorithm.HS256.getJcaName());
}

public String generateToken(Long userId, Role role) {
Key key = generateKey();
return Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("role", role.name())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getEXPIRATION_TIME_MILLIS()))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}

public Claims getClaimsFromToken(String token) {
try {
Key key = generateKey();
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
throw new IllegalArgumentException("유효하지 않은 토큰입니다.", e);
}
}
}
19 changes: 19 additions & 0 deletions src/main/java/roomescape/ExceptionController.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
package roomescape;

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

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class ExceptionController {

@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Void> handleResponseStatusException(ResponseStatusException e) {
return ResponseEntity.status(e.getStatusCode()).build();
}

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, Object>> handleIllegalArgumentException(IllegalArgumentException e) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<Void> handleRuntimeException(Exception e) {
e.printStackTrace();
return ResponseEntity.badRequest().build();
}
}

5 changes: 5 additions & 0 deletions src/main/java/roomescape/RoomescapeApplication.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package roomescape;

import jwt.JwtProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@EnableConfigurationProperties(JwtProperties.class)
@ComponentScan(basePackages = {"roomescape", "jwt"})
public class RoomescapeApplication {
public static void main(String[] args) {
SpringApplication.run(RoomescapeApplication.class, args);
Expand Down
51 changes: 51 additions & 0 deletions src/main/java/roomescape/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package roomescape;


import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@OpenAPIDefinition(
info = @Info(
title = "roomescape API 명세서",
description = "roomescape 서비스 API 명세서",
version = "v1"
)
)

@Configuration
public class SwaggerConfig {

@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
.components(new Components()
.addSecuritySchemes("bearerAuth",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOriginPattern("*");
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

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

import io.jsonwebtoken.Claims;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jwt.JwtUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.HandlerInterceptor;
import roomescape.member.Role;

import java.io.IOException;
import java.util.Arrays;

@Component
public class AdminInterceptor implements HandlerInterceptor {
private final JwtUtils jwtUtils;

public AdminInterceptor(JwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
String token = extractTokenFromCookies(request.getCookies());
if (token == null) {
response.sendRedirect("/login");
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "토큰을 찾을 수 없습니다.");
}

Claims claims = jwtUtils.getClaimsFromToken(token);
String role = claims.get("role", String.class);

if (!Role.ADMIN.name().equals(role)) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "권한이 없습니다.");
}
return true;
}

private String extractTokenFromCookies(Cookie[] cookies) {
if (cookies == null) {
return null;
}

return Arrays.stream(cookies)
.filter(cookie -> "token".equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
}
}
9 changes: 9 additions & 0 deletions src/main/java/roomescape/auth/Authentication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package roomescape.auth;

import java.lang.annotation.*;

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

import jwt.JwtService;
import org.springframework.beans.factory.annotation.Autowired;
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.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import roomescape.member.Member;

@Component
public class AuthenticationArgumentResolver implements HandlerMethodArgumentResolver {

@Autowired
private JwtService jwtService;

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

@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {

String token = CookieUtil.extractTokenFromHeader(webRequest)
.orElseThrow(() -> new IllegalArgumentException("토큰이 존재하지 않습니다. 로그인이 필요합니다."));

return jwtService.getMemberFromToken(token);
}
}
32 changes: 32 additions & 0 deletions src/main/java/roomescape/auth/CookieUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package roomescape.auth;

import jakarta.servlet.http.Cookie;
import org.springframework.web.context.request.NativeWebRequest;

import java.util.Arrays;
import java.util.Optional;

public class CookieUtil {

public static Optional<String> extractTokenFromCookies(Cookie[] cookies) {
if (cookies == null) {
return Optional.empty();
}
return Arrays.stream(cookies)
.filter(cookie -> "token".equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst();
}

public static Optional<String> extractTokenFromHeader(NativeWebRequest webRequest) {
String cookieHeader = webRequest.getHeader("Cookie");
if (cookieHeader == null) {
return Optional.empty();
}
return Arrays.stream(cookieHeader.split(";"))
.map(String::trim)
.filter(cookie -> cookie.startsWith("token="))
.map(cookie -> cookie.substring("token=".length()))
.findFirst();
}
}
Loading