-
Notifications
You must be signed in to change notification settings - Fork 56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Spring MVC (인증)] 김진학 미션 제출합니다. #121
base: iampingu99
Are you sure you want to change the base?
Changes from all commits
e420920
fdac865
70f22dc
1cfeebe
5c0cd10
51b6962
188e66e
073dce2
d336a7d
c6aaf7c
00a357b
42bd449
b57678b
9adc046
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package roomescape.auth; | ||
|
||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.servlet.HandlerInterceptor; | ||
|
||
@Component | ||
public class AdminInterceptor implements HandlerInterceptor { | ||
private final AuthService authService; | ||
private final AuthorizationExtractor authorizationExtractor; | ||
|
||
public AdminInterceptor(AuthService authService, AuthorizationExtractor authorizationExtractor) { | ||
this.authService = authService; | ||
this.authorizationExtractor = authorizationExtractor; | ||
} | ||
|
||
@Override | ||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) | ||
throws Exception { | ||
|
||
String token = authorizationExtractor.extract(request); | ||
LoginMember loginMember = authService.createAuthentication(token); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
if (!loginMember.isAdmin()) { | ||
response.setStatus(401); | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package roomescape.auth; | ||
|
||
import jakarta.servlet.http.HttpServletRequest; | ||
|
||
public interface AuthorizationExtractor { | ||
String extract(HttpServletRequest request); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package roomescape.auth; | ||
|
||
import jakarta.servlet.http.Cookie; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import java.util.Arrays; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class CookieAuthorizationExtractor implements AuthorizationExtractor { | ||
private static final String AUTHORIZATION_NAME = "token"; | ||
|
||
@Override | ||
public String extract(HttpServletRequest request) { | ||
return Arrays.stream(request.getCookies()) | ||
.filter(cookie -> cookie.getName().equals(AUTHORIZATION_NAME)) | ||
.map(Cookie::getValue) | ||
.findFirst() | ||
.orElseThrow(() -> new IllegalArgumentException("Empty cookie")); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package roomescape.auth; | ||
|
||
import com.fasterxml.jackson.annotation.JsonIgnore; | ||
import io.jsonwebtoken.Claims; | ||
|
||
public record LoginMember( | ||
String email, | ||
String name, | ||
String role | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 클린코드 관점에서 Role 이라는 ENUM 을 만들어도 될 거 같네요. isAdmin 역시도 Role 에서 메소드로 가지고 있고 LoginMember 는 호출만 해도 되겠네용 |
||
) { | ||
public static LoginMember from(Claims claims) { | ||
return new LoginMember( | ||
claims.getSubject(), | ||
claims.get("name", String.class), | ||
claims.get("role", String.class)); | ||
} | ||
|
||
@JsonIgnore | ||
public boolean isAdmin() { | ||
return this.role.equals("ADMIN"); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package roomescape.auth; | ||
|
||
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; | ||
|
||
@Component | ||
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HandlerMethodArgumentResolver 가 정확하게 하는게 뭔가요? |
||
private final AuthService authService; | ||
private final AuthorizationExtractor authorizationExtractor; | ||
|
||
public LoginMemberArgumentResolver(AuthService authService, AuthorizationExtractor authorizationExtractor) { | ||
this.authService = authService; | ||
this.authorizationExtractor = authorizationExtractor; | ||
} | ||
|
||
@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 { | ||
String token = authorizationExtractor.extract((HttpServletRequest) webRequest.getNativeRequest()); | ||
return authService.createAuthentication(token); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package roomescape.auth; | ||
|
||
import java.util.Date; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class SystemTimeProvider implements TimeProvider { | ||
@Override | ||
public Date now() { | ||
return new Date(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package roomescape.auth; | ||
|
||
import java.util.Date; | ||
|
||
public interface TimeProvider { | ||
Date now(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
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.InterceptorRegistry; | ||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||
|
||
@Configuration | ||
public class WebConfig implements WebMvcConfigurer { | ||
private final LoginMemberArgumentResolver loginMemberArgumentResolver; | ||
private final AdminInterceptor adminInterceptor; | ||
|
||
public WebConfig(LoginMemberArgumentResolver loginMemberArgumentResolver, AdminInterceptor adminInterceptor) { | ||
this.loginMemberArgumentResolver = loginMemberArgumentResolver; | ||
this.adminInterceptor = adminInterceptor; | ||
} | ||
|
||
@Override | ||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { | ||
resolvers.add(loginMemberArgumentResolver); | ||
} | ||
|
||
@Override | ||
public void addInterceptors(InterceptorRegistry registry) { | ||
registry.addInterceptor(adminInterceptor) | ||
.addPathPatterns("/admin/**"); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,15 @@ | ||
package roomescape.reservation; | ||
|
||
import java.net.URI; | ||
import java.util.List; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.DeleteMapping; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.PathVariable; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
import java.net.URI; | ||
import java.util.List; | ||
import roomescape.auth.LoginMember; | ||
|
||
@RestController | ||
public class ReservationController { | ||
|
@@ -26,12 +26,16 @@ public List<ReservationResponse> 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 물론, 기존 코드에서 검증을 여기서 하지만 Controller 가 DTO 의 특정 요소가 null 인걸 알 필요가 있을까? 라고 생각해서 입니다. |
||
|| reservationRequest.getTheme() == null | ||
|| reservationRequest.getTime() == null) { | ||
return ResponseEntity.badRequest().build(); | ||
return ResponseEntity.badRequest().body("Invalid reservation request"); | ||
} | ||
|
||
if (reservationRequest.getName() == null) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분도 위와 비슷하게 getName 이 null 인지 + 새로운 객체 생성을 DTO 내부로 은닉해도 될 거 같아요. |
||
reservationRequest = new ReservationRequest(loginMember.name(), reservationRequest.getDate(), | ||
reservationRequest.getTheme(), reservationRequest.getTime()); | ||
} | ||
ReservationResponse reservation = reservationService.save(reservationRequest); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ | |
import org.junit.jupiter.api.Test; | ||
import org.springframework.boot.test.context.SpringBootTest; | ||
import org.springframework.test.annotation.DirtiesContext; | ||
import roomescape.reservation.ReservationResponse; | ||
|
||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) | ||
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) | ||
|
@@ -43,4 +44,73 @@ public class MissionStepTest { | |
|
||
assertThat(checkResponse.body().jsonPath().getString("name")).isEqualTo("어드민"); | ||
} | ||
|
||
@Test | ||
void 이단계() { | ||
String token = createToken("[email protected]", "password"); // 일단계에서 토큰을 추출하는 로직을 메서드로 따로 만들어서 활용하세요. | ||
|
||
Map<String, String> params = new HashMap<>(); | ||
params.put("date", "2024-03-01"); | ||
params.put("time", "1"); | ||
params.put("theme", "1"); | ||
|
||
ExtractableResponse<Response> 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<Response> 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("브라운"); | ||
} | ||
|
||
private String createToken(String email, String password) { | ||
Map<String, String> params = new HashMap<>(); | ||
params.put("email", email); | ||
params.put("password", password); | ||
|
||
ExtractableResponse<Response> response = RestAssured.given().log().all() | ||
.contentType(ContentType.JSON) | ||
.body(params) | ||
.when().post("/login") | ||
.then().log().all() | ||
.statusCode(200) | ||
.extract(); | ||
|
||
return response.headers().get("Set-Cookie").getValue().split(";")[0].split("=")[1]; | ||
} | ||
|
||
@Test | ||
void 삼단계() { | ||
String brownToken = createToken("[email protected]", "password"); | ||
|
||
RestAssured.given().log().all() | ||
.cookie("token", brownToken) | ||
.get("/admin") | ||
.then().log().all() | ||
.statusCode(401); | ||
|
||
String adminToken = createToken("[email protected]", "password"); | ||
|
||
RestAssured.given().log().all() | ||
.cookie("token", adminToken) | ||
.get("/admin") | ||
.then().log().all() | ||
.statusCode(200); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interceptor 를 사용했네요.
Interceptor 와 Filter 의 차이점이 뭔가요?