diff --git a/build.gradle b/build.gradle index 8d52aebc..063960a5 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,8 @@ 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.projectlombok:lombok' + implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.0.0' diff --git a/src/main/java/roomescape/AdminHandlerInterceptor.java b/src/main/java/roomescape/AdminHandlerInterceptor.java new file mode 100644 index 00000000..4c2bea59 --- /dev/null +++ b/src/main/java/roomescape/AdminHandlerInterceptor.java @@ -0,0 +1,39 @@ +package roomescape; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import roomescape.infrastructure.JwtTokenDecoder; +import roomescape.infrastructure.TokenExtractor; +import roomescape.member.Member; +import roomescape.member.MemberService; + +@Component +public class AdminHandlerInterceptor implements HandlerInterceptor { + private MemberService memberService; + + public AdminHandlerInterceptor(MemberService memberService) { + this.memberService = memberService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + String token = TokenExtractor.extractTokenFromCookie(request.getCookies()); + Long id = JwtTokenDecoder.decodeToken(token); + Member member = memberService.findById(id); + + if (member == null || !member.getRole().equals("ADMIN")) { + response.setStatus(401); + return false; + } + + return true; + } + + +} \ No newline at end of file diff --git a/src/main/java/roomescape/LoginMemberArgumentResolver.java b/src/main/java/roomescape/LoginMemberArgumentResolver.java new file mode 100644 index 00000000..35903b26 --- /dev/null +++ b/src/main/java/roomescape/LoginMemberArgumentResolver.java @@ -0,0 +1,47 @@ +package roomescape; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.Cookie; +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.infrastructure.JwtTokenDecoder; +import roomescape.infrastructure.TokenExtractor; +import roomescape.member.LoginMember; +import roomescape.member.Member; +import roomescape.member.MemberService; + +@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(LoginMember.class); +// return Member.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + Cookie[] cookies = request.getCookies(); + String token = TokenExtractor.extractTokenFromCookie(cookies); + if (token == null) return null; + + Long id = JwtTokenDecoder.decodeToken(token); + Member member = memberService.findById(id); + return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getPassword()); + } + + +} diff --git a/src/main/java/roomescape/config/WebConfig.java b/src/main/java/roomescape/config/WebConfig.java new file mode 100644 index 00000000..ff21999b --- /dev/null +++ b/src/main/java/roomescape/config/WebConfig.java @@ -0,0 +1,33 @@ +package roomescape.config; + +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.AdminHandlerInterceptor; +import roomescape.LoginMemberArgumentResolver; + +import java.util.List; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + private final LoginMemberArgumentResolver loginMemberArgumentResolver; + private final AdminHandlerInterceptor adminHandlerInterceptor; + + + public WebConfig(LoginMemberArgumentResolver loginMemberArgumentResolver, AdminHandlerInterceptor adminHandlerInterceptor) { + this.loginMemberArgumentResolver = loginMemberArgumentResolver; + this.adminHandlerInterceptor = adminHandlerInterceptor; + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(loginMemberArgumentResolver); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(adminHandlerInterceptor) + .addPathPatterns("/admin"); + } +} diff --git a/src/main/java/roomescape/infrastructure/JwtTokenDecoder.java b/src/main/java/roomescape/infrastructure/JwtTokenDecoder.java new file mode 100644 index 00000000..b5c42291 --- /dev/null +++ b/src/main/java/roomescape/infrastructure/JwtTokenDecoder.java @@ -0,0 +1,15 @@ +package roomescape.infrastructure; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; + +public class JwtTokenDecoder { + public static Long decodeToken(String token) { + Long memberId = Long.valueOf(Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=".getBytes())) + .build() + .parseClaimsJws(token) + .getBody().getSubject()); + return memberId; + } +} diff --git a/src/main/java/roomescape/infrastructure/JwtTokenProvider.java b/src/main/java/roomescape/infrastructure/JwtTokenProvider.java new file mode 100644 index 00000000..c565947f --- /dev/null +++ b/src/main/java/roomescape/infrastructure/JwtTokenProvider.java @@ -0,0 +1,24 @@ +package roomescape.infrastructure; + + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import roomescape.member.Member; + +@Component +public class JwtTokenProvider { + @Value("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=") + private String secretKey; + + public String createToken(Member member) { + return Jwts.builder() + .setSubject(member.getId().toString()) + .claim("name", member.getName()) + .claim("role", member.getRole()) + .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) + .compact(); + } + +} diff --git a/src/main/java/roomescape/infrastructure/TokenExtractor.java b/src/main/java/roomescape/infrastructure/TokenExtractor.java new file mode 100644 index 00000000..53df35ae --- /dev/null +++ b/src/main/java/roomescape/infrastructure/TokenExtractor.java @@ -0,0 +1,15 @@ +package roomescape.infrastructure; + +import jakarta.servlet.http.Cookie; + +public class TokenExtractor { + + public static 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/LoginMember.java b/src/main/java/roomescape/member/LoginMember.java new file mode 100644 index 00000000..af3b292b --- /dev/null +++ b/src/main/java/roomescape/member/LoginMember.java @@ -0,0 +1,30 @@ +package roomescape.member; +public class LoginMember{ + private Long id; + private String name; + private String email; + private String password; + + public LoginMember(Long id, String name, String email, String password) { + this.id = id; + this.name = name; + this.email = email; + this.password = password; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } +} diff --git a/src/main/java/roomescape/member/LoginRequest.java b/src/main/java/roomescape/member/LoginRequest.java new file mode 100644 index 00000000..788afb13 --- /dev/null +++ b/src/main/java/roomescape/member/LoginRequest.java @@ -0,0 +1,22 @@ +package roomescape.member; + +import lombok.Getter; + + +public class LoginRequest { + private String email; + private String password; + + public LoginRequest(String email, String password) { + this.email = email; + this.password = password; + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } +} diff --git a/src/main/java/roomescape/member/LoginResponse.java b/src/main/java/roomescape/member/LoginResponse.java new file mode 100644 index 00000000..e7aff230 --- /dev/null +++ b/src/main/java/roomescape/member/LoginResponse.java @@ -0,0 +1,13 @@ +package roomescape.member; + +public class LoginResponse { + private String name; + + public LoginResponse(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/roomescape/member/MemberController.java b/src/main/java/roomescape/member/MemberController.java index 881ae5e0..dc024409 100644 --- a/src/main/java/roomescape/member/MemberController.java +++ b/src/main/java/roomescape/member/MemberController.java @@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import roomescape.infrastructure.TokenExtractor; import java.net.URI; @@ -25,6 +26,27 @@ public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member); } + @PostMapping("/login") + public ResponseEntity login (@RequestBody LoginRequest loginRequest, HttpServletResponse response) { + String token = memberService.login(loginRequest); + + Cookie cookie = new Cookie("token", token); + cookie.setHttpOnly(true); + cookie.setPath("/"); + response.addCookie(cookie); + + return ResponseEntity.ok().build(); + } + + @GetMapping("login/check") + public ResponseEntity checkLogin(HttpServletRequest request){ + Cookie[] cookies = request.getCookies(); + String token = TokenExtractor.extractTokenFromCookie(cookies); + LoginResponse loginResponse= memberService.checkLogin(token); + return ResponseEntity.ok(loginResponse); + } + + @PostMapping("/logout") public ResponseEntity logout(HttpServletResponse response) { Cookie cookie = new Cookie("token", ""); diff --git a/src/main/java/roomescape/member/MemberDao.java b/src/main/java/roomescape/member/MemberDao.java index 81f77f4c..aa977a29 100644 --- a/src/main/java/roomescape/member/MemberDao.java +++ b/src/main/java/roomescape/member/MemberDao.java @@ -52,4 +52,16 @@ 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/MemberService.java b/src/main/java/roomescape/member/MemberService.java index ccaa8cba..4ebca9b2 100644 --- a/src/main/java/roomescape/member/MemberService.java +++ b/src/main/java/roomescape/member/MemberService.java @@ -1,17 +1,42 @@ package roomescape.member; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; import org.springframework.stereotype.Service; +import roomescape.infrastructure.JwtTokenDecoder; +import roomescape.infrastructure.JwtTokenProvider; @Service public class MemberService { private MemberDao memberDao; + private JwtTokenProvider jwtTokenProvider; - public MemberService(MemberDao memberDao) { + public MemberService(MemberDao memberDao,JwtTokenProvider jwtTokenProvider) { this.memberDao = memberDao; + this.jwtTokenProvider = jwtTokenProvider; } 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 String login(LoginRequest loginRequest){ + Member member = memberDao.findByEmailAndPassword(loginRequest.getEmail(),loginRequest.getPassword()); + + String accessToken = jwtTokenProvider.createToken(member); + + return accessToken; + } + + public LoginResponse checkLogin (String token) { + + Long memberId = JwtTokenDecoder.decodeToken(token); + Member member = memberDao.findById(memberId); + + return new LoginResponse(member.getName()); + } + public Member findById(Long id) { + return memberDao.findById(id); + } } diff --git a/src/main/java/roomescape/reservation/ReservationController.java b/src/main/java/roomescape/reservation/ReservationController.java index b3bef399..467e5d1a 100644 --- a/src/main/java/roomescape/reservation/ReservationController.java +++ b/src/main/java/roomescape/reservation/ReservationController.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import roomescape.member.LoginMember; import java.net.URI; import java.util.List; @@ -25,15 +26,15 @@ public List list() { return reservationService.findAll(); } + @PostMapping("/reservations") - public ResponseEntity create(@RequestBody ReservationRequest reservationRequest) { - if (reservationRequest.getName() == null - || reservationRequest.getDate() == null + public ResponseEntity create(@RequestBody ReservationRequest reservationRequest, LoginMember member) { + if (reservationRequest.getDate() == null || reservationRequest.getTheme() == null || reservationRequest.getTime() == null) { return ResponseEntity.badRequest().build(); } - ReservationResponse reservation = reservationService.save(reservationRequest); + ReservationResponse reservation = reservationService.save(reservationRequest, member); 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 19f44124..0e6a70e5 100644 --- a/src/main/java/roomescape/reservation/ReservationRequest.java +++ b/src/main/java/roomescape/reservation/ReservationRequest.java @@ -6,6 +6,10 @@ public class ReservationRequest { private Long theme; private Long time; + public void setName(String name) { + this.name = name; + } + public String getName() { return name; } diff --git a/src/main/java/roomescape/reservation/ReservationService.java b/src/main/java/roomescape/reservation/ReservationService.java index bd331332..6de35068 100644 --- a/src/main/java/roomescape/reservation/ReservationService.java +++ b/src/main/java/roomescape/reservation/ReservationService.java @@ -1,6 +1,7 @@ package roomescape.reservation; import org.springframework.stereotype.Service; +import roomescape.member.LoginMember; import java.util.List; @@ -12,7 +13,12 @@ public ReservationService(ReservationDao reservationDao) { this.reservationDao = reservationDao; } - public ReservationResponse save(ReservationRequest reservationRequest) { + public ReservationResponse save(ReservationRequest reservationRequest, LoginMember member) { + + if (reservationRequest.getName() == null) { + reservationRequest.setName(member.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 6add784b..f0933b80 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -5,8 +5,14 @@ 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.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.annotation.DirtiesContext; +import roomescape.infrastructure.JwtTokenProvider; +import roomescape.member.*; +import roomescape.reservation.ReservationResponse; import java.util.HashMap; import java.util.Map; @@ -17,6 +23,9 @@ @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) public class MissionStepTest { + @Autowired + private MemberService memberService; + @Test void 일단계() { Map params = new HashMap<>(); @@ -35,4 +44,64 @@ public class MissionStepTest { assertThat(token).isNotBlank(); } + + @Test + void 이단계() { + String token = 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("브라운"); + } + + @Test + void 삼단계() { + String brownToken = createToken("brown@email.com", "password"); + + RestAssured.given().log().all() + .cookie("token", brownToken) + .get("/admin") + .then().log().all() + .statusCode(401); + + String adminToken = createToken("admin@email.com", "password"); + + RestAssured.given().log().all() + .cookie("token", adminToken) + .get("/admin") + .then().log().all() + .statusCode(200); + } + + private String createToken(String email, String password) { + LoginRequest request = new LoginRequest(email, password); + System.out.println(request.getEmail() + " , " + request.getPassword()); + String token = memberService.login(request); + return token; + } } \ No newline at end of file