-
Notifications
You must be signed in to change notification settings - Fork 5
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
[Feature] 리프레시 토큰 적용 #61
Changes from 152 commits
1546f99
144d327
90c046e
53dd77d
346e2f4
359bdfc
ae8474a
ae5f932
4f7d7bb
1589120
df9fb6f
40b5e66
bf8c87d
22eb891
7526e10
828ce81
84d1960
254fd3c
ef1dab4
2ed7971
8c5430b
57140b8
e3a7f98
81ccc15
c4ae87d
2e1b0f4
30e8538
1c981b7
f671943
1b00ce0
b2f77f0
bcaa1b7
4455fa2
995c00c
589430c
dfc7426
6ce4ea9
8469c4d
a35e16d
ea6abc8
baa9a27
ba16a33
a69324d
c061cf6
50aaadd
e67d005
39d530e
88362c3
735c1fd
ae92004
f5d0675
ae45f3f
329f437
4170481
42bc41e
1634b97
b55affc
10f8910
b828b0f
d89708f
e1295c5
a9ec1e9
1fec4c6
77fb111
311d50a
1a9334f
d894911
f50205c
9940bcf
2848492
f32d7a4
c45f08d
d5bb387
710d792
6498521
f7493f1
79ac9b0
150cbb1
c3b9ef5
c341be8
8578254
2e310a9
bf53dda
fd109a7
485ac63
96b5489
170f06d
9355f1b
d238a8d
646b865
0e52693
2c3c5fa
b8257b7
f0e20a2
8a70c30
310432e
579ef5b
6d689f0
87d5374
1a023c8
c54cbcf
edfacf1
e118078
62c9f77
bb4eed3
94e9431
4e483a9
cc61757
2d29576
e55e473
c569a8b
929dc1f
bf00af6
c22c3ae
102b1ff
6781e4c
dbc4470
412f487
dc64ecf
5a3ccc3
3d36629
404eba5
9b36071
95d4e2f
991fe62
cdd9462
e7d55f3
e14c8df
cabebe5
0cd8d93
1ed2946
cd63b5e
80b6fc9
0490de9
d582bd9
5deeca0
b3638a3
f0a99c5
f692feb
84a20ea
2a9ac91
a1ef5dd
23a2be5
971b048
81d408f
584a812
c34bbc4
28233f3
0fc65c3
9a7cec5
67de30f
f7db827
5f8aabd
12616e0
b2d37bf
d527bbd
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,59 @@ | ||
package dbdr.global.configuration; | ||
|
||
|
||
import io.lettuce.core.ClientOptions; | ||
import io.lettuce.core.SocketOptions; | ||
import java.time.Duration; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.data.redis.connection.RedisConnectionFactory; | ||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration; | ||
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; | ||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; | ||
import org.springframework.data.redis.core.RedisTemplate; | ||
import org.springframework.data.redis.serializer.StringRedisSerializer; | ||
|
||
@Configuration | ||
public class RedisConfig { | ||
|
||
@Value("${spring.data.redis.host}") | ||
private String host; | ||
|
||
@Value("${spring.data.redis.port}") | ||
private int port; | ||
|
||
@Bean | ||
public RedisConnectionFactory redisConnectionFactory() { | ||
RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(); | ||
redisConfig.setHostName(host); | ||
redisConfig.setPort(port); | ||
return new LettuceConnectionFactory(redisConfig, clientConfig()); | ||
} | ||
|
||
@Bean | ||
public LettuceClientConfiguration clientConfig() { | ||
return LettuceClientConfiguration.builder() | ||
// 서버 배포시 ssl 사용으로 돌려야함 | ||
//.useSsl() | ||
//.and() | ||
.clientOptions( | ||
ClientOptions.builder() | ||
.socketOptions(SocketOptions.builder() | ||
.connectTimeout(Duration.ofSeconds(10)) // 연결 타임아웃 설정 | ||
.build() | ||
) | ||
.build() | ||
) | ||
.build(); | ||
} | ||
|
||
@Bean | ||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { | ||
RedisTemplate<String, Object> template = new RedisTemplate<>(); | ||
template.setConnectionFactory(connectionFactory); | ||
template.setKeySerializer(new StringRedisSerializer()); | ||
template.setValueSerializer(new StringRedisSerializer()); | ||
return template; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package dbdr.global.util.api; | ||
|
||
public class JwtUtils { | ||
pykido marked this conversation as resolved.
Show resolved
Hide resolved
|
||
public static final String ISSUER = "CareBridge"; | ||
public static final String TOKEN_PREFIX = "Bearer "; | ||
public static final long REFRESH_TOKEN_EXPIRATION_TIME = 60 * 60 * 24 * 30L; // 30일 | ||
public static final long ACCESS_TOKEN_EXPIRATION_TIME = 60 * 60; // 1시간 | ||
|
||
private JwtUtils() { | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package dbdr.security.config; | ||
|
||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import dbdr.global.util.api.ApiUtils; | ||
import io.jsonwebtoken.ExpiredJwtException; | ||
import io.jsonwebtoken.JwtException; | ||
import io.jsonwebtoken.MalformedJwtException; | ||
import io.jsonwebtoken.UnsupportedJwtException; | ||
import io.jsonwebtoken.security.SignatureException; | ||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import java.io.IOException; | ||
import lombok.NonNull; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.filter.OncePerRequestFilter; | ||
|
||
@Component | ||
@Slf4j | ||
public class ExceptionHandlingFilter extends OncePerRequestFilter { | ||
|
||
private final ObjectMapper objectMapper = new ObjectMapper(); | ||
|
||
@Override | ||
protected void doFilterInternal(@NonNull HttpServletRequest request, | ||
@NonNull HttpServletResponse response, | ||
@NonNull FilterChain filterChain) throws ServletException, IOException { | ||
try { | ||
filterChain.doFilter(request, response); | ||
} catch (Exception ex) { | ||
handleException(request, response, ex); | ||
} | ||
} | ||
|
||
private void handleException(HttpServletRequest request, HttpServletResponse response, Exception ex) | ||
throws IOException { | ||
ApiUtils.ApiResult<?> apiResult; | ||
HttpStatus status = HttpStatus.UNAUTHORIZED; | ||
|
||
if (ex instanceof ExpiredJwtException) { | ||
apiResult = ApiUtils.error(status, "토큰이 만료되었습니다."); | ||
} else if (ex instanceof UnsupportedJwtException) { | ||
apiResult = ApiUtils.error(status, "지원하지 않는 토큰 형식입니다."); | ||
} else if (ex instanceof MalformedJwtException) { | ||
apiResult = ApiUtils.error(status, "토큰의 형식이 올바르지 않습니다."); | ||
} else if (ex instanceof SignatureException || ex instanceof SecurityException) { | ||
apiResult = ApiUtils.error(status, "토큰의 서명이 유효하지 않습니다."); | ||
} else if (ex instanceof IllegalArgumentException) { | ||
apiResult = ApiUtils.error(status, "토큰이 제공되지 않았습니다."); | ||
} else if (ex instanceof JwtException) { | ||
apiResult = ApiUtils.error(status, "유효하지 않은 토큰입니다."); | ||
} else { | ||
status = HttpStatus.INTERNAL_SERVER_ERROR; | ||
apiResult = ApiUtils.error(status, "서버 오류가 발생했습니다."); | ||
} | ||
|
||
log.error("Security Exception 발생: [{}] {}", request.getRequestURI(), ex.getMessage()); | ||
|
||
response.setStatus(status.value()); | ||
response.setContentType("application/json"); | ||
response.setCharacterEncoding("UTF-8"); | ||
|
||
String jsonResponse = objectMapper.writeValueAsString(apiResult); | ||
response.getWriter().write(jsonResponse); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,11 @@ | ||
package dbdr.security.config; | ||
|
||
import dbdr.security.service.JwtProvider; | ||
import io.jsonwebtoken.ExpiredJwtException; | ||
import io.jsonwebtoken.JwtException; | ||
import io.jsonwebtoken.MalformedJwtException; | ||
import io.jsonwebtoken.UnsupportedJwtException; | ||
import io.jsonwebtoken.security.SignatureException; | ||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
|
@@ -20,17 +25,21 @@ public class JwtFilter extends OncePerRequestFilter { | |
|
||
@Override | ||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, | ||
FilterChain filterChain) throws ServletException, IOException { | ||
FilterChain filterChain) throws ServletException, IOException { | ||
|
||
String token = request.getHeader("Authorization"); | ||
String token = jwtProvider.extractToken(request); | ||
|
||
if (token != null) { | ||
try { | ||
//jwtProvider.validateToken(token); | ||
Authentication authentication = jwtProvider.getAuthentication(token); | ||
SecurityContextHolder.getContext().setAuthentication(authentication); | ||
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | | ||
SignatureException | SecurityException | IllegalArgumentException ex) { | ||
throw ex; | ||
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. ex보단 에러명을 지정해서 던져주는 건 어떠신가요? 원래 저희가 사용하던 ApplicationError 대신 이걸 사용하신 이유가 있을까요? 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. 토큰 검증에서 발생하는 오류는 저희가 직접 던지는 오류가 아니에요! |
||
} catch (Exception e) { | ||
log.debug("토큰 유저 정보 추출 실패 : {}", e.getMessage()); | ||
throw new JwtException("유효하지 않은 토큰입니다."); | ||
} | ||
} | ||
|
||
|
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.
10초로 설정하신 이유가 있을까요? 그냥 궁금증입니다.
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.
보통 api 연결 테스트를 10초로 하더라구요
별 이유는 없습니다...ㅋㅋㅋㅋ