From 602e91ee9eb4dd0ac4c3ffc10cedbab7cfec7dff Mon Sep 17 00:00:00 2001 From: gogo1414 Date: Mon, 24 Jun 2024 19:26:42 +0900 Subject: [PATCH 01/25] =?UTF-8?q?=EC=BF=A0=ED=82=A4=EB=A5=BC=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 ++++ .../roomescape/application/AuthService.java | 43 ++++++++++++++++ .../application/AuthorizationException.java | 14 ++++++ .../infrastructure/JwtTokenProvider.java | 49 +++++++++++++++++++ .../roomescape/member/MemberController.java | 44 ++++++++++++++++- .../java/roomescape/token/TokenRequest.java | 22 +++++++++ .../java/roomescape/token/TokenResponse.java | 16 ++++++ src/main/resources/application.properties | 3 ++ src/test/java/roomescape/MissionStepTest.java | 11 ++++- 9 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 README.md create mode 100644 src/main/java/roomescape/application/AuthService.java create mode 100644 src/main/java/roomescape/application/AuthorizationException.java create mode 100644 src/main/java/roomescape/infrastructure/JwtTokenProvider.java create mode 100644 src/main/java/roomescape/token/TokenRequest.java create mode 100644 src/main/java/roomescape/token/TokenResponse.java diff --git a/README.md b/README.md new file mode 100644 index 000000000..81813f040 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +## 요구사항 분석 +### 1단계 - 로그인 +- 로그인 기능을 구현하세요. +- 로그인 후 Cookie를 이용하여 사용자의 정보를 조회하는 API를 구현하세요. +#### 로그인 기능 +- 아래의 request와 response 요구사항에 따라 /login에 email, password 값을 body에 포함하세요. +- 응답에 Cookie에 "token"값으로 토큰이 포함되도록 하세요. +#### 인증 정보 조회 +- 상단바 우측 로그인 상태를 표현해주기 위해 사용자의 정보를 조회하는 API를 구현하세요. +- Cookie를 이용하여 로그인 사용자의 정보확인하세요. diff --git a/src/main/java/roomescape/application/AuthService.java b/src/main/java/roomescape/application/AuthService.java new file mode 100644 index 000000000..a64a56224 --- /dev/null +++ b/src/main/java/roomescape/application/AuthService.java @@ -0,0 +1,43 @@ +package roomescape.application; + +import java.lang.module.FindException; +import org.springframework.stereotype.Service; +import roomescape.infrastructure.JwtTokenProvider; +import roomescape.member.Member; +import roomescape.member.MemberDao; +import roomescape.member.MemberResponse; +import roomescape.token.TokenRequest; +import roomescape.token.TokenResponse; + +@Service +public class AuthService { + private JwtTokenProvider jwtTokenProvider; + private MemberDao memberDao; + + public AuthService(JwtTokenProvider jwtTokenProvider, MemberDao memberDao) { + this.jwtTokenProvider = jwtTokenProvider; + this.memberDao = memberDao; + } + + public Member checkInvalidLogin(String principal, String credentials) { + Member member = null; + try { + member = memberDao.findByEmailAndPassword(principal, credentials); + } catch(AuthorizationException e) { + e.printStackTrace(); + } + return member; + } + + public void findMemberByToken(String token) { + Long memberId = jwtTokenProvider.getPayload(token); + System.out.println("mmmmmmmmmmm test : " +memberId); +// return memberDao.findById(memberId); + } + + public TokenResponse createToken(TokenRequest tokenRequest) { + Member member = checkInvalidLogin(tokenRequest.getEmail(), tokenRequest.getPassword()); + String accessToken = jwtTokenProvider.createToken(member); + return new TokenResponse(accessToken); + } +} diff --git a/src/main/java/roomescape/application/AuthorizationException.java b/src/main/java/roomescape/application/AuthorizationException.java new file mode 100644 index 000000000..d6d3a86df --- /dev/null +++ b/src/main/java/roomescape/application/AuthorizationException.java @@ -0,0 +1,14 @@ +package roomescape.application; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.UNAUTHORIZED) +public class AuthorizationException extends RuntimeException { + public AuthorizationException() { + } + + public AuthorizationException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/infrastructure/JwtTokenProvider.java b/src/main/java/roomescape/infrastructure/JwtTokenProvider.java new file mode 100644 index 000000000..8e571570b --- /dev/null +++ b/src/main/java/roomescape/infrastructure/JwtTokenProvider.java @@ -0,0 +1,49 @@ +package roomescape.infrastructure; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; +import roomescape.member.Member; + +@Component +public class JwtTokenProvider { + @Value("${security.jwt.token.secret-key}") + private String secretKey; + @Value("${security.jwt.token.expire-length}") + private long validityInMilliseconds; + + public String createToken(Member member) { + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliseconds); + + return Jwts.builder() + .setSubject(member.getId().toString()) + .claim("name", member.getName()) + .claim("role", member.getRole()) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) + .compact(); + } + + public Long getPayload(String token) { + return Long.valueOf(Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) + .build() + .parseClaimsJws(token) + .getBody().getSubject()); + } + + public boolean validateToken(String token) { + try { + Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + + return !claims.getBody().getExpiration().before(new Date()); + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/member/MemberController.java b/src/main/java/roomescape/member/MemberController.java index 881ae5e0d..d4d8db56a 100644 --- a/src/main/java/roomescape/member/MemberController.java +++ b/src/main/java/roomescape/member/MemberController.java @@ -3,6 +3,8 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.apache.el.parser.Token; +import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -10,13 +12,18 @@ import org.springframework.web.bind.annotation.RestController; import java.net.URI; +import roomescape.application.AuthService; +import roomescape.token.TokenRequest; +import roomescape.token.TokenResponse; @RestController public class MemberController { private MemberService memberService; + private AuthService authService; - public MemberController(MemberService memberService) { + public MemberController(MemberService memberService, AuthService authService) { this.memberService = memberService; + this.authService = authService; } @PostMapping("/members") @@ -25,6 +32,31 @@ public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member); } + @PostMapping("/login") + public ResponseEntity login(@RequestBody TokenRequest request, HttpServletResponse response) { + TokenRequest tokenRequest = request; + TokenResponse tokenResponse = authService.createToken(tokenRequest); + + Cookie cookie = new Cookie("token", "eyJhbGciOiJIUzI1NiJ9.ey..."); + cookie.setHttpOnly(true); + cookie.setPath("/"); + response.addCookie(cookie); + + // HttpHeaders 객체 생성 +// HttpHeaders headers = new HttpHeaders(); +// headers.add("Set-Cookie", "token=" + tokenResponse.getAccessToken() + "; Path=/; HttpOnly"); + + return ResponseEntity.ok().build(); + } + + @GetMapping("/login/check") + public ResponseEntity checkLogin(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + String token = extractTokenFromCookie(cookies); + authService.findMemberByToken(token); + return ResponseEntity.ok().build(); + } + @PostMapping("/logout") public ResponseEntity logout(HttpServletResponse response) { Cookie cookie = new Cookie("token", ""); @@ -34,4 +66,14 @@ public ResponseEntity logout(HttpServletResponse response) { response.addCookie(cookie); return ResponseEntity.ok().build(); } + + private String extractTokenFromCookie(Cookie[] cookies) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals("token")) { + return cookie.getValue(); + } + } + + return ""; + } } diff --git a/src/main/java/roomescape/token/TokenRequest.java b/src/main/java/roomescape/token/TokenRequest.java new file mode 100644 index 000000000..7da498f93 --- /dev/null +++ b/src/main/java/roomescape/token/TokenRequest.java @@ -0,0 +1,22 @@ +package roomescape.token; + +public class TokenRequest { + private String email; + private String password; + + public TokenRequest() { + } + + public TokenRequest(String email, String password) { + this.email = email; + this.password = password; + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/token/TokenResponse.java b/src/main/java/roomescape/token/TokenResponse.java new file mode 100644 index 000000000..c325d3eba --- /dev/null +++ b/src/main/java/roomescape/token/TokenResponse.java @@ -0,0 +1,16 @@ +package roomescape.token; + +public class TokenResponse { + private String accessToken; + + public TokenResponse() { + } + + public TokenResponse(String accessToken) { + this.accessToken = accessToken; + } + + public String getAccessToken() { + return accessToken; + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a0f33bbab..6041e5c9b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,6 +3,9 @@ spring.h2.console.enabled=true spring.h2.console.path=/h2-console spring.datasource.url=jdbc:h2:mem:database +security.jwt.token.secret-key=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJzdWIiOiIiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9ih1aovtQShabQ7l0cINw4k1fagApg3qLWiB8Kt59Lno +security.jwt.token.expire-length=3600000 + #spring.jpa.show-sql=true #spring.jpa.properties.hibernate.format_sql=true #spring.jpa.ddl-auto=create-drop diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 6add784bd..02e484664 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -32,7 +32,16 @@ public class MissionStepTest { .extract(); String token = response.headers().get("Set-Cookie").getValue().split(";")[0].split("=")[1]; - assertThat(token).isNotBlank(); + + ExtractableResponse checkResponse = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .cookie("token", token) + .when().get("/login/check") + .then().log().all() + .statusCode(200) + .extract(); + +// assertThat(checkResponse.body().jsonPath().getString("name")).isEqualTo("어드민"); } } \ No newline at end of file From 48d0d15c36c62124468a514b0ca9f272c77a2e26 Mon Sep 17 00:00:00 2001 From: gogo1414 Date: Mon, 24 Jun 2024 19:56:24 +0900 Subject: [PATCH 02/25] =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/application/AuthService.java | 5 ++--- .../java/roomescape/member/MemberController.java | 8 +++++--- src/main/java/roomescape/member/MemberDao.java | 13 +++++++++++++ src/main/java/roomescape/member/MemberResponse.java | 7 +++++++ src/main/resources/application.properties | 2 +- src/test/java/roomescape/MissionStepTest.java | 2 +- 6 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/main/java/roomescape/application/AuthService.java b/src/main/java/roomescape/application/AuthService.java index a64a56224..3444a8316 100644 --- a/src/main/java/roomescape/application/AuthService.java +++ b/src/main/java/roomescape/application/AuthService.java @@ -29,10 +29,9 @@ public Member checkInvalidLogin(String principal, String credentials) { return member; } - public void findMemberByToken(String token) { + public Member findMemberByToken(String token) { Long memberId = jwtTokenProvider.getPayload(token); - System.out.println("mmmmmmmmmmm test : " +memberId); -// return memberDao.findById(memberId); + return memberDao.findById(memberId); } public TokenResponse createToken(TokenRequest tokenRequest) { diff --git a/src/main/java/roomescape/member/MemberController.java b/src/main/java/roomescape/member/MemberController.java index d4d8db56a..b2033e3bb 100644 --- a/src/main/java/roomescape/member/MemberController.java +++ b/src/main/java/roomescape/member/MemberController.java @@ -37,7 +37,7 @@ public ResponseEntity login(@RequestBody TokenRequest request, HttpServletRespon TokenRequest tokenRequest = request; TokenResponse tokenResponse = authService.createToken(tokenRequest); - Cookie cookie = new Cookie("token", "eyJhbGciOiJIUzI1NiJ9.ey..."); + Cookie cookie = new Cookie("token", tokenResponse.getAccessToken()); cookie.setHttpOnly(true); cookie.setPath("/"); response.addCookie(cookie); @@ -53,8 +53,10 @@ public ResponseEntity login(@RequestBody TokenRequest request, HttpServletRespon public ResponseEntity checkLogin(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); String token = extractTokenFromCookie(cookies); - authService.findMemberByToken(token); - return ResponseEntity.ok().build(); + Member member = authService.findMemberByToken(token); + MemberResponse response = new MemberResponse(member.getName()); + return ResponseEntity.ok() + .body(response); } @PostMapping("/logout") diff --git a/src/main/java/roomescape/member/MemberDao.java b/src/main/java/roomescape/member/MemberDao.java index 81f77f4cd..114fd342d 100644 --- a/src/main/java/roomescape/member/MemberDao.java +++ b/src/main/java/roomescape/member/MemberDao.java @@ -52,4 +52,17 @@ public Member findByName(String name) { name ); } + + public Member findById(Long id) { + return jdbcTemplate.queryForObject( + "SELECT id, name, email, role FROM member WHERE id = ?", + (rs, rowNum) -> new Member( + rs.getLong("id"), + rs.getString("name"), + rs.getString("email"), + rs.getString("role") + ), + id + ); + } } diff --git a/src/main/java/roomescape/member/MemberResponse.java b/src/main/java/roomescape/member/MemberResponse.java index b9fa3b97a..fe6cec414 100644 --- a/src/main/java/roomescape/member/MemberResponse.java +++ b/src/main/java/roomescape/member/MemberResponse.java @@ -1,10 +1,17 @@ package roomescape.member; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) public class MemberResponse { private Long id; private String name; private String email; + public MemberResponse(String name) { + this.name = name; + } + public MemberResponse(Long id, String name, String email) { this.id = id; this.name = name; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6041e5c9b..2c0312b87 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,7 +3,7 @@ spring.h2.console.enabled=true spring.h2.console.path=/h2-console spring.datasource.url=jdbc:h2:mem:database -security.jwt.token.secret-key=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJzdWIiOiIiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9ih1aovtQShabQ7l0cINw4k1fagApg3qLWiB8Kt59Lno +security.jwt.token.secret-key=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.ih1aovtQShabQ7l0cINw4k1fagApg3qLWiB8Kt59Lno security.jwt.token.expire-length=3600000 #spring.jpa.show-sql=true diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 02e484664..765c287d7 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -42,6 +42,6 @@ public class MissionStepTest { .statusCode(200) .extract(); -// assertThat(checkResponse.body().jsonPath().getString("name")).isEqualTo("어드민"); + assertThat(checkResponse.body().jsonPath().getString("name")).isEqualTo("어드민"); } } \ No newline at end of file From 0d71c014b22a4164f7d1e06729dd8d574f4f0352 Mon Sep 17 00:00:00 2001 From: gogo1414 Date: Mon, 24 Jun 2024 22:14:46 +0900 Subject: [PATCH 03/25] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=84=B0=EB=A7=81=EC=9D=84=20=ED=86=B5=ED=95=B4=20?= =?UTF-8?q?=EC=98=88=EC=95=BD=20=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 ++++ src/main/java/roomescape/auth/AuthConfig.java | 21 +++++++ .../{application => auth}/AuthService.java | 21 ++++--- .../AuthorizationException.java | 2 +- ...wtTokenProvider.java => JwtTokenUtil.java} | 22 +++++++- .../LoginMemberArgumentResolver.java | 41 ++++++++++++++ .../java/roomescape/member/LoginMember.java | 31 +++++++++++ .../roomescape/member/MemberController.java | 22 ++------ .../java/roomescape/member/MemberService.java | 4 ++ .../reservation/ReservationController.java | 8 +-- .../reservation/ReservationRequest.java | 4 ++ .../reservation/ReservationService.java | 6 +- src/test/java/roomescape/MissionStepTest.java | 55 +++++++++++++++++++ 13 files changed, 210 insertions(+), 38 deletions(-) create mode 100644 src/main/java/roomescape/auth/AuthConfig.java rename src/main/java/roomescape/{application => auth}/AuthService.java (59%) rename src/main/java/roomescape/{application => auth}/AuthorizationException.java (91%) rename src/main/java/roomescape/infrastructure/{JwtTokenProvider.java => JwtTokenUtil.java} (66%) create mode 100644 src/main/java/roomescape/infrastructure/LoginMemberArgumentResolver.java create mode 100644 src/main/java/roomescape/member/LoginMember.java diff --git a/README.md b/README.md index 81813f040..0a755253d 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,14 @@ #### 인증 정보 조회 - 상단바 우측 로그인 상태를 표현해주기 위해 사용자의 정보를 조회하는 API를 구현하세요. - Cookie를 이용하여 로그인 사용자의 정보확인하세요. + +### 2단계 - 로그인 리팩터링 +- 사용자의 정보를 조회하는 로직을 리팩터링 합니다. +- 예약 생성 API 및 기능을 리팩터링 합니다. +#### 로그인 리팩터링 +- Cookie에 담긴 인증 정보를 이용해서 멤버 객체를 만드는 로직을 분리합니다. + - HandlerMethodArgumentResolver을 활용하면 회원정보를 객체를 컨트롤러 메서드에 주입할 수 있습니다. +#### 예약 생성 기능 변경 +- 예약 생성 시 ReservationReqeust의 name이 없는 경우 Cookie에 담긴 정보를 활용하도록 리팩터링 합니다. + - ReservationReqeust에 name값이 있으면 name으로 Member를 찾고 + - 없으며 로그인 정보를 활용해서 Member를 찾도록 수정합니다. diff --git a/src/main/java/roomescape/auth/AuthConfig.java b/src/main/java/roomescape/auth/AuthConfig.java new file mode 100644 index 000000000..ba517f16e --- /dev/null +++ b/src/main/java/roomescape/auth/AuthConfig.java @@ -0,0 +1,21 @@ +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.WebMvcConfigurer; +import roomescape.infrastructure.LoginMemberArgumentResolver; + +@Configuration +public class AuthConfig implements WebMvcConfigurer { + private final LoginMemberArgumentResolver loginMemberArgumentResolver; + + public AuthConfig(LoginMemberArgumentResolver loginMemberArgumentResolver) { + this.loginMemberArgumentResolver = loginMemberArgumentResolver; + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(loginMemberArgumentResolver); + } +} diff --git a/src/main/java/roomescape/application/AuthService.java b/src/main/java/roomescape/auth/AuthService.java similarity index 59% rename from src/main/java/roomescape/application/AuthService.java rename to src/main/java/roomescape/auth/AuthService.java index 3444a8316..e53d99f31 100644 --- a/src/main/java/roomescape/application/AuthService.java +++ b/src/main/java/roomescape/auth/AuthService.java @@ -1,21 +1,21 @@ -package roomescape.application; +package roomescape.auth; -import java.lang.module.FindException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.stereotype.Service; -import roomescape.infrastructure.JwtTokenProvider; +import roomescape.infrastructure.JwtTokenUtil; import roomescape.member.Member; import roomescape.member.MemberDao; -import roomescape.member.MemberResponse; import roomescape.token.TokenRequest; import roomescape.token.TokenResponse; @Service public class AuthService { - private JwtTokenProvider jwtTokenProvider; + private JwtTokenUtil jwtTokenUtil; private MemberDao memberDao; - public AuthService(JwtTokenProvider jwtTokenProvider, MemberDao memberDao) { - this.jwtTokenProvider = jwtTokenProvider; + public AuthService(JwtTokenUtil jwtTokenUtil, MemberDao memberDao) { + this.jwtTokenUtil = jwtTokenUtil; this.memberDao = memberDao; } @@ -29,14 +29,13 @@ public Member checkInvalidLogin(String principal, String credentials) { return member; } - public Member findMemberByToken(String token) { - Long memberId = jwtTokenProvider.getPayload(token); - return memberDao.findById(memberId); + public Long findMemberIdByToken(HttpServletRequest request) { + return jwtTokenUtil.getPayload(request); } public TokenResponse createToken(TokenRequest tokenRequest) { Member member = checkInvalidLogin(tokenRequest.getEmail(), tokenRequest.getPassword()); - String accessToken = jwtTokenProvider.createToken(member); + String accessToken = jwtTokenUtil.createToken(member); return new TokenResponse(accessToken); } } diff --git a/src/main/java/roomescape/application/AuthorizationException.java b/src/main/java/roomescape/auth/AuthorizationException.java similarity index 91% rename from src/main/java/roomescape/application/AuthorizationException.java rename to src/main/java/roomescape/auth/AuthorizationException.java index d6d3a86df..bb7efbb6e 100644 --- a/src/main/java/roomescape/application/AuthorizationException.java +++ b/src/main/java/roomescape/auth/AuthorizationException.java @@ -1,4 +1,4 @@ -package roomescape.application; +package roomescape.auth; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; diff --git a/src/main/java/roomescape/infrastructure/JwtTokenProvider.java b/src/main/java/roomescape/infrastructure/JwtTokenUtil.java similarity index 66% rename from src/main/java/roomescape/infrastructure/JwtTokenProvider.java rename to src/main/java/roomescape/infrastructure/JwtTokenUtil.java index 8e571570b..615d69acf 100644 --- a/src/main/java/roomescape/infrastructure/JwtTokenProvider.java +++ b/src/main/java/roomescape/infrastructure/JwtTokenUtil.java @@ -2,6 +2,8 @@ import io.jsonwebtoken.*; 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; @@ -9,7 +11,7 @@ import roomescape.member.Member; @Component -public class JwtTokenProvider { +public class JwtTokenUtil { @Value("${security.jwt.token.secret-key}") private String secretKey; @Value("${security.jwt.token.expire-length}") @@ -21,6 +23,7 @@ public String createToken(Member member) { return Jwts.builder() .setSubject(member.getId().toString()) + .claim("email", member.getEmail()) .claim("name", member.getName()) .claim("role", member.getRole()) .setIssuedAt(now) @@ -29,7 +32,8 @@ public String createToken(Member member) { .compact(); } - public Long getPayload(String token) { + public Long getPayload(HttpServletRequest request) { + String token = extractTokenFromCookie(request); return Long.valueOf(Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) .build() @@ -39,11 +43,23 @@ public Long getPayload(String token) { public boolean validateToken(String token) { try { - Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + Jws claims = Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token); return !claims.getBody().getExpiration().before(new Date()); } catch (JwtException | IllegalArgumentException e) { return false; } } + + private String extractTokenFromCookie(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + for (Cookie cookie : cookies) { + if (cookie.getName().equals("token")) { + return cookie.getValue(); + } + } + return ""; + } } \ No newline at end of file diff --git a/src/main/java/roomescape/infrastructure/LoginMemberArgumentResolver.java b/src/main/java/roomescape/infrastructure/LoginMemberArgumentResolver.java new file mode 100644 index 000000000..649399d22 --- /dev/null +++ b/src/main/java/roomescape/infrastructure/LoginMemberArgumentResolver.java @@ -0,0 +1,41 @@ +package roomescape.infrastructure; + +import jakarta.servlet.http.HttpServletRequest; +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.LoginMember; +import roomescape.member.Member; +import roomescape.member.MemberService; + +@Component +public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + private MemberService memberService; + private JwtTokenUtil jwtTokenUtil; + + public LoginMemberArgumentResolver(MemberService memberService, JwtTokenUtil jwtTokenUtil) { + this.memberService = memberService; + this.jwtTokenUtil = jwtTokenUtil; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(LoginMember.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest httpServletRequest = (HttpServletRequest) webRequest.getNativeRequest(); + + Long memberId = jwtTokenUtil.getPayload(httpServletRequest); + Member member = memberService.findMemberById(memberId); + + return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole()); + } +} diff --git a/src/main/java/roomescape/member/LoginMember.java b/src/main/java/roomescape/member/LoginMember.java new file mode 100644 index 000000000..0acfe7a79 --- /dev/null +++ b/src/main/java/roomescape/member/LoginMember.java @@ -0,0 +1,31 @@ +package roomescape.member; + +public class LoginMember { + private Long id; + private String name; + private String email; + private String role; + + public LoginMember(Long id, String name, String email, String role) { + this.id = id; + this.name = name; + this.email = email; + this.role = role; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + public String getRole() { + return role; + } +} diff --git a/src/main/java/roomescape/member/MemberController.java b/src/main/java/roomescape/member/MemberController.java index b2033e3bb..b9d1aff27 100644 --- a/src/main/java/roomescape/member/MemberController.java +++ b/src/main/java/roomescape/member/MemberController.java @@ -3,8 +3,6 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.apache.el.parser.Token; -import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -12,7 +10,7 @@ import org.springframework.web.bind.annotation.RestController; import java.net.URI; -import roomescape.application.AuthService; +import roomescape.auth.AuthService; import roomescape.token.TokenRequest; import roomescape.token.TokenResponse; @@ -34,8 +32,7 @@ public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { @PostMapping("/login") public ResponseEntity login(@RequestBody TokenRequest request, HttpServletResponse response) { - TokenRequest tokenRequest = request; - TokenResponse tokenResponse = authService.createToken(tokenRequest); + TokenResponse tokenResponse = authService.createToken(request); Cookie cookie = new Cookie("token", tokenResponse.getAccessToken()); cookie.setHttpOnly(true); @@ -51,9 +48,8 @@ public ResponseEntity login(@RequestBody TokenRequest request, HttpServletRespon @GetMapping("/login/check") public ResponseEntity checkLogin(HttpServletRequest request) { - Cookie[] cookies = request.getCookies(); - String token = extractTokenFromCookie(cookies); - Member member = authService.findMemberByToken(token); + Long memberId = authService.findMemberIdByToken(request); + Member member = memberService.findMemberById(memberId); MemberResponse response = new MemberResponse(member.getName()); return ResponseEntity.ok() .body(response); @@ -68,14 +64,4 @@ public ResponseEntity logout(HttpServletResponse response) { response.addCookie(cookie); return ResponseEntity.ok().build(); } - - private String extractTokenFromCookie(Cookie[] cookies) { - for (Cookie cookie : cookies) { - if (cookie.getName().equals("token")) { - return cookie.getValue(); - } - } - - return ""; - } } diff --git a/src/main/java/roomescape/member/MemberService.java b/src/main/java/roomescape/member/MemberService.java index ccaa8cba5..15735b3a3 100644 --- a/src/main/java/roomescape/member/MemberService.java +++ b/src/main/java/roomescape/member/MemberService.java @@ -14,4 +14,8 @@ public MemberResponse createMember(MemberRequest memberRequest) { Member member = memberDao.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), "USER")); return new MemberResponse(member.getId(), member.getName(), member.getEmail()); } + + public Member findMemberById(Long memberId) { + return memberDao.findById(memberId); + } } diff --git a/src/main/java/roomescape/reservation/ReservationController.java b/src/main/java/roomescape/reservation/ReservationController.java index b3bef3990..df2a04f81 100644 --- a/src/main/java/roomescape/reservation/ReservationController.java +++ b/src/main/java/roomescape/reservation/ReservationController.java @@ -10,6 +10,7 @@ import java.net.URI; import java.util.List; +import roomescape.member.LoginMember; @RestController public class ReservationController { @@ -26,14 +27,13 @@ public List list() { } @PostMapping("/reservations") - public ResponseEntity create(@RequestBody ReservationRequest reservationRequest) { - if (reservationRequest.getName() == null - || reservationRequest.getDate() == null + public ResponseEntity create(@RequestBody ReservationRequest reservationRequest, LoginMember loginMember) { + if (reservationRequest.getDate() == null || reservationRequest.getTheme() == null || reservationRequest.getTime() == null) { return ResponseEntity.badRequest().build(); } - ReservationResponse reservation = reservationService.save(reservationRequest); + ReservationResponse reservation = reservationService.save(reservationRequest, loginMember); return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation); } diff --git a/src/main/java/roomescape/reservation/ReservationRequest.java b/src/main/java/roomescape/reservation/ReservationRequest.java index 19f441246..a07a8483e 100644 --- a/src/main/java/roomescape/reservation/ReservationRequest.java +++ b/src/main/java/roomescape/reservation/ReservationRequest.java @@ -21,4 +21,8 @@ public Long getTheme() { public Long getTime() { return time; } + + public void setName(String name) { + this.name = name; + } } diff --git a/src/main/java/roomescape/reservation/ReservationService.java b/src/main/java/roomescape/reservation/ReservationService.java index bd3313328..d2241b7c1 100644 --- a/src/main/java/roomescape/reservation/ReservationService.java +++ b/src/main/java/roomescape/reservation/ReservationService.java @@ -3,6 +3,7 @@ import org.springframework.stereotype.Service; import java.util.List; +import roomescape.member.LoginMember; @Service public class ReservationService { @@ -12,7 +13,10 @@ public ReservationService(ReservationDao reservationDao) { this.reservationDao = reservationDao; } - public ReservationResponse save(ReservationRequest reservationRequest) { + public ReservationResponse save(ReservationRequest reservationRequest, LoginMember loginMember) { + if(reservationRequest.getName() == null){ + reservationRequest.setName(loginMember.getName()); + } Reservation reservation = reservationDao.save(reservationRequest); return new ReservationResponse(reservation.getId(), reservationRequest.getName(), reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue()); diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 765c287d7..64cc71b92 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -1,21 +1,41 @@ package roomescape; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; import io.restassured.RestAssured; import io.restassured.http.ContentType; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; +import java.util.Date; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; import java.util.HashMap; import java.util.Map; +import roomescape.auth.AuthService; +import roomescape.member.Member; +import roomescape.member.MemberDao; +import roomescape.member.MemberService; +import roomescape.reservation.ReservationResponse; +import roomescape.token.TokenRequest; +import roomescape.token.TokenResponse; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) public class MissionStepTest { + @Value("${security.jwt.token.secret-key}") + private String secretKey; + @Value("${security.jwt.token.expire-length}") + private long validityInMilliseconds; + + @Autowired + private AuthService authService; + @Test void 일단계() { @@ -44,4 +64,39 @@ public class MissionStepTest { assertThat(checkResponse.body().jsonPath().getString("name")).isEqualTo("어드민"); } + + @Test + void 이단계() { + TokenRequest tokenRequest = new TokenRequest("admin@email.com", "password"); + String token = authService.createToken(tokenRequest).getAccessToken(); + + Map params = new HashMap<>(); + params.put("date", "2024-03-01"); + params.put("time", "1"); + params.put("theme", "1"); + + ExtractableResponse response = RestAssured.given().log().all() + .body(params) + .cookie("token", token) + .contentType(ContentType.JSON) + .post("/reservations") + .then().log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(201); + assertThat(response.as(ReservationResponse.class).getName()).isEqualTo("어드민"); + + params.put("name", "브라운"); + + ExtractableResponse adminResponse = RestAssured.given().log().all() + .body(params) + .cookie("token", token) + .contentType(ContentType.JSON) + .post("/reservations") + .then().log().all() + .extract(); + + assertThat(adminResponse.statusCode()).isEqualTo(201); + assertThat(adminResponse.as(ReservationResponse.class).getName()).isEqualTo("브라운"); + } } \ No newline at end of file From 6c424372cb0e61b54b5cc97780e40efebccf9aca Mon Sep 17 00:00:00 2001 From: gogo1414 Date: Mon, 24 Jun 2024 23:46:07 +0900 Subject: [PATCH 04/25] =?UTF-8?q?3=EB=8B=A8=EA=B3=84=20HandlerInterceptor?= =?UTF-8?q?=EB=A5=BC=20=ED=99=9C=EC=9A=A9=ED=95=9C=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +++ .../roomescape/auth/AuthAdminInteceptor.java | 31 +++++++++++++++++++ src/main/java/roomescape/auth/AuthConfig.java | 10 +++++- src/test/java/roomescape/MissionStepTest.java | 24 ++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/main/java/roomescape/auth/AuthAdminInteceptor.java diff --git a/README.md b/README.md index 0a755253d..cfd49acbc 100644 --- a/README.md +++ b/README.md @@ -19,3 +19,7 @@ - 예약 생성 시 ReservationReqeust의 name이 없는 경우 Cookie에 담긴 정보를 활용하도록 리팩터링 합니다. - ReservationReqeust에 name값이 있으면 name으로 Member를 찾고 - 없으며 로그인 정보를 활용해서 Member를 찾도록 수정합니다. + +### 3단계 - 관리자 기능 +- 어드민 페이지 진입은 admin권한이 있는 사람만 할 수 있도록 제한하세요. +- HandlerInterceptor를 활용하여 권한이 없는 경우 401코드를 응답하세요. \ No newline at end of file diff --git a/src/main/java/roomescape/auth/AuthAdminInteceptor.java b/src/main/java/roomescape/auth/AuthAdminInteceptor.java new file mode 100644 index 000000000..a47eb35b6 --- /dev/null +++ b/src/main/java/roomescape/auth/AuthAdminInteceptor.java @@ -0,0 +1,31 @@ +package roomescape.auth; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import roomescape.infrastructure.JwtTokenUtil; +import roomescape.member.Member; +import roomescape.member.MemberService; + +@Component +public class AuthAdminInteceptor implements HandlerInterceptor { + private MemberService memberService; + private JwtTokenUtil jwtTokenUtil; + + public AuthAdminInteceptor(MemberService memberService, JwtTokenUtil jwtTokenUtil) { + this.memberService = memberService; + this.jwtTokenUtil = jwtTokenUtil; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + Long memberId = jwtTokenUtil.getPayload(request); + Member member = memberService.findMemberById(memberId); + if (member == null || !member.getRole().equals("ADMIN")) { + response.setStatus(401); + return false; + } + return true; + } +} diff --git a/src/main/java/roomescape/auth/AuthConfig.java b/src/main/java/roomescape/auth/AuthConfig.java index ba517f16e..c377c3de1 100644 --- a/src/main/java/roomescape/auth/AuthConfig.java +++ b/src/main/java/roomescape/auth/AuthConfig.java @@ -3,19 +3,27 @@ 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; import roomescape.infrastructure.LoginMemberArgumentResolver; @Configuration public class AuthConfig implements WebMvcConfigurer { private final LoginMemberArgumentResolver loginMemberArgumentResolver; + private final AuthAdminInteceptor authAdminInteceptor; - public AuthConfig(LoginMemberArgumentResolver loginMemberArgumentResolver) { + public AuthConfig(LoginMemberArgumentResolver loginMemberArgumentResolver, AuthAdminInteceptor authAdminInteceptor) { this.loginMemberArgumentResolver = loginMemberArgumentResolver; + this.authAdminInteceptor = authAdminInteceptor; } @Override public void addArgumentResolvers(List resolvers) { resolvers.add(loginMemberArgumentResolver); } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authAdminInteceptor); + } } diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 64cc71b92..77e323405 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -67,6 +67,7 @@ public class MissionStepTest { @Test void 이단계() { +// String token = createToken("admin@email.com", "password"); // 일단계에서 토큰을 추출하는 로직을 메서드로 따로 만들어서 활용하세요. TokenRequest tokenRequest = new TokenRequest("admin@email.com", "password"); String token = authService.createToken(tokenRequest).getAccessToken(); @@ -99,4 +100,27 @@ public class MissionStepTest { assertThat(adminResponse.statusCode()).isEqualTo(201); assertThat(adminResponse.as(ReservationResponse.class).getName()).isEqualTo("브라운"); } + + @Test + void 삼단계() { + // String brownToken = createToken("brown@email.com", "password"); + TokenRequest brownTokenRequest = new TokenRequest("brown@email.com", "password"); + String brownToken = authService.createToken(brownTokenRequest).getAccessToken(); + + RestAssured.given().log().all() + .cookie("token", brownToken) + .get("/admin") + .then().log().all() + .statusCode(401); + + // String adminToken = createToken("admin@email.com", "password"); + TokenRequest adminTokenRequest = new TokenRequest("admin@email.com", "password"); + String adminToken = authService.createToken(adminTokenRequest).getAccessToken(); + + RestAssured.given().log().all() + .cookie("token", adminToken) + .get("/admin") + .then().log().all() + .statusCode(200); + } } \ No newline at end of file From ad519f48f2cf872ff817f0671dcda025348933aa Mon Sep 17 00:00:00 2001 From: gogo1414 Date: Wed, 26 Jun 2024 09:55:53 +0900 Subject: [PATCH 05/25] =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/auth/AuthAdminInteceptor.java | 4 ++-- .../java/roomescape/auth/AuthService.java | 15 +++++++------- .../infrastructure/JwtTokenUtil.java | 20 ++++++++++++------- .../LoginMemberArgumentResolver.java | 16 +++++++++++---- .../roomescape/member/MemberController.java | 4 ++-- 5 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/main/java/roomescape/auth/AuthAdminInteceptor.java b/src/main/java/roomescape/auth/AuthAdminInteceptor.java index a47eb35b6..0b9e1b990 100644 --- a/src/main/java/roomescape/auth/AuthAdminInteceptor.java +++ b/src/main/java/roomescape/auth/AuthAdminInteceptor.java @@ -10,8 +10,8 @@ @Component public class AuthAdminInteceptor implements HandlerInterceptor { - private MemberService memberService; - private JwtTokenUtil jwtTokenUtil; + private final MemberService memberService; + private final JwtTokenUtil jwtTokenUtil; public AuthAdminInteceptor(MemberService memberService, JwtTokenUtil jwtTokenUtil) { this.memberService = memberService; diff --git a/src/main/java/roomescape/auth/AuthService.java b/src/main/java/roomescape/auth/AuthService.java index e53d99f31..af891ce42 100644 --- a/src/main/java/roomescape/auth/AuthService.java +++ b/src/main/java/roomescape/auth/AuthService.java @@ -11,8 +11,10 @@ @Service public class AuthService { - private JwtTokenUtil jwtTokenUtil; - private MemberDao memberDao; + private final String INVALID_MEMBER_MSG = "존재하지 않는 email 또는 password 입니다."; + + private final JwtTokenUtil jwtTokenUtil; + private final MemberDao memberDao; public AuthService(JwtTokenUtil jwtTokenUtil, MemberDao memberDao) { this.jwtTokenUtil = jwtTokenUtil; @@ -20,12 +22,11 @@ public AuthService(JwtTokenUtil jwtTokenUtil, MemberDao memberDao) { } public Member checkInvalidLogin(String principal, String credentials) { - Member member = null; - try { - member = memberDao.findByEmailAndPassword(principal, credentials); - } catch(AuthorizationException e) { - e.printStackTrace(); + Member member = memberDao.findByEmailAndPassword(principal, credentials); + if(member == null) { + throw new AuthorizationException(INVALID_MEMBER_MSG); } + return member; } diff --git a/src/main/java/roomescape/infrastructure/JwtTokenUtil.java b/src/main/java/roomescape/infrastructure/JwtTokenUtil.java index 615d69acf..7eda4cf72 100644 --- a/src/main/java/roomescape/infrastructure/JwtTokenUtil.java +++ b/src/main/java/roomescape/infrastructure/JwtTokenUtil.java @@ -4,6 +4,7 @@ import io.jsonwebtoken.security.Keys; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -17,6 +18,8 @@ public class JwtTokenUtil { @Value("${security.jwt.token.expire-length}") private long validityInMilliseconds; + private final String INVALID_COOKIES = "쿠키 정보가 없습니다."; + public String createToken(Member member) { Date now = new Date(); Date validity = new Date(now.getTime() + validityInMilliseconds); @@ -43,8 +46,9 @@ public Long getPayload(HttpServletRequest request) { public boolean validateToken(String token) { try { - Jws claims = Jwts.parser() - .setSigningKey(secretKey) + Jws claims = Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) + .build() .parseClaimsJws(token); return !claims.getBody().getExpiration().before(new Date()); @@ -55,11 +59,13 @@ public boolean validateToken(String token) { private String extractTokenFromCookie(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); - for (Cookie cookie : cookies) { - if (cookie.getName().equals("token")) { - return cookie.getValue(); - } + if(cookies == null) { + throw new IllegalArgumentException(INVALID_COOKIES); } - return ""; + return Arrays.stream(request.getCookies()) + .filter(cookie -> "token".equals(cookie.getName())) + .map(Cookie::getValue) + .findFirst() + .orElse(""); } } \ No newline at end of file diff --git a/src/main/java/roomescape/infrastructure/LoginMemberArgumentResolver.java b/src/main/java/roomescape/infrastructure/LoginMemberArgumentResolver.java index 649399d22..5e927c68a 100644 --- a/src/main/java/roomescape/infrastructure/LoginMemberArgumentResolver.java +++ b/src/main/java/roomescape/infrastructure/LoginMemberArgumentResolver.java @@ -1,6 +1,7 @@ package roomescape.infrastructure; import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -13,13 +14,13 @@ @Component public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + @Autowired private MemberService memberService; + @Autowired private JwtTokenUtil jwtTokenUtil; - public LoginMemberArgumentResolver(MemberService memberService, JwtTokenUtil jwtTokenUtil) { - this.memberService = memberService; - this.jwtTokenUtil = jwtTokenUtil; - } + private final String INVALID_MEMBERID = "회원 아이디를 찾을 수 없습니다."; + private final String INVALID_MEMBER = "유효하지 않은 회원 정보입니다."; @Override public boolean supportsParameter(MethodParameter parameter) { @@ -34,7 +35,14 @@ public Object resolveArgument(MethodParameter parameter, HttpServletRequest httpServletRequest = (HttpServletRequest) webRequest.getNativeRequest(); Long memberId = jwtTokenUtil.getPayload(httpServletRequest); + if(memberId == null) { + throw new IllegalArgumentException(INVALID_MEMBERID); + } + Member member = memberService.findMemberById(memberId); + if(member == null) { + throw new IllegalArgumentException(INVALID_MEMBER); + } return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole()); } diff --git a/src/main/java/roomescape/member/MemberController.java b/src/main/java/roomescape/member/MemberController.java index b9d1aff27..5f539a253 100644 --- a/src/main/java/roomescape/member/MemberController.java +++ b/src/main/java/roomescape/member/MemberController.java @@ -16,8 +16,8 @@ @RestController public class MemberController { - private MemberService memberService; - private AuthService authService; + private final MemberService memberService; + private final AuthService authService; public MemberController(MemberService memberService, AuthService authService) { this.memberService = memberService; From 8b35084b271ddc5322ca250090f28118ffddd7fc Mon Sep 17 00:00:00 2001 From: gogo1414 Date: Wed, 26 Jun 2024 17:00:07 +0900 Subject: [PATCH 06/25] =?UTF-8?q?secret=20key=20=EA=B0=90=EC=B6=94?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index c2065bc26..6bc47d9ae 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ +### application.properties 안보이게 만들기 ### +!**/src/main/resources/application.properties + ### STS ### .apt_generated .classpath From 5b159400d1c7ad85092fc438b7e3849b5b343a12 Mon Sep 17 00:00:00 2001 From: gogo1414 Date: Wed, 26 Jun 2024 17:02:43 +0900 Subject: [PATCH 07/25] =?UTF-8?q?secret=20key=20=EA=B0=90=EC=B6=94?= =?UTF-8?q?=EA=B8=B02?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6bc47d9ae..c988fb160 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ build/ !**/src/test/**/build/ ### application.properties 안보이게 만들기 ### -!**/src/main/resources/application.properties +application.properties ### STS ### .apt_generated From 8ca9829ead8ef8e943ee0267b4bff39afe93ef80 Mon Sep 17 00:00:00 2001 From: gogo1414 Date: Wed, 26 Jun 2024 17:05:20 +0900 Subject: [PATCH 08/25] =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EC=95=88=EB=B3=B4=EC=9D=B4=EA=B2=8C=20=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.properties | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 src/main/resources/application.properties diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 2c0312b87..000000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,14 +0,0 @@ -spring.sql.init.encoding=utf-8 -spring.h2.console.enabled=true -spring.h2.console.path=/h2-console -spring.datasource.url=jdbc:h2:mem:database - -security.jwt.token.secret-key=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.ih1aovtQShabQ7l0cINw4k1fagApg3qLWiB8Kt59Lno -security.jwt.token.expire-length=3600000 - -#spring.jpa.show-sql=true -#spring.jpa.properties.hibernate.format_sql=true -#spring.jpa.ddl-auto=create-drop -#spring.jpa.defer-datasource-initialization=true - -#roomescape.auth.jwt.secret= Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= \ No newline at end of file From 1da7036a522a22b1f811e5678aad689948c9b69f Mon Sep 17 00:00:00 2001 From: gogo1414 Date: Mon, 1 Jul 2024 13:53:56 +0900 Subject: [PATCH 09/25] =?UTF-8?q?4=EB=8B=A8=EA=B3=84=20-=20JPA=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=20=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 0 -> 6148 bytes README.md | 20 ++++++++++++++++-- build.gradle | 3 ++- .../roomescape/auth/AuthAdminInteceptor.java | 3 ++- .../java/roomescape/auth/AuthService.java | 4 ++-- .../infrastructure/JwtTokenUtil.java | 9 ++++---- .../LoginMemberArgumentResolver.java | 2 +- .../roomescape/member/MemberController.java | 2 +- 8 files changed, 30 insertions(+), 13 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..b1568bd6aae90c9e63e7f8f67910bb3c495d7c88 GIT binary patch literal 6148 zcmeHLzfZzI6n-Z~D>^V3qlrTYXA=hZO3=ZT*+ifON!paC>>VBb6PTDB985@ba=}3z zoHWtJK_?gWAMm}q3*1p~G)CV`?!NZ@==b_T?SU5JKHzcVFMq8DxC3$lli>qPOelgU|DKeMOQ zPZs-WVlQ>5SiQh~-Hw`lTvTpub4m-N-t1v~hQ%*5e-+x;K?s4xzuG;L{DY{;ztg{a z?)%gOJR49nCEC8;aUN z(C^7~5WYg5SpinSRv@EJ^R)kOPJaKli};KcUUYkOGUgTy|<=rj`ms)$1V;s q%1ac=5}fpQEDvZarvDYBVa%fgL|T_r&G^ literal 0 HcmV?d00001 diff --git a/README.md b/README.md index cfd49acbc..a721af07f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -## 요구사항 분석 +## Spring MVC 요구사항 분석 ### 1단계 - 로그인 - 로그인 기능을 구현하세요. - 로그인 후 Cookie를 이용하여 사용자의 정보를 조회하는 API를 구현하세요. @@ -22,4 +22,20 @@ ### 3단계 - 관리자 기능 - 어드민 페이지 진입은 admin권한이 있는 사람만 할 수 있도록 제한하세요. -- HandlerInterceptor를 활용하여 권한이 없는 경우 401코드를 응답하세요. \ No newline at end of file +- HandlerInterceptor를 활용하여 권한이 없는 경우 401코드를 응답하세요. + +## Spring JPA 요구사항 분석 +### 4단계 - JPA 전환 +- JPA를 활용하여 데이터베이스에 접근하도록 수정하세요. + +#### gradle 의존성 추가 +- build.gradle 파일을 이용하여 다음 의존성을 대체하세요. + - as is: `spring-boot-stater-jdbc` to be: `spring-boot-starter-data-jpa` + +#### 엔티티 매핑 +- 다른 클래스를 의존하지 않는 클래스 먼저 엔티티 설정을 하세요. + - ex) Theme나 Time 등 + +#### 연관관계 매핑 +- 다른 클래스에 의존하는 클래스는 연관관계 매핑을 추가로 하세요. + - ex) Reservation은 Member나 Theme 등의 객체에 의존합니다. diff --git a/build.gradle b/build.gradle index 8d52aebc6..95d1a0a43 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,8 @@ 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' diff --git a/src/main/java/roomescape/auth/AuthAdminInteceptor.java b/src/main/java/roomescape/auth/AuthAdminInteceptor.java index 0b9e1b990..fb5767bf8 100644 --- a/src/main/java/roomescape/auth/AuthAdminInteceptor.java +++ b/src/main/java/roomescape/auth/AuthAdminInteceptor.java @@ -20,7 +20,8 @@ public AuthAdminInteceptor(MemberService memberService, JwtTokenUtil jwtTokenUti @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - Long memberId = jwtTokenUtil.getPayload(request); + System.out.println("--------- : " + request.getCookies()); + Long memberId = jwtTokenUtil.getPayload(request.getCookies()); Member member = memberService.findMemberById(memberId); if (member == null || !member.getRole().equals("ADMIN")) { response.setStatus(401); diff --git a/src/main/java/roomescape/auth/AuthService.java b/src/main/java/roomescape/auth/AuthService.java index af891ce42..f45d21534 100644 --- a/src/main/java/roomescape/auth/AuthService.java +++ b/src/main/java/roomescape/auth/AuthService.java @@ -30,8 +30,8 @@ public Member checkInvalidLogin(String principal, String credentials) { return member; } - public Long findMemberIdByToken(HttpServletRequest request) { - return jwtTokenUtil.getPayload(request); + public Long findMemberIdByToken(Cookie[] cookies) { + return jwtTokenUtil.getPayload(cookies); } public TokenResponse createToken(TokenRequest tokenRequest) { diff --git a/src/main/java/roomescape/infrastructure/JwtTokenUtil.java b/src/main/java/roomescape/infrastructure/JwtTokenUtil.java index 7eda4cf72..e182dcc73 100644 --- a/src/main/java/roomescape/infrastructure/JwtTokenUtil.java +++ b/src/main/java/roomescape/infrastructure/JwtTokenUtil.java @@ -35,8 +35,8 @@ public String createToken(Member member) { .compact(); } - public Long getPayload(HttpServletRequest request) { - String token = extractTokenFromCookie(request); + public Long getPayload(Cookie[] cookies) { + String token = extractTokenFromCookie(cookies); return Long.valueOf(Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) .build() @@ -57,12 +57,11 @@ public boolean validateToken(String token) { } } - private String extractTokenFromCookie(HttpServletRequest request) { - Cookie[] cookies = request.getCookies(); + private String extractTokenFromCookie(Cookie[] cookies) { if(cookies == null) { throw new IllegalArgumentException(INVALID_COOKIES); } - return Arrays.stream(request.getCookies()) + return Arrays.stream(cookies) .filter(cookie -> "token".equals(cookie.getName())) .map(Cookie::getValue) .findFirst() diff --git a/src/main/java/roomescape/infrastructure/LoginMemberArgumentResolver.java b/src/main/java/roomescape/infrastructure/LoginMemberArgumentResolver.java index 5e927c68a..368f84464 100644 --- a/src/main/java/roomescape/infrastructure/LoginMemberArgumentResolver.java +++ b/src/main/java/roomescape/infrastructure/LoginMemberArgumentResolver.java @@ -34,7 +34,7 @@ public Object resolveArgument(MethodParameter parameter, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) webRequest.getNativeRequest(); - Long memberId = jwtTokenUtil.getPayload(httpServletRequest); + Long memberId = jwtTokenUtil.getPayload(httpServletRequest.getCookies()); if(memberId == null) { throw new IllegalArgumentException(INVALID_MEMBERID); } diff --git a/src/main/java/roomescape/member/MemberController.java b/src/main/java/roomescape/member/MemberController.java index 5f539a253..0b13b6a4a 100644 --- a/src/main/java/roomescape/member/MemberController.java +++ b/src/main/java/roomescape/member/MemberController.java @@ -48,7 +48,7 @@ public ResponseEntity login(@RequestBody TokenRequest request, HttpServletRespon @GetMapping("/login/check") public ResponseEntity checkLogin(HttpServletRequest request) { - Long memberId = authService.findMemberIdByToken(request); + Long memberId = authService.findMemberIdByToken(request.getCookies()); Member member = memberService.findMemberById(memberId); MemberResponse response = new MemberResponse(member.getName()); return ResponseEntity.ok() From 0774c670551a0eb13e7619fef36d9b03b3ef33c2 Mon Sep 17 00:00:00 2001 From: gogo1414 Date: Mon, 1 Jul 2024 14:42:09 +0900 Subject: [PATCH 10/25] =?UTF-8?q?=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/roomescape/JpaTest.java | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/test/java/roomescape/JpaTest.java diff --git a/src/test/java/roomescape/JpaTest.java b/src/test/java/roomescape/JpaTest.java new file mode 100644 index 000000000..00f3d9200 --- /dev/null +++ b/src/test/java/roomescape/JpaTest.java @@ -0,0 +1,29 @@ +package roomescape; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import roomescape.time.Time; + +@DataJpaTest +public class JpaTest { + @Autowired + private TestEntityManager entityManager; + + @Autowired + private TimeRepository timeRepository; + + @Test + void 사단계() { + Time time = new Time("10:00"); + entityManager.persist(time); + entityManager.flush(); + + Time persistTime = timeRepository.findById(time.getId()).orElse(null); + + assertThat(persistTime.getTime()).isEqualTo(time.getTime()); + } +} From eeeb47b7eea346a225a5d072dfa34ecc0400e811 Mon Sep 17 00:00:00 2001 From: gogo1414 Date: Mon, 1 Jul 2024 16:17:31 +0900 Subject: [PATCH 11/25] =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A3=BC?= =?UTF-8?q?=EC=9E=85,=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=A7=A4=ED=95=91,?= =?UTF-8?q?=20=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=20=EB=A7=A4=ED=95=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/member/Member.java | 17 ++++++++++++++ .../roomescape/reservation/Reservation.java | 12 ++++++++++ src/main/java/roomescape/theme/Theme.java | 9 ++++++++ src/main/java/roomescape/time/Time.java | 14 +++++++++++ src/main/java/roomescape/time/TimeDao.java | 23 +++++-------------- .../java/roomescape/time/TimeRepository.java | 6 +++++ src/main/resources/schema.sql | 8 +++---- src/test/java/roomescape/JpaTest.java | 3 ++- 8 files changed, 70 insertions(+), 22 deletions(-) create mode 100644 src/main/java/roomescape/time/TimeRepository.java diff --git a/src/main/java/roomescape/member/Member.java b/src/main/java/roomescape/member/Member.java index 903aaa9b0..38c129a1a 100644 --- a/src/main/java/roomescape/member/Member.java +++ b/src/main/java/roomescape/member/Member.java @@ -1,10 +1,25 @@ package roomescape.member; +import jakarta.persistence.Column; +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; + + @Column(unique = true) private String email; + private String password; + private String role; public Member(Long id, String name, String email, String role) { @@ -21,6 +36,8 @@ public Member(String name, String email, String password, String role) { this.role = role; } + public Member() {} + public Long getId() { return id; } diff --git a/src/main/java/roomescape/reservation/Reservation.java b/src/main/java/roomescape/reservation/Reservation.java index 83a7edf1b..8d9e204a8 100644 --- a/src/main/java/roomescape/reservation/Reservation.java +++ b/src/main/java/roomescape/reservation/Reservation.java @@ -1,13 +1,25 @@ package roomescape.reservation; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; import roomescape.theme.Theme; import roomescape.time.Time; +@Entity public class Reservation { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String date; + + @OneToOne private Time time; + + @OneToOne private Theme theme; public Reservation(Long id, String name, String date, Time time, Theme theme) { diff --git a/src/main/java/roomescape/theme/Theme.java b/src/main/java/roomescape/theme/Theme.java index 430a6239c..8be4abd92 100644 --- a/src/main/java/roomescape/theme/Theme.java +++ b/src/main/java/roomescape/theme/Theme.java @@ -1,6 +1,15 @@ package roomescape.theme; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity public class Theme { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String description; diff --git a/src/main/java/roomescape/time/Time.java b/src/main/java/roomescape/time/Time.java index 008ed93cf..b3b27ab11 100644 --- a/src/main/java/roomescape/time/Time.java +++ b/src/main/java/roomescape/time/Time.java @@ -1,7 +1,21 @@ package roomescape.time; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "time") public class Time { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @Column(name = "time_value", nullable = false) private String value; public Time(Long id, String value) { diff --git a/src/main/java/roomescape/time/TimeDao.java b/src/main/java/roomescape/time/TimeDao.java index f39a9a328..c1d913a31 100644 --- a/src/main/java/roomescape/time/TimeDao.java +++ b/src/main/java/roomescape/time/TimeDao.java @@ -10,32 +10,21 @@ @Repository public class TimeDao { - private final JdbcTemplate jdbcTemplate; + private final TimeRepository timeRepository; - public TimeDao(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; + public TimeDao(TimeRepository timeRepository) { + this.timeRepository = timeRepository; } public List