-
Notifications
You must be signed in to change notification settings - Fork 0
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
Feat #14 인증 인가 로직 구현 #14
The head ref may contain hidden characters: "feat/#13/\uC778\uC99D-\uC778\uAC00-\uB85C\uC9C1-\uAD6C\uD604"
Changes from 39 commits
bf23d5e
5101c46
e456744
1faf269
23dc33b
afb8b27
4e7f5e2
b54c418
6fd82fc
0bb72a3
5f1469d
9eaddf7
1511b62
089b940
8f631db
80e2382
23baab2
e3cf22c
9171f96
c25c256
43fa66b
4870d6e
9d043c7
28f02ee
12fe042
a4b3ff9
3acbfd5
d388252
9359012
eb5d617
64bcea1
4e580c5
8bd4515
a697244
bc9fd01
bba74ad
5df37e0
8004176
e5ebaf3
fc9ea01
ca47e03
fb59c77
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,19 @@ | ||
package com.gachtaxi.global.auth.jwt.annotation; | ||
|
||
import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target(ElementType.PARAMETER) | ||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : id") | ||
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. CurrentMemberId 부분도 위 코멘트 내용과 동일합니다 ! 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. 리뷰 내용을 참고해 코드를 수정해봤는데 검토 부탁드립니다!! import static com.gachtaxi.global.auth.jwt.annotation.CurrentMemberId.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AuthenticationPrincipal(expression = "#this == '" + ANONYMOUS_USER + "' ? null : id")
public @interface CurrentMemberId {
String ANONYMOUS_USER = "anonymousUser";
} 제가 찾은 그나마 깔끔한 방법입니다. 위 방법은 하드코딩을 지양하고 상수화를 거쳤다는 장점이 있습니다. 혹시 더 나은 개선 방법이 있다면 알려주실 수 있을까요?? 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. anonymousUser를 상수화하는 방식 자체에 대해서는 긍정적으로 생각합니다. |
||
public @interface CurrentMemberId { | ||
/* | ||
* AuthenticationPrincipal의 id 필드를 반환 | ||
* 즉, JwtUserDetails의 id 필드를 반환 | ||
* JwtUserDetails의 id는 Userid | ||
* */ | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package com.gachtaxi.global.auth.jwt.authentication; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.gachtaxi.global.common.response.ApiResponse; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.security.web.access.AccessDeniedHandler; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.io.IOException; | ||
|
||
import static com.gachtaxi.global.auth.jwt.exception.JwtErrorMessage.JWT_TOKEN_FORBIDDEN; | ||
|
||
@Slf4j | ||
@Component | ||
public class CustomAccessDeniedHandler implements AccessDeniedHandler { | ||
|
||
private final static String LOG_FORMAT = "ExceptionClass: {}, Message: {}"; | ||
private final static String CONTENT_TYPE = "application/json"; | ||
private final static String CHAR_ENCODING = "UTF-8"; | ||
|
||
@Override | ||
public void handle(HttpServletRequest request, HttpServletResponse response, org.springframework.security.access.AccessDeniedException accessDeniedException) throws IOException, ServletException { | ||
setResponse(response); | ||
log.error(LOG_FORMAT, accessDeniedException.getClass().getSimpleName(), accessDeniedException.getMessage()); | ||
} | ||
|
||
private void setResponse(HttpServletResponse response) throws IOException { | ||
response.setStatus(HttpServletResponse.SC_FORBIDDEN); | ||
response.setContentType(CONTENT_TYPE); | ||
response.setCharacterEncoding(CHAR_ENCODING); | ||
|
||
String body = new ObjectMapper().writeValueAsString(ApiResponse.response(HttpStatus.FORBIDDEN, JWT_TOKEN_FORBIDDEN.getMessage())); | ||
response.getWriter().write(body); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package com.gachtaxi.global.auth.jwt.authentication; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.gachtaxi.global.auth.jwt.exception.JwtErrorMessage; | ||
import com.gachtaxi.global.common.response.ApiResponse; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.security.core.AuthenticationException; | ||
import org.springframework.security.web.AuthenticationEntryPoint; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.io.IOException; | ||
|
||
@Slf4j | ||
@Component | ||
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { | ||
|
||
private final static String LOG_FORMAT = "ExceptionClass: {}, Message: {}"; | ||
private final static String JWT_ERROR = "jwtError"; | ||
private final static String CONTENT_TYPE = "application/json"; | ||
private final static String CHAR_ENCODING = "UTF-8"; | ||
|
||
@Override | ||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { | ||
JwtErrorMessage jwtError = (JwtErrorMessage) request.getAttribute(JWT_ERROR); | ||
|
||
if (jwtError != null) { | ||
setResponse(response, jwtError.getMessage()); | ||
log.error(LOG_FORMAT, jwtError, jwtError.getMessage()); | ||
} else { | ||
setResponse(response, authException.getMessage()); | ||
log.error(LOG_FORMAT, authException.getClass().getSimpleName(), authException.getMessage()); | ||
} | ||
} | ||
|
||
private void setResponse(HttpServletResponse response, String message) throws IOException { | ||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); | ||
response.setContentType(CONTENT_TYPE); | ||
response.setCharacterEncoding(CHAR_ENCODING); | ||
|
||
String body = new ObjectMapper().writeValueAsString(ApiResponse.response(HttpStatus.UNAUTHORIZED, message)); | ||
response.getWriter().write(body); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.gachtaxi.global.auth.jwt.exception; | ||
|
||
import com.gachtaxi.global.common.exception.BaseException; | ||
|
||
import static com.gachtaxi.global.auth.jwt.exception.JwtErrorMessage.COOKIE_NOT_FOUND; | ||
import static org.springframework.http.HttpStatus.BAD_REQUEST; | ||
|
||
public class CookieNotFoundException extends BaseException { | ||
public CookieNotFoundException() { | ||
super(BAD_REQUEST, COOKIE_NOT_FOUND.getMessage()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.gachtaxi.global.auth.jwt.exception; | ||
|
||
import com.gachtaxi.global.common.exception.BaseException; | ||
|
||
import static com.gachtaxi.global.auth.jwt.exception.JwtErrorMessage.REDIS_NOT_FOUND; | ||
import static org.springframework.http.HttpStatus.UNAUTHORIZED; | ||
|
||
public class RefreshTokenNotFoundException extends BaseException { | ||
public RefreshTokenNotFoundException() { | ||
super(UNAUTHORIZED, REDIS_NOT_FOUND.getMessage()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.gachtaxi.global.auth.jwt.exception; | ||
|
||
import com.gachtaxi.global.common.exception.BaseException; | ||
|
||
import static com.gachtaxi.global.auth.jwt.exception.JwtErrorMessage.JWT_TOKEN_EXPIRED; | ||
import static org.springframework.http.HttpStatus.UNAUTHORIZED; | ||
|
||
public class TokenExpiredException extends BaseException { | ||
public TokenExpiredException() { | ||
super(UNAUTHORIZED, JWT_TOKEN_EXPIRED.getMessage()); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,12 @@ | ||
package com.gachtaxi.global.auth.jwt.exception; | ||
|
||
import com.gachtaxi.global.common.exception.BaseException; | ||
import static com.gachtaxi.global.auth.jwt.exception.JwtErrorMessage.JWT_TOKEN_UN_VALID; | ||
import static org.springframework.http.HttpStatus.BAD_REQUEST; | ||
|
||
import static com.gachtaxi.global.auth.jwt.exception.JwtErrorMessage.JWT_TOKEN_INVALID; | ||
import static org.springframework.http.HttpStatus.UNAUTHORIZED; | ||
|
||
public class TokenInvalidException extends BaseException { | ||
public TokenInvalidException() { | ||
super(BAD_REQUEST, JWT_TOKEN_UN_VALID.getMessage()); | ||
super(UNAUTHORIZED, JWT_TOKEN_INVALID.getMessage()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.gachtaxi.global.auth.jwt.exception; | ||
|
||
import com.gachtaxi.global.common.exception.BaseException; | ||
|
||
import static com.gachtaxi.global.auth.jwt.exception.JwtErrorMessage.USER_NOT_FOUND_EMAIL; | ||
import static org.springframework.http.HttpStatus.UNAUTHORIZED; | ||
|
||
public class UserEmailNotFoundException extends BaseException { | ||
public UserEmailNotFoundException() { | ||
super(UNAUTHORIZED, USER_NOT_FOUND_EMAIL.getMessage()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package com.gachtaxi.global.auth.jwt.filter; | ||
|
||
import com.gachtaxi.global.auth.jwt.user.JwtUserDetails; | ||
import com.gachtaxi.global.auth.jwt.util.JwtExtractor; | ||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.security.core.userdetails.UserDetails; | ||
import org.springframework.web.filter.OncePerRequestFilter; | ||
|
||
import java.io.IOException; | ||
import java.util.Optional; | ||
|
||
import static com.gachtaxi.global.auth.jwt.exception.JwtErrorMessage.JWT_TOKEN_EXPIRED; | ||
import static com.gachtaxi.global.auth.jwt.exception.JwtErrorMessage.JWT_TOKEN_NOT_FOUND; | ||
|
||
@RequiredArgsConstructor | ||
public class JwtAuthenticationFilter extends OncePerRequestFilter { | ||
|
||
private final JwtExtractor jwtExtractor; | ||
|
||
private final static String JWT_ERROR = "jwtError"; | ||
|
||
@Override | ||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { | ||
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. 현재 필터에서 JWT 토큰의 유무, 만기 일자만 검증을 하고 토큰의 유효성 검증은 빠져있는 것 같아요! 필터에서는 조금 더 명시적으로 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. 맞습니다! 이 메서드는 jwtToken의 유효성 검증 및 디코딩을 수행 후 Claims를 반환합니다! private Claims parseClaims(String token) {
try{
JwtParser parser = Jwts.parserBuilder()
.setSigningKey(key)
.build();
return parser.parseClaimsJws(token).getBody();
}catch (JwtException e){
throw new TokenInvalidException();
}
} 저는 토큰의 값을 꺼내는 전제 조건이 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. 그럼 해당 메서드에 유효성 검증이라는 의미가 들어나도 좋을 것 같아요! 어떻게 보면 추출의 역할은 이 메서드를 호출하는 쪽에서 진행하는 것이니 이 메서드의 책임은 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. 그리고 갑자기 든 생각인데, 현재 jwtExtractor에 구현된 추출 메서드 들은 매번 parseClaims를 거쳐야하는데, 이것이 조금 비 효율적일 수도 있겠다는 생각이 들었어요 가장 초기에 토큰 검증 후에 추출 추출 추출 이렇게 사용할 수 있는게 아니라 토큰 검증+추출 이다보니 id, email 등이 같이 필요할 때는 매번 검증을 할 필요는 없지 않나 라는 생각이 들었습니다! |
||
Optional<String> token = jwtExtractor.extractJwtToken(request); | ||
koreaioi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if (token.isEmpty()) { | ||
request.setAttribute(JWT_ERROR, JWT_TOKEN_NOT_FOUND); | ||
filterChain.doFilter(request, response); | ||
return; | ||
} | ||
koreaioi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
String accessToken = token.get(); | ||
|
||
if(jwtExtractor.isExpired(accessToken)){ | ||
request.setAttribute(JWT_ERROR, JWT_TOKEN_EXPIRED); | ||
filterChain.doFilter(request, response); | ||
return; | ||
} | ||
|
||
saveAuthentcation(accessToken); | ||
filterChain.doFilter(request, response); | ||
} | ||
|
||
private void saveAuthentcation(String token) { | ||
koreaioi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Long id = jwtExtractor.getId(token); | ||
String email = jwtExtractor.getEmail(token); | ||
String role = jwtExtractor.getRole(token); | ||
|
||
UserDetails userDetails = JwtUserDetails.of(id, email, role); | ||
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); | ||
SecurityContextHolder.getContext().setAuthentication(authentication); | ||
} | ||
} |
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를 직접 넘겨주기 보다는 @CookieValue, @RequestHeader 등의 어노테이션을 사용해서 쿠키를 가져올 수 있을 것 같아요!
저도 쿠키는 가져와보진 못했지만, 헤더에서 값을 빼올 때는 @RequestHeader를 사용해서 빼오는 것이 해당 컨트롤러에서 필요로 하는 값이 한 눈에 보여서 조은 것 같습니다