diff --git a/src/main/java/roomescape/config/WebConfig.java b/src/main/java/roomescape/config/WebConfig.java new file mode 100644 index 000000000..f68d79972 --- /dev/null +++ b/src/main/java/roomescape/config/WebConfig.java @@ -0,0 +1,23 @@ +package roomescape; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import roomescape.member.AuthenticationInterceptor; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final AuthenticationInterceptor authenticationInterceptor; + + public WebConfig(AuthenticationInterceptor authenticationInterceptor) { + this.authenticationInterceptor = authenticationInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authenticationInterceptor) + .addPathPatterns("/admin/**") + .excludePathPatterns("/login", "/register"); + } +} diff --git a/src/main/java/roomescape/member/AuthenticationInterceptor.java b/src/main/java/roomescape/member/AuthenticationInterceptor.java new file mode 100644 index 000000000..b1c186561 --- /dev/null +++ b/src/main/java/roomescape/member/AuthenticationInterceptor.java @@ -0,0 +1,48 @@ +package roomescape.member; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +public class AuthenticationInterceptor implements HandlerInterceptor { + + private final MemberService memberService; + + public AuthenticationInterceptor(MemberService memberService) { + this.memberService = memberService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + System.out.println("Debug: start"); + String token = null; + if (request.getCookies() != null) { + for (jakarta.servlet.http.Cookie cookie : request.getCookies()) { + if ("token".equals(cookie.getName())) { + token = cookie.getValue(); + break; + } + } + } + + if (token == null) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 Unauthorized + return false; + } + + Member member = memberService.findMemberByToken(token); + if (member == null) { + throw new IllegalArgumentException("유효하지 않은 토큰이나 사용자입니다."); + } + + System.out.println("Debug: Role = " + member.getRole()); + if (!"ADMIN".equals(member.getRole())) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/member/LoginMemberArgumentResolver.java b/src/main/java/roomescape/member/LoginMemberArgumentResolver.java new file mode 100644 index 000000000..1355752c0 --- /dev/null +++ b/src/main/java/roomescape/member/LoginMemberArgumentResolver.java @@ -0,0 +1,54 @@ +package roomescape.member; + +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; + +@Component +public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + + private MemberService memberService; + + public LoginMemberArgumentResolver(MemberService memberService) { + this.memberService = memberService; + } + + @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 { + jakarta.servlet.http.HttpServletRequest request = webRequest.getNativeRequest(jakarta.servlet.http.HttpServletRequest.class); + if(request == null) { + throw new IllegalArgumentException("요청이 유효하지 않습니다."); + } + jakarta.servlet.http.Cookie[] cookies = request.getCookies(); + if(cookies == null) { + throw new IllegalArgumentException("유효한 쿠키를 찾을 수 없습니다."); + } + + String token = null; + for (jakarta.servlet.http.Cookie cookie : cookies) { + if ("token".equals(cookie.getName())) { + token = cookie.getValue(); + break; + } + } + if(token == null) { + throw new IllegalArgumentException("유효한 토큰을 찾을 수 없습니다."); + } + + Member member = memberService.findMemberByToken(token); + if (member == null) { + throw new IllegalArgumentException("유효하지 않은 토큰 또는 사용자입니다."); + } + + return member; + } +} \ 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..33b6cfb30 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 java.util.Map; +import java.util.UUID; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -25,6 +27,36 @@ public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member); } + @PostMapping("/login") + public ResponseEntity login(@RequestBody MemberRequest memberRequest, HttpServletResponse response) { + if(memberService.findMember(memberRequest) != null) { + String token = memberService.createToken(memberRequest.getEmail(), memberRequest.getPassword()); + Cookie cookie = new Cookie("token", token); + cookie.setPath("/"); + cookie.setMaxAge(3600); + response.addCookie(cookie); + return ResponseEntity.ok().build(); + } + return ResponseEntity.notFound().build(); + } + + @GetMapping("/login/check") + public ResponseEntity checkLogin(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if(cookies != null) { + for (Cookie cookie : cookies) { + if(cookie.getName().equals("token")) { + String token = cookie.getValue(); + String name = memberService.findMemberByToken(token).getName(); + + return ResponseEntity.ok(Map.of("name", name)); + } + } + } + return ResponseEntity.status(401).build(); + } + + @PostMapping("/logout") public ResponseEntity logout(HttpServletResponse response) { Cookie cookie = new Cookie("token", ""); diff --git a/src/main/java/roomescape/member/MemberService.java b/src/main/java/roomescape/member/MemberService.java index ccaa8cba5..460962eb8 100644 --- a/src/main/java/roomescape/member/MemberService.java +++ b/src/main/java/roomescape/member/MemberService.java @@ -1,5 +1,11 @@ package roomescape.member; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import java.security.Key; +import java.util.Date; +import javax.crypto.spec.SecretKeySpec; import org.springframework.stereotype.Service; @Service @@ -14,4 +20,37 @@ 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 MemberResponse findMember(MemberRequest memberRequest) { + Member member = memberDao.findByEmailAndPassword(memberRequest.getEmail(), memberRequest.getPassword()); + return new MemberResponse(member.getId(), member.getName(), member.getEmail()); + } + + public String createToken(String email, String password) { + String secret = "roomescape-application-secret-key-for-login!"; + Key key = new SecretKeySpec(secret.getBytes(), SignatureAlgorithm.HS256.getJcaName()); + Member member = memberDao.findByEmailAndPassword(email, password); + return Jwts.builder() + .setSubject("user-identifier") + .claim("name", member.getName()) + .claim("role", member.getRole()) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 3600000)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + public Member findMemberByToken(String token) { + String secret = "roomescape-application-secret-key-for-login!"; + Key key = new SecretKeySpec(secret.getBytes(), SignatureAlgorithm.HS256.getJcaName()); + + Claims claims = Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + + String name = claims.get("name", String.class); + return memberDao.findByName(name); + } } diff --git a/src/main/java/roomescape/reservation/ReservationController.java b/src/main/java/roomescape/reservation/ReservationController.java index b3bef3990..401a17eac 100644 --- a/src/main/java/roomescape/reservation/ReservationController.java +++ b/src/main/java/roomescape/reservation/ReservationController.java @@ -1,5 +1,9 @@ package roomescape.reservation; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.coyote.Response; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -10,14 +14,18 @@ import java.net.URI; import java.util.List; +import roomescape.member.MemberService; @RestController public class ReservationController { private final ReservationService reservationService; + private final MemberService memberService; - public ReservationController(ReservationService reservationService) { + public ReservationController(ReservationService reservationService, + final MemberService memberService) { this.reservationService = reservationService; + this.memberService = memberService; } @GetMapping("/reservations") @@ -26,13 +34,31 @@ public List list() { } @PostMapping("/reservations") - public ResponseEntity create(@RequestBody ReservationRequest reservationRequest) { - if (reservationRequest.getName() == null - || reservationRequest.getDate() == null + public ResponseEntity create(@RequestBody ReservationRequest reservationRequest, HttpServletRequest request) { + if (reservationRequest.getDate() == null || reservationRequest.getTheme() == null || reservationRequest.getTime() == null) { return ResponseEntity.badRequest().build(); } + else if(reservationRequest.getName() == null) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + return ResponseEntity.status(401).body("유효한 쿠키를 찾을 수 없습니다."); + } + for(Cookie cookie : cookies) { + if(cookie.getName().equals("token")) { + String token = cookie.getValue(); + if (token == null) { + return ResponseEntity.status(401).body("인증 토큰을 찾을 수 없습니다."); + } + String name = memberService.findMemberByToken(token).getName(); + if(name == null) { + return ResponseEntity.status(401).body("유효하지 않은 토큰입니다."); + } + reservationRequest.setName(name); + } + } + } ReservationResponse reservation = reservationService.save(reservationRequest); 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..ef7d8ec13 100644 --- a/src/main/java/roomescape/reservation/ReservationRequest.java +++ b/src/main/java/roomescape/reservation/ReservationRequest.java @@ -9,6 +9,9 @@ public class ReservationRequest { public String getName() { return name; } + public void setName(String name) { + this.name = name; + } public String getDate() { return date; diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 6add784bd..1b97f3b8d 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -5,11 +5,15 @@ import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; import java.util.HashMap; import java.util.Map; +import roomescape.member.LoginMemberArgumentResolver; +import roomescape.member.MemberService; +import roomescape.reservation.ReservationResponse; import static org.assertj.core.api.Assertions.assertThat; @@ -17,6 +21,9 @@ @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) public class MissionStepTest { + @Autowired + MemberService memberService; + @Test void 일단계() { Map params = new HashMap<>(); @@ -24,15 +31,78 @@ public class MissionStepTest { params.put("password", "password"); ExtractableResponse response = RestAssured.given().log().all() - .contentType(ContentType.JSON) - .body(params) - .when().post("/login") - .then().log().all() - .statusCode(200) - .extract(); + .contentType(ContentType.JSON) + .body(params) + .when().post("/login") + .then().log().all() + .statusCode(200) + .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("어드민"); + } + + @Test + void 이단계() { + String token = memberService.createToken("admin@email.com", "password"); // 일단계에서 토큰을 추출하는 로직을 메서드로 따로 만들어서 활용하세요. + + 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 + + @Test + void 삼단계() { + String brownToken = memberService.createToken("brown@email.com", "password"); + + RestAssured.given().log().all() + .cookie("token", brownToken) + .get("/admin") + .then().log().all() + .statusCode(401); + + String adminToken = memberService.createToken("admin@email.com", "password"); + + RestAssured.given().log().all() + .cookie("token", adminToken) + .get("/admin") + .then().log().all() + .statusCode(200); + } +} +