-
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] 김준수 미션 제출합니다. #31
base: gogo1414
Are you sure you want to change the base?
Changes from 4 commits
602e91e
48d0d15
0d71c01
6c42437
ad519f4
8b35084
5b15940
8ca9829
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,25 @@ | ||
## 요구사항 분석 | ||
### 1단계 - 로그인 | ||
- 로그인 기능을 구현하세요. | ||
- 로그인 후 Cookie를 이용하여 사용자의 정보를 조회하는 API를 구현하세요. | ||
#### 로그인 기능 | ||
- 아래의 request와 response 요구사항에 따라 /login에 email, password 값을 body에 포함하세요. | ||
- 응답에 Cookie에 "token"값으로 토큰이 포함되도록 하세요. | ||
#### 인증 정보 조회 | ||
- 상단바 우측 로그인 상태를 표현해주기 위해 사용자의 정보를 조회하는 API를 구현하세요. | ||
- Cookie를 이용하여 로그인 사용자의 정보확인하세요. | ||
|
||
### 2단계 - 로그인 리팩터링 | ||
- 사용자의 정보를 조회하는 로직을 리팩터링 합니다. | ||
- 예약 생성 API 및 기능을 리팩터링 합니다. | ||
#### 로그인 리팩터링 | ||
- Cookie에 담긴 인증 정보를 이용해서 멤버 객체를 만드는 로직을 분리합니다. | ||
- HandlerMethodArgumentResolver을 활용하면 회원정보를 객체를 컨트롤러 메서드에 주입할 수 있습니다. | ||
#### 예약 생성 기능 변경 | ||
- 예약 생성 시 ReservationReqeust의 name이 없는 경우 Cookie에 담긴 정보를 활용하도록 리팩터링 합니다. | ||
- ReservationReqeust에 name값이 있으면 name으로 Member를 찾고 | ||
- 없으며 로그인 정보를 활용해서 Member를 찾도록 수정합니다. | ||
|
||
### 3단계 - 관리자 기능 | ||
- 어드민 페이지 진입은 admin권한이 있는 사람만 할 수 있도록 제한하세요. | ||
- HandlerInterceptor를 활용하여 권한이 없는 경우 401코드를 응답하세요. |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -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; | ||||||||||||||||||||
} | ||||||||||||||||||||
Comment on lines
+16
to
+19
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.
Suggested change
|
||||||||||||||||||||
|
||||||||||||||||||||
@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; | ||||||||||||||||||||
} | ||||||||||||||||||||
} |
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; | ||||||||||||||||||||
import roomescape.infrastructure.LoginMemberArgumentResolver; | ||||||||||||||||||||
|
||||||||||||||||||||
@Configuration | ||||||||||||||||||||
public class AuthConfig implements WebMvcConfigurer { | ||||||||||||||||||||
private final LoginMemberArgumentResolver loginMemberArgumentResolver; | ||||||||||||||||||||
private final AuthAdminInteceptor authAdminInteceptor; | ||||||||||||||||||||
|
||||||||||||||||||||
public AuthConfig(LoginMemberArgumentResolver loginMemberArgumentResolver, AuthAdminInteceptor authAdminInteceptor) { | ||||||||||||||||||||
this.loginMemberArgumentResolver = loginMemberArgumentResolver; | ||||||||||||||||||||
this.authAdminInteceptor = authAdminInteceptor; | ||||||||||||||||||||
} | ||||||||||||||||||||
Comment on lines
+15
to
+18
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.
Suggested change
|
||||||||||||||||||||
|
||||||||||||||||||||
@Override | ||||||||||||||||||||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { | ||||||||||||||||||||
resolvers.add(loginMemberArgumentResolver); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
@Override | ||||||||||||||||||||
public void addInterceptors(InterceptorRegistry registry) { | ||||||||||||||||||||
registry.addInterceptor(authAdminInteceptor); | ||||||||||||||||||||
} | ||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package roomescape.auth; | ||
|
||
import jakarta.servlet.http.Cookie; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import org.springframework.stereotype.Service; | ||
import roomescape.infrastructure.JwtTokenUtil; | ||
import roomescape.member.Member; | ||
import roomescape.member.MemberDao; | ||
import roomescape.token.TokenRequest; | ||
import roomescape.token.TokenResponse; | ||
|
||
@Service | ||
public class AuthService { | ||
private JwtTokenUtil jwtTokenUtil; | ||
private MemberDao memberDao; | ||
|
||
public AuthService(JwtTokenUtil jwtTokenUtil, MemberDao memberDao) { | ||
this.jwtTokenUtil = jwtTokenUtil; | ||
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; | ||
} | ||
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. "존재하지 않는 email 또는 password 입니다." 이런식으로 예외 처리하면 나중에 더 편할 것 같아요! 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. 예외 처리를 꼼꼼하게 못했는데 알려주셔서 감사해요👍 |
||
|
||
public Long findMemberIdByToken(HttpServletRequest request) { | ||
return jwtTokenUtil.getPayload(request); | ||
} | ||
|
||
public TokenResponse createToken(TokenRequest tokenRequest) { | ||
Member member = checkInvalidLogin(tokenRequest.getEmail(), tokenRequest.getPassword()); | ||
String accessToken = jwtTokenUtil.createToken(member); | ||
return new TokenResponse(accessToken); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package roomescape.auth; | ||
|
||
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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,65 @@ | ||||||||||||||||
package roomescape.infrastructure; | ||||||||||||||||
|
||||||||||||||||
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; | ||||||||||||||||
|
||||||||||||||||
import java.util.Date; | ||||||||||||||||
import roomescape.member.Member; | ||||||||||||||||
|
||||||||||||||||
@Component | ||||||||||||||||
public class JwtTokenUtil { | ||||||||||||||||
@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("email", member.getEmail()) | ||||||||||||||||
.claim("name", member.getName()) | ||||||||||||||||
.claim("role", member.getRole()) | ||||||||||||||||
.setIssuedAt(now) | ||||||||||||||||
.setExpiration(validity) | ||||||||||||||||
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) | ||||||||||||||||
.compact(); | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
public Long getPayload(HttpServletRequest request) { | ||||||||||||||||
String token = extractTokenFromCookie(request); | ||||||||||||||||
return Long.valueOf(Jwts.parserBuilder() | ||||||||||||||||
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) | ||||||||||||||||
.build() | ||||||||||||||||
.parseClaimsJws(token) | ||||||||||||||||
.getBody().getSubject()); | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
public boolean validateToken(String token) { | ||||||||||||||||
try { | ||||||||||||||||
Jws<Claims> claims = Jwts.parser() | ||||||||||||||||
.setSigningKey(secretKey) | ||||||||||||||||
.parseClaimsJws(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. 위에랑 통일되게
Suggested change
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. 통일성이 떨어지고 있었네요! 해당부분 발견해서 바로 수정했어요! |
||||||||||||||||
|
||||||||||||||||
return !claims.getBody().getExpiration().before(new Date()); | ||||||||||||||||
} catch (JwtException | IllegalArgumentException e) { | ||||||||||||||||
return false; | ||||||||||||||||
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. 예외 발생시 로그를 남기는 것이 나중에 원인을 찾을 때도 좋을 것 같아요! 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. 문제가 일어나면 어디서 발생했는지 찾을 때 도움이 되겠네요! |
||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
private String extractTokenFromCookie(HttpServletRequest request) { | ||||||||||||||||
Cookie[] cookies = request.getCookies(); | ||||||||||||||||
for (Cookie cookie : cookies) { | ||||||||||||||||
if (cookie.getName().equals("token")) { | ||||||||||||||||
return cookie.getValue(); | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
return ""; | ||||||||||||||||
} | ||||||||||||||||
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.
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. 감사해요! 코드의 인덴트도 2가 넘어가서 같이 수정 진행했습니다ㅎㅎ |
||||||||||||||||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -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; | ||||||||||||||||||||||||
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.
Suggested change
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. 수정했습니다! |
||||||||||||||||||||||||
|
||||||||||||||||||||||||
public LoginMemberArgumentResolver(MemberService memberService, JwtTokenUtil jwtTokenUtil) { | ||||||||||||||||||||||||
this.memberService = memberService; | ||||||||||||||||||||||||
this.jwtTokenUtil = jwtTokenUtil; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
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.
Suggested change
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.
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
@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); | ||||||||||||||||||||||||
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. 토큰이 없거나 회원 정보를 찾을 수 없는 경우 예외 처리를 해주면 좋을 거 같아요!
Suggested change
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. 예외 처리 진행했습니다! |
||||||||||||||||||||||||
|
||||||||||||||||||||||||
return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole()); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -10,13 +10,18 @@ | |||||||||
import org.springframework.web.bind.annotation.RestController; | ||||||||||
|
||||||||||
import java.net.URI; | ||||||||||
import roomescape.auth.AuthService; | ||||||||||
import roomescape.token.TokenRequest; | ||||||||||
import roomescape.token.TokenResponse; | ||||||||||
|
||||||||||
@RestController | ||||||||||
public class MemberController { | ||||||||||
private MemberService memberService; | ||||||||||
private AuthService authService; | ||||||||||
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. 위에서 알려드린 거 처럼
Suggested change
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. 수정 진행 했어요! |
||||||||||
|
||||||||||
public MemberController(MemberService memberService) { | ||||||||||
public MemberController(MemberService memberService, AuthService authService) { | ||||||||||
this.memberService = memberService; | ||||||||||
this.authService = authService; | ||||||||||
} | ||||||||||
|
||||||||||
@PostMapping("/members") | ||||||||||
|
@@ -25,6 +30,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) { | ||||||||||
TokenResponse tokenResponse = authService.createToken(request); | ||||||||||
|
||||||||||
Cookie cookie = new Cookie("token", tokenResponse.getAccessToken()); | ||||||||||
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<MemberResponse> checkLogin(HttpServletRequest request) { | ||||||||||
Long memberId = authService.findMemberIdByToken(request); | ||||||||||
Member member = memberService.findMemberById(memberId); | ||||||||||
MemberResponse response = new MemberResponse(member.getName()); | ||||||||||
return ResponseEntity.ok() | ||||||||||
.body(response); | ||||||||||
} | ||||||||||
|
||||||||||
@PostMapping("/logout") | ||||||||||
public ResponseEntity logout(HttpServletResponse response) { | ||||||||||
Cookie cookie = new Cookie("token", ""); | ||||||||||
|
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.
final
로 처리하는 것이 더 좋아보여요![Java] final 을 알아보자