-
Notifications
You must be signed in to change notification settings - Fork 55
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 (인증)] 이준민 미션 제출합니다. #100
base: jermany17
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,55 @@ | ||
package roomescape; | ||
|
||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.security.Keys; | ||
import jakarta.servlet.http.Cookie; | ||
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 AdminInterceptor implements HandlerInterceptor { | ||
private final String SECRET_KEY = "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E="; | ||
private final MemberService memberService; | ||
|
||
public AdminInterceptor(MemberService memberService) { | ||
this.memberService = memberService; | ||
} | ||
|
||
@Override | ||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { | ||
String token = extractTokenFromCookie(request.getCookies()); | ||
if (token.isEmpty()) { | ||
response.setStatus(401); // Unauthorized | ||
return false; | ||
} | ||
|
||
// JWT 토큰에서 사용자 정보 추출 | ||
String role = Jwts.parserBuilder() | ||
.setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes())) | ||
.build() | ||
.parseClaimsJws(token) | ||
.getBody() | ||
.get("role", String.class); | ||
|
||
if (role == null || !role.equals("ADMIN")) { | ||
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. [Request Change] 누가 토큰을 만들 때 role을 admin이라고 소문자로 입력하면 실패하겠군요. |
||
response.setStatus(401); // Unauthorized | ||
return false; | ||
} | ||
|
||
return true; // ADMIN 권한이 있는 경우만 true 반환 | ||
} | ||
|
||
private String extractTokenFromCookie(Cookie[] cookies) { | ||
if (cookies != null) { | ||
for (Cookie cookie : cookies) { | ||
if (cookie.getName().equals("token")) { | ||
return cookie.getValue(); | ||
} | ||
} | ||
} | ||
return ""; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package roomescape; | ||
|
||
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 java.util.List; | ||
|
||
@Configuration | ||
public class WebConfig implements WebMvcConfigurer { | ||
private final LoginMemberArgumentResolver loginMemberArgumentResolver; | ||
private final AdminInterceptor adminInterceptor; | ||
|
||
public WebConfig(AdminInterceptor adminInterceptor, LoginMemberArgumentResolver loginMemberArgumentResolver) { | ||
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"); // /admin 경로에만 적용 | ||
} | ||
Comment on lines
+26
to
+30
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. [Request Change] 엥? Path를 설정할 때, 접근하려는 리소스를 계층적으로 나타내는 경우가 많습니다.
이런식으로 맨 첫 계층에는 그렇게 하기 위해서 스프링에서는 패스를 패턴으로 등록하는 것이 일반적입니다. |
||
} |
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 |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package roomescape.member; | ||
|
||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.security.Keys; | ||
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; | ||
|
||
@Component | ||
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { | ||
private final String secretKey = "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E="; | ||
private final MemberService memberService; | ||
|
||
public LoginMemberArgumentResolver(MemberService memberService) { | ||
this.memberService = memberService; | ||
} | ||
|
||
@Override | ||
public boolean supportsParameter(MethodParameter parameter) { | ||
return parameter.getParameterType().equals(LoginMember.class); | ||
} | ||
|
||
@Override | ||
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { | ||
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); | ||
String token = extractTokenFromCookie(request.getCookies()); | ||
if (token.isEmpty()) { | ||
return null; // 로그인되지 않은 사용자 | ||
} | ||
|
||
// JWT에서 사용자 정보 추출 | ||
Long memberId = Long.valueOf(Jwts.parserBuilder() | ||
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) | ||
.build() | ||
.parseClaimsJws(token) | ||
.getBody() | ||
.getSubject()); | ||
Comment on lines
+37
to
+42
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. [Request Change] JWT에서 사용자 인증 정보를 추출하는 과정이 여러곳에 존재할 수 있는데요.
이 두가지 역할을 확실히 구분하고, |
||
|
||
Member member = memberService.findById(memberId); | ||
return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole()); | ||
} | ||
|
||
private String extractTokenFromCookie(Cookie[] cookies) { | ||
if (cookies != null) { | ||
for (Cookie cookie : cookies) { | ||
if (cookie.getName().equals("token")) { | ||
return cookie.getValue(); | ||
} | ||
} | ||
} | ||
return ""; | ||
} | ||
Comment on lines
+28
to
+57
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. [Request Change] 인증 정보를 추출하는 곳이 ArgumentResolver와 Interceptor 두군데가 있는데요.
[고민 1] 두 방식을 비교하며 ArgumentResolver에서 인증정보가 옳지 않을 때 어떤 문제가 있을지?컨트롤러에서 LoginMember를 argument로 받겠다는것은 인증정보가 꼭 필요하다는 것입니다. [고민 2] 과연 인증 정보가 옳지 않은 모든 상황을 커버하고 있는가?제가 생각했을 땐, 인증정보가 옳지 않다고 생각할만한 지점은 여러가지 있어요.
이런 여러 상황에서 발생할 수 있는 문제들(혹은 예외)을 어떻게 하면 잘 관리해서, 인증 실패라고 잘 클라이언트에게 전달할 수 있을까요? |
||
} |
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.
[Request Change]
이 시크릿 키로 jwt 토큰의 서명정보를 입력하였군요!
이 시크릿 키를 모르면 임의로 jwt를 만들 수가 없겠네요!!
이 파일 line30에서 디코딩되는 토큰은 이 시크릿키를 알고 만들었다는 확실한 믿음이 생기네요.
시크릿키의 일치를 통해 토큰의 신뢰를 판단하는 만큼 해당 정보는 매우 중요한 정보입니다.
다른 멤버들의 PR을 둘러보시면, properties 파일에 시크릿 키를 선언해두고
실제 코드에는
@Value, @ConfigurationProperties
같은 어노테이션을 이용해 빈에 주입받아 사용하는 경우가 많습니다.그렇게 하는 이유는 여러가지가 있겠죠.
이러한 이유들로 따로 변수로 관리하는 경우가 많은데요.
그렇다면 그런 궁금증이 있을 수 있겠네요.
방법적인 부분은 추후에 고민해도 좋을 것 같아요. 다만, 로직적으로 관리할 영역과 프로젝트 설정 영역에 쓰는 변수들을 구분해서 관리하는 습관을 가져보면 좋겠어요.
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.
넵 감사합니다. 더 공부해보고 고민해보겠습니다.