-
Notifications
You must be signed in to change notification settings - Fork 78
[Spring MVC (인증)] 천희정 미션 제출합니다. #193
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: heejung72
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,58 @@ | ||
package roomescape.auth; | ||
|
||
import io.jsonwebtoken.Claims; | ||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.SignatureAlgorithm; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.util.Date; | ||
|
||
@Component | ||
public class JwtProvider { | ||
|
||
@Value("${roomescape.auth.jwt.secret}") | ||
private String SECRET_KEY; | ||
Comment on lines
+14
to
+15
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 static final long EXPIRATION_MS = 1000 * 60 * 60 * 24; | ||
|
||
public String generateToken(Long memberId, String email, String role) { | ||
return Jwts.builder() | ||
.setSubject(String.valueOf(memberId)) | ||
.claim("email", email) | ||
.claim("role", role) | ||
.setIssuedAt(new Date()) | ||
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS)) | ||
.signWith(SignatureAlgorithm.HS256, SECRET_KEY) | ||
.compact(); | ||
} | ||
|
||
public boolean validateToken(String token) { | ||
try { | ||
Jwts.parser() | ||
.setSigningKey(SECRET_KEY) | ||
.parseClaimsJws(token); | ||
return true; | ||
} catch (Exception e) { | ||
return false; | ||
} | ||
} | ||
Comment on lines
+30
to
+39
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. 일반적으로 validate~ 라는 네이밍을 가진 메서드의 경우 |
||
|
||
public String getRoleFromToken(String token) { | ||
Claims claims = Jwts.parser() | ||
.setSigningKey(SECRET_KEY) | ||
.parseClaimsJws(token) | ||
.getBody(); | ||
|
||
return claims.get("role", String.class); | ||
} | ||
|
||
public String getEmailFromToken(String token) { | ||
Claims claims = Jwts.parser() | ||
.setSigningKey(SECRET_KEY) | ||
.parseClaimsJws(token) | ||
.getBody(); | ||
|
||
return claims.get("email", String.class); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package roomescape.config; | ||
|
||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; | ||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||
import roomescape.interceptor.AdminInterceptor; | ||
|
||
@Configuration | ||
public class WebConfig implements WebMvcConfigurer { | ||
|
||
private final AdminInterceptor adminInterceptor; | ||
|
||
@Autowired | ||
public WebConfig(AdminInterceptor adminInterceptor) { | ||
this.adminInterceptor = adminInterceptor; | ||
} | ||
Comment on lines
+14
to
+17
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. 스프링 4.3 버전 이후부터는 단일 생성자에 대해서는 |
||
|
||
@Override | ||
public void addInterceptors(InterceptorRegistry registry) { | ||
registry.addInterceptor(adminInterceptor) | ||
.addPathPatterns("/admin/**"); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package roomescape.controller; | ||
|
||
import jakarta.servlet.http.Cookie; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.*; | ||
import roomescape.auth.JwtProvider; | ||
import roomescape.login.LoginRequest; | ||
import roomescape.member.Member; | ||
import roomescape.member.MemberDao; | ||
|
||
@RestController | ||
public class LoginController { | ||
|
||
private final MemberDao memberDao; | ||
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. 생각해보기) 우리가 지금 계층형 구조(흔히 레이어드 아키텍쳐라고 하죠)를 채택하고 사용하고 있는데, 표현 계층인 컨트롤러가 서비스를 건너뛰고 바로 DAO를 사용하는 것에는 어떤 문제가 있을까요? 물론 이 문제에 대해서는 의존 방향이 아래로 향하기만 하면 문제가 없다는 의견도 있기 때문에 반드시 DAO를 여기서 쓰는 것이 틀렸다고 리뷰드리는 것은 아닙니다! |
||
private final JwtProvider jwtProvider; | ||
|
||
public LoginController(MemberDao memberDao, JwtProvider jwtProvider) { | ||
this.memberDao = memberDao; | ||
this.jwtProvider = jwtProvider; | ||
} | ||
|
||
@PostMapping("/login") | ||
public ResponseEntity<Void> login(@RequestBody LoginRequest loginRequest, HttpServletResponse response) { | ||
try { | ||
Member member = memberDao.findByEmailAndPassword( | ||
loginRequest.getEmail(), | ||
loginRequest.getPassword() | ||
); | ||
|
||
String token = jwtProvider.generateToken( | ||
member.getId(), | ||
member.getEmail(), | ||
member.getRole() | ||
); | ||
|
||
Cookie cookie = new Cookie("token", token); | ||
cookie.setHttpOnly(true); | ||
cookie.setPath("/"); | ||
response.addCookie(cookie); | ||
|
||
return ResponseEntity.ok().build(); | ||
} catch (Exception e) { | ||
return ResponseEntity.status(401).build(); | ||
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. 401 Unauthorized 는 |
||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package roomescape.interceptor; | ||
|
||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.servlet.HandlerInterceptor; | ||
import roomescape.auth.JwtProvider; | ||
|
||
@Component | ||
public class AdminInterceptor implements HandlerInterceptor { | ||
|
||
private final JwtProvider jwtProvider; | ||
|
||
public AdminInterceptor(JwtProvider jwtProvider) { | ||
this.jwtProvider = jwtProvider; | ||
} | ||
|
||
@Override | ||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { | ||
String token = extractTokenFromCookies(request); | ||
|
||
if (token == null || !jwtProvider.validateToken(token) || !"ADMIN".equals(jwtProvider.getRoleFromToken(token))) { | ||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private String extractTokenFromCookies(HttpServletRequest request) { | ||
if (request.getCookies() == null) return null; | ||
|
||
for (var cookie : request.getCookies()) { | ||
Comment on lines
+31
to
+33
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. 사소하지만 |
||
if ("token".equals(cookie.getName())) { | ||
return cookie.getValue(); | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package roomescape.login; | ||
|
||
public class LoginMember { | ||
private final Long id; | ||
private final String name; | ||
private final String email; | ||
private final 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,65 @@ | ||
package roomescape.login; | ||
|
||
import io.jsonwebtoken.Claims; | ||
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.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.Member; | ||
import roomescape.member.MemberDao; | ||
|
||
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { | ||
|
||
private final String secretKey; | ||
private final MemberDao memberDao; | ||
|
||
public LoginMemberArgumentResolver(String secretKey, MemberDao memberDao) { | ||
this.secretKey = secretKey; | ||
this.memberDao = memberDao; | ||
} | ||
|
||
@Override | ||
public boolean supportsParameter(MethodParameter parameter) { | ||
return LoginMember.class.isAssignableFrom(parameter.getParameterType()); | ||
} | ||
|
||
@Override | ||
public Object resolveArgument(MethodParameter parameter, | ||
ModelAndViewContainer mavContainer, | ||
NativeWebRequest webRequest, | ||
WebDataBinderFactory binderFactory) { | ||
|
||
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); | ||
if (request == null || request.getCookies() == null) { | ||
return null; | ||
} | ||
|
||
String token = ""; | ||
for (Cookie cookie : request.getCookies()) { | ||
if ("token".equals(cookie.getName())) { | ||
token = cookie.getValue(); | ||
break; | ||
} | ||
} | ||
if (token.isBlank()) { | ||
return null; | ||
} | ||
|
||
Claims claims = Jwts.parserBuilder() | ||
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) | ||
.build() | ||
.parseClaimsJws(token) | ||
.getBody(); | ||
|
||
Long memberId = Long.valueOf(claims.getSubject()); | ||
Member member = memberDao.findById(memberId) | ||
.orElseThrow(() -> new IllegalStateException("회원을 찾을 수 없습니다")); | ||
|
||
return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package roomescape.login; | ||
|
||
public class LoginRequest { | ||
private String email; | ||
private String password; | ||
|
||
public String getEmail() { | ||
return email; | ||
} | ||
|
||
public String getPassword() { | ||
return password; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,28 @@ | ||
package roomescape.member; | ||
|
||
import org.springframework.stereotype.Service; | ||
import roomescape.auth.JwtProvider; | ||
|
||
@Service | ||
public class MemberService { | ||
private MemberDao memberDao; | ||
private final MemberDao memberDao; | ||
private final JwtProvider jwtProvider; | ||
|
||
public MemberService(MemberDao memberDao) { | ||
public MemberService(MemberDao memberDao, JwtProvider jwtProvider) { | ||
this.memberDao = memberDao; | ||
this.jwtProvider = jwtProvider; | ||
} | ||
|
||
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 Member findByToken(String token) { | ||
String email = jwtProvider.getEmailFromToken(token); | ||
if (email == null) { | ||
return null; | ||
} | ||
return memberDao.findByEmail(email).orElse(null); | ||
} | ||
Comment on lines
+21
to
+27
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. Member 라는 도메인만 보면 토큰이라는 정보를 모르는데요! Member가 토큰을 받아서 그 토큰 안에 이메일이라는 것이 있다는 것까지 알고 이메일을 파싱해서 조회해도 괜찮은걸까요? 회원이라는 도메인이 어디까지 책임을 가져야 하는가에 대해서 고민해보면 좋을 것 같아요! |
||
} |
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.
요거 추가했는데
.DS_Store
은 PR에 들어있네요?