-
Notifications
You must be signed in to change notification settings - Fork 77
[Sping MVC(인증)] 김예진 미션 제출합니다. #160
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
base: dpwls0125
Are you sure you want to change the base?
Changes from all commits
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,27 @@ | ||
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.member.MemberService; | ||
|
||
@Component | ||
public class AdminRoleCheckInterceptor implements HandlerInterceptor { | ||
|
||
private final MemberService memberService; | ||
|
||
public AdminRoleCheckInterceptor(final MemberService memberService) { | ||
this.memberService = memberService; | ||
} | ||
|
||
@Override | ||
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception { | ||
LoginMember member = memberService.parseTokenAndGetMemberInfo(request.getCookies()); | ||
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,32 @@ | ||
package roomescape.auth; | ||
|
||
public class LoginMember { | ||
|
||
private Long id; | ||
private String name; | ||
private String email; | ||
private String role; | ||
|
||
public LoginMember(final Long id, final String name, final String email, final 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 |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package roomescape.auth; | ||
|
||
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.member.MemberService; | ||
|
||
@Component | ||
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { | ||
|
||
private final MemberService memberService; | ||
|
||
public LoginMemberArgumentResolver(final MemberService memberService) { | ||
this.memberService = memberService; | ||
} | ||
|
||
@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(); | ||
Cookie[] cookies = request.getCookies(); | ||
return memberService.parseTokenAndGetMemberInfo(cookies); | ||
} | ||
|
||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package roomescape.auth; | ||
|
||
public class LoginRequest { | ||
private String email; | ||
private String password; | ||
|
||
public String getEmail() { | ||
return email; | ||
} | ||
|
||
public String getPassword() { | ||
return password; | ||
} | ||
} | ||
Comment on lines
+3
to
+14
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. record를 사용해봐도 좋을 것 같네요 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.auth.AdminRoleCheckInterceptor; | ||
import roomescape.auth.LoginMemberArgumentResolver; | ||
|
||
import java.util.List; | ||
|
||
@Configuration | ||
public class WebConfig implements WebMvcConfigurer { | ||
|
||
private final LoginMemberArgumentResolver loginMemberArgumentResolver; | ||
private final AdminRoleCheckInterceptor adminRoleCheckInterceptor; | ||
|
||
public WebConfig(final LoginMemberArgumentResolver loginMemberArgumentResolver, final AdminRoleCheckInterceptor adminRoleCheckInterceptor) { | ||
this.loginMemberArgumentResolver = loginMemberArgumentResolver; | ||
this.adminRoleCheckInterceptor = adminRoleCheckInterceptor; | ||
} | ||
|
||
@Override | ||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { | ||
resolvers.add(loginMemberArgumentResolver); | ||
} | ||
|
||
@Override | ||
public void addInterceptors(final InterceptorRegistry registry) { | ||
registry.addInterceptor(adminRoleCheckInterceptor) | ||
.addPathPatterns("/admin/**"); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,11 +3,16 @@ | |
import jakarta.servlet.http.Cookie; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.ResponseCookie; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RestController; | ||
import roomescape.auth.LoginMember; | ||
import roomescape.auth.LoginRequest; | ||
import roomescape.token.TokenResponse; | ||
|
||
import java.net.URI; | ||
|
||
|
@@ -19,6 +24,27 @@ public MemberController(MemberService memberService) { | |
this.memberService = memberService; | ||
} | ||
|
||
@PostMapping("/login") | ||
public ResponseEntity<Void> login(@RequestBody LoginRequest loginRequest) { | ||
TokenResponse token = memberService.getAccessToken(loginRequest.getEmail(), loginRequest.getPassword()); | ||
ResponseCookie cookie = ResponseCookie.from("token", token.getAccessToken()) | ||
.httpOnly(true) | ||
.path("/") | ||
.build(); | ||
|
||
return ResponseEntity.ok() | ||
.header(HttpHeaders.SET_COOKIE, cookie.toString()) | ||
.build(); | ||
} | ||
|
||
@GetMapping("/login/check") | ||
public ResponseEntity<MemberResponse> loingCheck(HttpServletRequest request) { | ||
Cookie[] cookies = request.getCookies(); | ||
LoginMember loginMember = memberService.parseTokenAndGetMemberInfo(cookies); | ||
MemberResponse response = new MemberResponse(loginMember.getId(), loginMember.getName(), loginMember.getEmail()); | ||
return ResponseEntity.ok().body(response); | ||
} | ||
|
||
Comment on lines
+27
to
+47
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. 로그인 관련해서는 컨트롤러가 별도로 분리되는게 좋을 것 같아요! 회원 컨트롤러는 회원 매니징에 대해서만 갖는게 맞고 로그인은 인증인가의 영역이니까요! |
||
@PostMapping("/members") | ||
public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { | ||
MemberResponse member = memberService.createMember(memberRequest); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,51 @@ | ||
package roomescape.member; | ||
|
||
import jakarta.servlet.http.Cookie; | ||
import org.springframework.stereotype.Service; | ||
import roomescape.auth.LoginMember; | ||
import roomescape.token.TokenParser; | ||
import roomescape.token.TokenProvider; | ||
import roomescape.token.TokenResponse; | ||
|
||
@Service | ||
public class MemberService { | ||
private MemberDao memberDao; | ||
private final MemberDao memberDao; | ||
private final TokenProvider tokenProvider; | ||
private final TokenParser tokenParser; | ||
Comment on lines
+13
to
+14
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 MemberService(MemberDao memberDao) { | ||
public MemberService(final MemberDao memberDao, final TokenProvider tokenProvider, final TokenParser tokenParser) { | ||
this.memberDao = memberDao; | ||
this.tokenProvider = tokenProvider; | ||
this.tokenParser = tokenParser; | ||
} | ||
|
||
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 TokenResponse getAccessToken(String email, String password) { | ||
Member member = getMember(email, password); | ||
String token = tokenProvider.createAccessToken(member); | ||
return new TokenResponse(token); | ||
} | ||
|
||
public LoginMember parseTokenAndGetMemberInfo(Cookie[] cookies) { | ||
|
||
String 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. 토큰이 없는 것은 null이 더 명시적이지 않을까 하는 생각이 듭니다. |
||
for (Cookie cookie : cookies) { | ||
if (cookie.getName().equals("token")) | ||
token = cookie.getValue(); | ||
} | ||
|
||
if (token.isEmpty()) throw new IllegalArgumentException("로그인하지 않은 사용자입니다."); | ||
|
||
return tokenParser.paseMemberInfo(token); | ||
} | ||
Comment on lines
+33
to
+44
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. 쿠키는 웹 기술의 영역이기 때문에 인터셉터에서 쿠키에서 원하는 값을 꺼내고 서비스에 전달해주는 형태로 구현해보는 것은 어떨까요? 지금은 너무 비즈니스 영역까지 들어온 느낌이에요. 토큰 값이 쿠키에 있든 리퀘스트바디로 넘어오든 MemberService 입장에서는 알 필요 없고 회원에 대한 책임만 가지면 되지 않을까요? |
||
|
||
private Member getMember(String email, String password) { | ||
Member member = memberDao.findByEmailAndPassword(email, password); | ||
if (member == null) throw new IllegalArgumentException("등록되지 않은 사용자입니다."); | ||
return member; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
package roomescape.reservation; | ||
|
||
import org.springframework.stereotype.Service; | ||
import roomescape.auth.LoginMember; | ||
|
||
import java.util.List; | ||
|
||
|
@@ -12,12 +13,19 @@ public ReservationService(ReservationDao reservationDao) { | |
this.reservationDao = reservationDao; | ||
} | ||
|
||
public ReservationResponse save(ReservationRequest reservationRequest) { | ||
public ReservationResponse save(ReservationRequest reservationRequest, LoginMember loginMember) { | ||
reservationRequest = replaceNameIfEmpty(reservationRequest, loginMember); | ||
Reservation reservation = reservationDao.save(reservationRequest); | ||
|
||
return new ReservationResponse(reservation.getId(), reservationRequest.getName(), reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue()); | ||
} | ||
|
||
private ReservationRequest replaceNameIfEmpty(ReservationRequest request, LoginMember loginMember) { | ||
if (request.getName() == null || request.getName().isBlank()) { | ||
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을 미리 변수로 꺼내놓으면 두 번 호출하지 않아도 되겠죠? |
||
return new ReservationRequest(loginMember.getName(), request.getDate(), request.getTheme(), request.getTime()); | ||
} | ||
return request; | ||
} | ||
|
||
public void deleteById(Long id) { | ||
reservationDao.deleteById(id); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package roomescape.token; | ||
|
||
import io.jsonwebtoken.Claims; | ||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.security.Keys; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
import roomescape.auth.LoginMember; | ||
|
||
import java.util.Base64; | ||
|
||
@Component | ||
public class TokenParser { | ||
|
||
@Value("${roomescape.auth.jwt.secret}") | ||
private String secretKey; | ||
|
||
public LoginMember paseMemberInfo(String 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. 오타 있네요! |
||
Claims claims = Jwts.parserBuilder() | ||
.setSigningKey(Keys.hmacShaKeyFor(Base64.getDecoder().decode(secretKey))) | ||
.build() | ||
.parseClaimsJws(token) | ||
.getBody(); | ||
|
||
String name = claims.get("name", String.class); | ||
String email = claims.get("email", String.class); | ||
String role = claims.get("role", String.class); | ||
long id = Long.parseLong(claims.getSubject()); | ||
|
||
return new LoginMember(id, name, email, role); | ||
|
||
|
||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package roomescape.token; | ||
|
||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.SignatureAlgorithm; | ||
import io.jsonwebtoken.security.Keys; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
import roomescape.member.Member; | ||
|
||
import java.util.Base64; | ||
import java.util.Date; | ||
|
||
@Component | ||
public class TokenProvider { | ||
|
||
@Value("${roomescape.auth.jwt.secret}") | ||
private String secretKey; | ||
|
||
@Value("${roomescape.auth.jwt.token.expire-length}") | ||
private long expire; | ||
|
||
public String createAccessToken(Member member) { | ||
Date now = new Date(); | ||
Date validity = new Date(now.getTime() + expire); | ||
return Jwts.builder() | ||
.setSubject(Long.toString(member.getId())) | ||
.claim("name", member.getName()) | ||
.claim("role", member.getRole()) | ||
.claim("email", member.getEmail()) | ||
Comment on lines
+27
to
+29
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. 토큰에 너무 많은 정보가 들어가는 것 아닐까요? |
||
.setIssuedAt(now) | ||
.setExpiration(validity) | ||
.signWith( | ||
Keys.hmacShaKeyFor(Base64.getDecoder().decode(secretKey)), | ||
SignatureAlgorithm.HS256 | ||
) | ||
.compact(); | ||
} | ||
} |
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.
role은 enum인 것이 더 관리되기 쉽지 않을까요?