Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Spring MVC(인증)] 한성재 미션 제출합니다. #104

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/main/java/roomescape/PageController.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package roomescape;

import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import roomescape.member.LoginMember;
import roomescape.member.Member;

@Controller
public class PageController {
31 changes: 31 additions & 0 deletions src/main/java/roomescape/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package roomescape.config;

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.member.LoginMemberArgumentResolver;
import roomescape.member.LoginMemberInterceptor;

@Configuration
public class WebConfig implements WebMvcConfigurer {

private final LoginMemberArgumentResolver loginMemberArgumentResolver;
private final LoginMemberInterceptor loginMemberInterceptor;

public WebConfig(LoginMemberArgumentResolver loginMemberArgumentResolver, LoginMemberInterceptor loginMemberInterceptor) {
this.loginMemberArgumentResolver = loginMemberArgumentResolver;
this.loginMemberInterceptor = loginMemberInterceptor;
}

@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginMemberArgumentResolver);
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginMemberInterceptor).addPathPatterns("/admin");
}
}
8 changes: 8 additions & 0 deletions src/main/java/roomescape/member/LoginMember.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package roomescape.member;

public class LoginMember extends Member {

public LoginMember(final Long id, final String name, final String email, final String role) {
super(id, name, email, role);
}
}
42 changes: 42 additions & 0 deletions src/main/java/roomescape/member/LoginMemberArgumentResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package roomescape.member;

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 {

private final MemberService memberService;
private final TokenUtil tokenUtil;

public LoginMemberArgumentResolver(MemberService memberService, TokenUtil tokenUtil) {
this.memberService = memberService;
this.tokenUtil = tokenUtil;
}

@Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.getParameterType().equals(LoginMember.class);

}

@Override
public Object resolveArgument(
final MethodParameter parameter,
final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest,
final WebDataBinderFactory binderFactory)
throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
Long memberId = tokenUtil.getMemberId(request.getCookies());

Member member = memberService.find(memberId);
return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole());

}
}
31 changes: 31 additions & 0 deletions src/main/java/roomescape/member/LoginMemberInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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 LoginMemberInterceptor implements HandlerInterceptor {

private final TokenUtil tokenUtil;
private final MemberService memberService;

public LoginMemberInterceptor(TokenUtil tokenUtil, MemberService memberService) {
this.tokenUtil = tokenUtil;
this.memberService = memberService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Long memberId = this.tokenUtil.getMemberId(request.getCookies());

Member member = this.memberService.find(memberId);

if (member == null || !member.getRole().equals("ADMIN")) {
response.setStatus(401);
return false;
}

return true;
}
}
74 changes: 53 additions & 21 deletions src/main/java/roomescape/member/MemberController.java
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Map;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -13,25 +14,56 @@

@RestController
public class MemberController {
private MemberService memberService;

public MemberController(MemberService memberService) {
this.memberService = memberService;
}

@PostMapping("/members")
public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) {
MemberResponse member = memberService.createMember(memberRequest);
return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member);
}

@PostMapping("/logout")
public ResponseEntity logout(HttpServletResponse response) {
Cookie cookie = new Cookie("token", "");
cookie.setHttpOnly(true);
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
return ResponseEntity.ok().build();
}

private final MemberDao memberDao;
private final TokenUtil tokenUtil;
private MemberService memberService;

public MemberController(MemberService memberService, MemberDao memberDao, TokenUtil tokenUtil) {
this.memberService = memberService;
this.memberDao = memberDao;
this.tokenUtil = tokenUtil;
}

@PostMapping("/members")
public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) {
MemberResponse member = memberService.createMember(memberRequest);
return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member);
}

@PostMapping("/login")
public ResponseEntity<Void> login(@RequestBody Map<String, String> body) {
// HttpServletRequest 가 Request 객체가 아닌가? 일단 Map으로 대체
String email = body.get("email");
String password = body.get("password");

// TODO: member가 존재하지 않을때의 처리
Member member = memberDao.findByEmailAndPassword(email, password);
String token = tokenUtil.generate(member);

// TODO: 쿠키를 header 에 정상적으로 넣도록 수정
Cookie cookie = new Cookie("token", token);
cookie.setHttpOnly(true);
cookie.setPath("/");

return ResponseEntity.ok().header("Set-Cookie", "token=" + token + ";").build();
}

@GetMapping("/login/check")
public ResponseEntity<Map<String, String>> checkLogin(HttpServletRequest request) {
Long memberId = tokenUtil.getMemberId(request.getCookies());
Member member = memberDao.findById(memberId);

return ResponseEntity.ok().body(Map.of("name", member.getName()));
}

@PostMapping("/logout")
public ResponseEntity<Void> logout(HttpServletResponse response) {
Cookie cookie = new Cookie("token", "");
cookie.setHttpOnly(true);
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
return ResponseEntity.ok().build();
}
}
12 changes: 12 additions & 0 deletions src/main/java/roomescape/member/MemberDao.java
Original file line number Diff line number Diff line change
@@ -52,4 +52,16 @@ public Member findByName(String name) {
name
);
}

public Member findById(final Long memberId) {
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")
),
memberId
);
}
}
4 changes: 4 additions & 0 deletions src/main/java/roomescape/member/MemberService.java
Original file line number Diff line number Diff line change
@@ -10,6 +10,10 @@ public MemberService(MemberDao memberDao) {
this.memberDao = memberDao;
}

