Skip to content

[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

Open
wants to merge 3 commits into
base: dpwls0125
Choose a base branch
from
Open
Show file tree
Hide file tree
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
27 changes: 27 additions & 0 deletions src/main/java/roomescape/auth/AdminRoleCheckInterceptor.java
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;
}
}
32 changes: 32 additions & 0 deletions src/main/java/roomescape/auth/LoginMember.java
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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

role은 enum인 것이 더 관리되기 쉽지 않을까요?


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;
}
}
35 changes: 35 additions & 0 deletions src/main/java/roomescape/auth/LoginMemberArgumentResolver.java
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);
}


}
14 changes: 14 additions & 0 deletions src/main/java/roomescape/auth/LoginRequest.java
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

record를 사용해봐도 좋을 것 같네요

33 changes: 33 additions & 0 deletions src/main/java/roomescape/config/WebConfig.java
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/**");
}
}
26 changes: 26 additions & 0 deletions src/main/java/roomescape/member/MemberController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Copy link

Choose a reason for hiding this comment

The 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);
Expand Down
38 changes: 36 additions & 2 deletions src/main/java/roomescape/member/MemberService.java
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
Copy link

Choose a reason for hiding this comment

The 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 = "";
Copy link

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

The 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
Expand Up @@ -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.auth.LoginMember;

import java.net.URI;
import java.util.List;
Expand All @@ -26,15 +27,15 @@ 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
|| reservationRequest.getTheme() == null
|| reservationRequest.getTime() == null) {
return ResponseEntity.badRequest().build();
}
ReservationResponse reservation = reservationService.save(reservationRequest);

ReservationResponse reservation = reservationService.save(reservationRequest, loginMember);
return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation);
}

Expand Down
7 changes: 7 additions & 0 deletions src/main/java/roomescape/reservation/ReservationRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ public class ReservationRequest {
private Long theme;
private Long time;

public ReservationRequest(final String name, final String date, final Long theme, final Long time) {
this.name = name;
this.date = date;
this.theme = theme;
this.time = time;
}

public String getName() {
return name;
}
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/roomescape/reservation/ReservationService.java
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;

Expand All @@ -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()) {
Copy link

Choose a reason for hiding this comment

The 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);
}
Expand Down
35 changes: 35 additions & 0 deletions src/main/java/roomescape/token/TokenParser.java
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) {
Copy link

Choose a reason for hiding this comment

The 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);


}

}
38 changes: 38 additions & 0 deletions src/main/java/roomescape/token/TokenProvider.java
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
Copy link

Choose a reason for hiding this comment

The 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();
}
}
Loading