-
Notifications
You must be signed in to change notification settings - Fork 1
Feature/42 Jwt (Auth, Filter, Exception) 기능 #50
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
Changes from 16 commits
e9e78df
35a6795
4ecb1a4
7cd389e
830d42f
50c9db8
3b0586a
3063e8a
f92c8f2
a7c91e7
9271e29
aa3ab9e
e26c565
d620164
2cf3323
375cf9e
0f38b46
fcf21c6
45409b7
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,15 @@ | ||
| package com.somemore.auth.jwt.exception; | ||
|
|
||
| import lombok.Getter; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Getter | ||
| @RequiredArgsConstructor | ||
| public enum JwtErrorType { | ||
| MISSING_TOKEN("JWT 토큰이 없습니다."), | ||
| INVALID_TOKEN("JWT 서명이 유효하지 않습니다."), | ||
| EXPIRED_TOKEN("JWT 토큰이 만료되었습니다."), | ||
| UNKNOWN_ERROR("알 수 없는 JWT 처리 오류가 발생했습니다."); | ||
|
|
||
| private final String message; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.somemore.auth.jwt.exception; | ||
|
|
||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| public class JwtException extends RuntimeException { | ||
| private final JwtErrorType errorType; | ||
|
|
||
| public JwtException(JwtErrorType errorType) { | ||
| super(errorType.getMessage()); | ||
| this.errorType = errorType; | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| package com.somemore.auth.jwt.filter; | ||
|
|
||
| import com.somemore.auth.UserRole; | ||
| import com.somemore.auth.jwt.domain.EncodedToken; | ||
| import com.somemore.auth.jwt.exception.JwtErrorType; | ||
| import com.somemore.auth.jwt.exception.JwtException; | ||
| import com.somemore.auth.jwt.usecase.JwtUseCase; | ||
| import io.jsonwebtoken.Claims; | ||
| import jakarta.servlet.FilterChain; | ||
| import jakarta.servlet.ServletException; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.security.core.Authentication; | ||
| import org.springframework.security.core.authority.SimpleGrantedAuthority; | ||
| import org.springframework.security.core.context.SecurityContextHolder; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.filter.OncePerRequestFilter; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.List; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Slf4j | ||
| @Component | ||
| public class JwtAuthFilter extends OncePerRequestFilter { | ||
|
|
||
| private final JwtUseCase jwtUseCase; | ||
|
|
||
| @Override | ||
| protected boolean shouldNotFilter(HttpServletRequest request) { | ||
| return true; // 개발 중 모든 요청 허용 | ||
| // return httpServletRequest.getRequestURI().contains("token"); | ||
| } | ||
|
|
||
| @Override | ||
| protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { | ||
| EncodedToken accessToken = getAccessToken(request); | ||
| jwtUseCase.processAccessToken(accessToken, response); | ||
|
|
||
| Claims claims = jwtUseCase.getClaims(accessToken); | ||
| Authentication auth = createAuthenticationToken(claims, accessToken); | ||
|
|
||
| SecurityContextHolder.getContext().setAuthentication(auth); | ||
| filterChain.doFilter(request, response); | ||
| } | ||
|
|
||
| private JwtAuthenticationToken createAuthenticationToken(Claims claims, EncodedToken accessToken) { | ||
| String userId = claims.get("id", String.class); | ||
| UserRole role = claims.get("role", UserRole.class); | ||
|
|
||
| return new JwtAuthenticationToken( | ||
| userId, | ||
| accessToken, | ||
| List.of(new SimpleGrantedAuthority(role.name())) | ||
| ); | ||
| } | ||
|
|
||
| private EncodedToken getAccessToken(HttpServletRequest request) { | ||
| String accessToken = request.getHeader("Authorization"); | ||
| if (accessToken == null || accessToken.isEmpty()) { | ||
| throw new JwtException(JwtErrorType.MISSING_TOKEN); | ||
| } | ||
| return new EncodedToken(accessToken); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package com.somemore.auth.jwt.filter; | ||
|
|
||
| import org.springframework.security.authentication.AbstractAuthenticationToken; | ||
| import org.springframework.security.core.GrantedAuthority; | ||
|
|
||
| import java.io.Serializable; | ||
| import java.util.Collection; | ||
|
|
||
| public class JwtAuthenticationToken extends AbstractAuthenticationToken { | ||
| private final Serializable principal; | ||
| private final transient Object credentials; | ||
|
|
||
|
Collaborator
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.
Collaborator
Author
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 JwtAuthenticationToken(Serializable principal, | ||
| Object credentials, | ||
| Collection<? extends GrantedAuthority> authorities) { | ||
| super(authorities); | ||
| this.principal = principal; | ||
| this.credentials = credentials; | ||
| setAuthenticated(true); | ||
| } | ||
|
|
||
| @Override | ||
| public Object getCredentials() { | ||
| return credentials; | ||
| } | ||
|
|
||
| @Override | ||
| public Object getPrincipal() { | ||
| return principal; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package com.somemore.auth.jwt.filter; | ||
|
|
||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.somemore.auth.jwt.exception.JwtException; | ||
| import jakarta.servlet.FilterChain; | ||
| import jakarta.servlet.ServletException; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.http.ProblemDetail; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.filter.OncePerRequestFilter; | ||
|
|
||
| import java.io.IOException; | ||
| import java.net.URI; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Slf4j | ||
| @Component | ||
| public class JwtExceptionFilter extends OncePerRequestFilter { | ||
|
|
||
| private final ObjectMapper objectMapper; | ||
|
|
||
| @Override | ||
| protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { | ||
| try { | ||
| filterChain.doFilter(request, response); | ||
| } catch (JwtException e) { | ||
| ProblemDetail problemDetail = buildUnauthorizedProblemDetail(e); | ||
| configureUnauthorizedResponse(response); | ||
|
|
||
| objectMapper.writeValue(response.getWriter(), problemDetail); | ||
| } | ||
| } | ||
|
|
||
| private void configureUnauthorizedResponse(HttpServletResponse response) { | ||
| response.setStatus(HttpStatus.UNAUTHORIZED.value()); | ||
| response.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE); | ||
| response.setCharacterEncoding("UTF-8"); | ||
| } | ||
|
|
||
| private ProblemDetail buildUnauthorizedProblemDetail(JwtException e) { | ||
| ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, e.getMessage()); | ||
| problemDetail.setTitle("Authentication Error"); | ||
| problemDetail.setType(URI.create("http://프론트엔드주소/errors/unauthorized")); | ||
| problemDetail.setProperty("timestamp", System.currentTimeMillis()); | ||
|
Collaborator
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.
Collaborator
Author
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 problemDetail; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ | |
| import javax.crypto.SecretKey; | ||
| import java.time.Instant; | ||
| import java.util.Date; | ||
| import java.util.UUID; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
|
|
@@ -23,9 +24,11 @@ public EncodedToken generateToken(String userId, String role, TokenType tokenTyp | |
| Claims claims = buildClaims(userId, role); | ||
| Instant now = Instant.now(); | ||
| Instant expiration = now.plusMillis(tokenType.getPeriod()); | ||
| String uniqueId = UUID.randomUUID().toString(); // JTI | ||
|
Collaborator
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.
Collaborator
Author
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. https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7 여기 확인해 보시면, jwt에 유니크한 값을 삽입해서 동일 시점, 동일 클레임으로 생성된 토큰 간의 차이점을 만드는 것이라고 확인하실 수 있습니다. 테스트하면서 발견해서 추가했습니다! |
||
|
|
||
| return new EncodedToken(Jwts.builder() | ||
| .claims(claims) | ||
| .id(uniqueId) | ||
| .issuedAt(Date.from(now)) | ||
| .expiration(Date.from(expiration)) | ||
| .signWith(secretKey, ALGORITHM) | ||
|
|
||
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.
getAccessToken()메서드가createAuthenticationToken()보다 먼저오는게 좋아보여요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.
안목이 있으십니다.