public Member find(Long memberId) {
return this.memberDao.findById(memberId);
}

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());
58 changes: 58 additions & 0 deletions src/main/java/roomescape/member/TokenUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package roomescape.member;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.Cookie;
import java.security.Key;
import java.util.Date;
import org.springframework.stereotype.Component;


@Component
public class TokenUtil {

private Key key;

public TokenUtil() {
this.key = Keys.secretKeyFor(SignatureAlgorithm.HS512);
}

public String generate(final Member member) {
Date now = new Date();

// duration 1시간으로 가정
int durationSecond = 60 * 60;
Date expirationDate = new Date(now.getTime() + 1000L * durationSecond);

return Jwts.builder()
.setSubject(member.getId().toString())
.claim("name", member.getName())
.claim("role", member.getName())
.signWith(this.key)
.setIssuedAt(now)
.setExpiration(expirationDate)
.compact();
}

public Long getMemberId(final Cookie[] cookies) {

String token = this.extractTokenFromCookie(cookies);

return Long.valueOf(Jwts.parserBuilder()
.setSigningKey(this.key)
.build()
.parseClaimsJws(token)
.getBody().getSubject());
}

private String extractTokenFromCookie(Cookie[] cookies) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("token")) {
return cookie.getValue();
}
}

return "";
}
}
52 changes: 28 additions & 24 deletions src/main/java/roomescape/reservation/ReservationController.java
Original file line number Diff line number Diff line change
@@ -10,37 +10,41 @@

import java.net.URI;
import java.util.List;
import roomescape.member.LoginMember;
import roomescape.member.Member;

@RestController
public class ReservationController {

private final ReservationService reservationService;
private final ReservationService reservationService;

public ReservationController(ReservationService reservationService) {
this.reservationService = reservationService;
}
public ReservationController(ReservationService reservationService) {
this.reservationService = reservationService;
}

@GetMapping("/reservations")
public List<ReservationResponse> list() {
return reservationService.findAll();
}
@GetMapping("/reservations")
public List<ReservationResponse> list() {
return reservationService.findAll();
}

@PostMapping("/reservations")
public ResponseEntity create(@RequestBody ReservationRequest reservationRequest) {
if (reservationRequest.getName() == null
|| reservationRequest.getDate() == null
|| reservationRequest.getTheme() == null
|| reservationRequest.getTime() == null) {
return ResponseEntity.badRequest().build();
}
ReservationResponse reservation = reservationService.save(reservationRequest);

return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation);
@PostMapping("/reservations")
public ResponseEntity create(@RequestBody ReservationRequest reservationRequest, LoginMember member) {
if ((member == null && reservationRequest.getName() == null)
|| reservationRequest.getDate() == null
|| reservationRequest.getTheme() == null
|| reservationRequest.getTime() == null) {
return ResponseEntity.badRequest().build();
}

@DeleteMapping("/reservations/{id}")
public ResponseEntity delete(@PathVariable Long id) {
reservationService.deleteById(id);
return ResponseEntity.noContent().build();
}
ReservationResponse reservation = reservationService.save(member, reservationRequest);

return ResponseEntity.created(URI.create("/reservations/" + reservation.getId()))
.body(reservation);
}

@DeleteMapping("/reservations/{id}")
public ResponseEntity delete(@PathVariable Long id) {
reservationService.deleteById(id);
return ResponseEntity.noContent().build();
}
}
4 changes: 4 additions & 0 deletions src/main/java/roomescape/reservation/ReservationRequest.java
Original file line number Diff line number Diff line change
@@ -21,4 +21,8 @@ public Long getTheme() {
public Long getTime() {
return time;
}

public void setName(final String name) {
this.name = name;
}
}
11 changes: 11 additions & 0 deletions src/main/java/roomescape/reservation/ReservationService.java
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@
import org.springframework.stereotype.Service;

import java.util.List;
import roomescape.member.LoginMember;
import roomescape.member.Member;

@Service
public class ReservationService {
@@ -12,6 +14,15 @@ public ReservationService(ReservationDao reservationDao) {
this.reservationDao = reservationDao;
}

public ReservationResponse save(LoginMember member, ReservationRequest reservationRequest) {
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());
}

public ReservationResponse save(ReservationRequest reservationRequest) {
Reservation reservation = reservationDao.save(reservationRequest);

98 changes: 83 additions & 15 deletions src/test/java/roomescape/MissionStepTest.java
Original file line number Diff line number Diff line change
@@ -10,29 +10,97 @@

import java.util.HashMap;
import java.util.Map;
import roomescape.reservation.ReservationResponse;

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 {

@Test
void 일단계() {
Map<String, String> params = new HashMap<>();
params.put("email", "admin@email.com");
params.put("password", "password");
@Test
void 일단계() {
String token = createToken("admin@email.com", "password");
assertThat(token).isNotBlank();

ExtractableResponse<Response> response = RestAssured.given().log().all()
.contentType(ContentType.JSON)
.body(params)
.when().post("/login")
.then().log().all()
.statusCode(200)
.extract();
ExtractableResponse<Response> checkResponse = RestAssured.given().log().all()
.contentType(ContentType.JSON)
.cookie("token", token)
.when().get("/login/check")
.then().log().all()
.statusCode(200)
.extract();

String token = response.headers().get("Set-Cookie").getValue().split(";")[0].split("=")[1];
assertThat(checkResponse.body().jsonPath().getString("name")).isEqualTo("어드민");
}

assertThat(token).isNotBlank();
}
private static String createToken(final String mail, final String password) {
Map<String, String> params = new HashMap<>();
params.put("email", mail);
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 token = createToken("admin@email.com",
"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("브라운");
}

@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);
}
}