Skip to content

Commit a39f148

Browse files
authored
️♻️ refactor: 시큐리티 구조 개선 (#73)
* refactor: 시큐리티 구조개선, 예외처리 개선 * feat: 임시로 예외 세부내용을 처리하도록 변경
1 parent cbd300f commit a39f148

13 files changed

+164
-84
lines changed

src/main/java/com/sponus/sponusbe/auth/jwt/exception/CustomExpiredJwtException.java

-8
This file was deleted.

src/main/java/com/sponus/sponusbe/auth/jwt/exception/CustomMalformedException.java

-8
This file was deleted.

src/main/java/com/sponus/sponusbe/auth/jwt/exception/CustomNoTokenException.java

-8
This file was deleted.

src/main/java/com/sponus/sponusbe/auth/jwt/exception/CustomSignatureException.java

-8
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.sponus.sponusbe.auth.jwt.exception;
2+
3+
import java.io.IOException;
4+
5+
import org.springframework.http.HttpStatus;
6+
import org.springframework.security.access.AccessDeniedException;
7+
import org.springframework.security.web.access.AccessDeniedHandler;
8+
import org.springframework.stereotype.Component;
9+
10+
import com.sponus.sponusbe.auth.jwt.util.HttpResponseUtil;
11+
12+
import jakarta.servlet.http.HttpServletRequest;
13+
import jakarta.servlet.http.HttpServletResponse;
14+
import lombok.extern.slf4j.Slf4j;
15+
16+
/**
17+
* 인증된 사용자가 필요한 권한없이 접근하려고 할 때 발생하는 예외 처리
18+
*/
19+
@Slf4j
20+
@Component
21+
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
22+
23+
@Override
24+
public void handle(HttpServletRequest request, HttpServletResponse response,
25+
AccessDeniedException accessDeniedException) throws IOException {
26+
log.warn("Access Denied: ", accessDeniedException);
27+
28+
HttpResponseUtil.setErrorResponse(response, HttpStatus.FORBIDDEN,
29+
SecurityErrorCode.FORBIDDEN.getErrorResponse());
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.sponus.sponusbe.auth.jwt.exception;
2+
3+
import java.io.IOException;
4+
5+
import org.springframework.http.HttpStatus;
6+
import org.springframework.security.core.AuthenticationException;
7+
import org.springframework.security.web.AuthenticationEntryPoint;
8+
import org.springframework.stereotype.Component;
9+
10+
import com.sponus.sponusbe.auth.jwt.util.HttpResponseUtil;
11+
import com.sponus.sponusbe.global.common.ApiResponse;
12+
13+
import jakarta.servlet.http.HttpServletRequest;
14+
import jakarta.servlet.http.HttpServletResponse;
15+
import lombok.extern.slf4j.Slf4j;
16+
17+
/**
18+
* 사용자가 인증되지 않은 상태에서 접근하려고 할 때 발생하는 예외 처리
19+
*/
20+
@Slf4j
21+
@Component
22+
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
23+
24+
@Override
25+
public void commence(HttpServletRequest request, HttpServletResponse response,
26+
AuthenticationException authException)
27+
throws IOException {
28+
HttpStatus httpStatus;
29+
ApiResponse<String> errorResponse;
30+
31+
log.error(">>>>>> AuthenticationException: ", authException);
32+
httpStatus = HttpStatus.UNAUTHORIZED;
33+
errorResponse = ApiResponse.onFailure(
34+
SecurityErrorCode.UNAUTHORIZED.getCode(),
35+
SecurityErrorCode.UNAUTHORIZED.getMessage(),
36+
authException.getMessage());
37+
38+
HttpResponseUtil.setErrorResponse(response, httpStatus, errorResponse);
39+
}
40+
}

src/main/java/com/sponus/sponusbe/auth/jwt/exception/SecurityCustomException.java

+11
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,20 @@
33
import com.sponus.sponusbe.global.common.BaseErrorCode;
44
import com.sponus.sponusbe.global.common.exception.CustomException;
55

6+
import lombok.Getter;
7+
8+
@Getter
69
public class SecurityCustomException extends CustomException {
710

11+
private final Throwable cause;
12+
813
public SecurityCustomException(BaseErrorCode errorCode) {
914
super(errorCode);
15+
this.cause = null;
16+
}
17+
18+
public SecurityCustomException(BaseErrorCode errorCode, Throwable cause) {
19+
super(errorCode);
20+
this.cause = cause;
1021
}
1122
}

src/main/java/com/sponus/sponusbe/auth/jwt/exception/SecurityErrorCode.java

+8-7
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212
@AllArgsConstructor
1313
public enum SecurityErrorCode implements BaseErrorCode {
1414

15-
INVALID_TOKEN(HttpStatus.BAD_REQUEST, "4001", "유효하지 않은 형식의 토큰입니다."),
16-
TOKEN_EXCEPTION(HttpStatus.UNAUTHORIZED, "4011", "토큰 처리 중 예외가 발생했습니다."),
17-
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "4012", "권한이 없습니다."),
18-
TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "4013", "만료된 토큰입니다."),
19-
SIGNATURE_ERROR(HttpStatus.UNAUTHORIZED, "4014", "무결하지 않은 토큰입니다."),
20-
TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "4015", "토큰이 존재하지 않습니다."),
21-
INTERNAL_SECURITY_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "5001", "서버 에러가 발생했습니다. 관리자에게 문의해주세요.");
15+
INVALID_TOKEN(HttpStatus.BAD_REQUEST, "SEC4001", "잘못된 형식의 토큰입니다."),
16+
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "SEC4010", "인증이 필요합니다."),
17+
TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "SEC4011", "토큰이 만료되었습니다."),
18+
TOKEN_SIGNATURE_ERROR(HttpStatus.UNAUTHORIZED, "SEC4012", "토큰이 위조되었거나 손상되었습니다."),
19+
FORBIDDEN(HttpStatus.FORBIDDEN, "SEC4030", "권한이 없습니다."),
20+
TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "SEC4041", "토큰이 존재하지 않습니다."),
21+
INTERNAL_SECURITY_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "SEC5000", "인증 처리 중 서버 에러가 발생했습니다."),
22+
INTERNAL_TOKEN_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "SEC5001", "토큰 처리 중 서버 에러가 발생했습니다.");
2223

2324
private final HttpStatus httpStatus;
2425
private final String code;

src/main/java/com/sponus/sponusbe/auth/jwt/filter/JwtFilter.java src/main/java/com/sponus/sponusbe/auth/jwt/filter/JwtAuthenticationFilter.java

+7-9
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import lombok.RequiredArgsConstructor;
2828

2929
@RequiredArgsConstructor
30-
public class JwtFilter extends OncePerRequestFilter {
30+
public class JwtAuthenticationFilter extends OncePerRequestFilter {
3131

3232
private final JwtUtil jwtUtil;
3333
private final RedisUtil redisUtil;
@@ -51,8 +51,7 @@ protected void doFilterInternal(
5151
}
5252

5353
// logout 처리된 accessToken
54-
if (redisUtil.get(accessToken) != null &&
55-
redisUtil.get(accessToken).equals("logout")) {
54+
if (redisUtil.get(accessToken) != null && redisUtil.get(accessToken).equals("logout")) {
5655
logger.info("[*] Logout accessToken");
5756
filterChain.doFilter(cachedHttpServletRequest, response);
5857
return;
@@ -63,7 +62,6 @@ protected void doFilterInternal(
6362
filterChain.doFilter(cachedHttpServletRequest, response);
6463
} catch (ExpiredJwtException e) {
6564
logger.warn("[*] case : accessToken Expired");
66-
6765
// accessToken 만료 시 Body에 있는 refreshToken 확인
6866
String refreshToken = request.getHeader("refreshToken");
6967

@@ -75,12 +73,12 @@ protected void doFilterInternal(
7573
JwtPair reissueTokens = jwtUtil.reissueToken(refreshToken);
7674
setSuccessResponse(response, CREATED, reissueTokens);
7775
}
78-
} catch (ExpiredJwtException e1) {
76+
} catch (ExpiredJwtException eje) {
7977
logger.info("[*] case : accessToken, refreshToken expired");
80-
throw new SecurityCustomException(SecurityErrorCode.TOKEN_EXPIRED);
81-
} catch (IllegalArgumentException e2) {
78+
throw new SecurityCustomException(SecurityErrorCode.TOKEN_EXPIRED, eje);
79+
} catch (IllegalArgumentException iae) {
8280
logger.info("[*] case : Invalid refreshToken");
83-
throw new SecurityCustomException(SecurityErrorCode.INVALID_TOKEN);
81+
throw new SecurityCustomException(SecurityErrorCode.INVALID_TOKEN, iae);
8482
}
8583
}
8684
}
@@ -101,7 +99,7 @@ private void authenticateAccessToken(String accessToken) {
10199
null,
102100
userDetails.getAuthorities());
103101

104-
// 세션에 사용자 등록
102+
// 컨텍스트 홀더에 저장
105103
SecurityContextHolder.getContext().setAuthentication(authToken);
106104
}
107105
}

src/main/java/com/sponus/sponusbe/auth/jwt/filter/JwtExceptionFilter.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,20 @@ protected void doFilterInternal(
2525
@NonNull HttpServletRequest request,
2626
@NonNull HttpServletResponse response,
2727
@NonNull FilterChain filterChain) throws IOException {
28-
// TODO : entrypoint로 처리하도록 변경
2928
try {
3029
filterChain.doFilter(request, response);
3130
} catch (SecurityCustomException e) {
3231
log.warn(">>>>> SecurityCustomException : ", e);
3332
BaseErrorCode errorCode = e.getErrorCode();
33+
ApiResponse<String> errorResponse = ApiResponse.onFailure(
34+
errorCode.getCode(),
35+
errorCode.getMessage(),
36+
e.getMessage()
37+
);
3438
HttpResponseUtil.setErrorResponse(
3539
response,
3640
errorCode.getHttpStatus(),
37-
errorCode.getErrorResponse()
41+
errorResponse
3842
);
3943
} catch (Exception e) {
4044
log.error(">>>>> Exception : ", e);

src/main/java/com/sponus/sponusbe/auth/jwt/util/JwtUtil.java

+35-29
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
import com.sponus.sponusbe.auth.jwt.exception.SecurityErrorCode;
1717
import com.sponus.sponusbe.auth.user.CustomUserDetails;
1818

19+
import io.jsonwebtoken.Claims;
1920
import io.jsonwebtoken.Jwts;
21+
import io.jsonwebtoken.MalformedJwtException;
22+
import io.jsonwebtoken.UnsupportedJwtException;
23+
import io.jsonwebtoken.security.SignatureException;
2024
import jakarta.servlet.http.HttpServletRequest;
2125
import lombok.extern.slf4j.Slf4j;
2226

@@ -44,32 +48,6 @@ public JwtUtil(
4448
redisUtil = redis;
4549
}
4650

47-
public Long getId(String token) {
48-
return Long.parseLong(Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload()
49-
.getSubject());
50-
}
51-
52-
public String getEmail(String token) {
53-
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload()
54-
.get("email", String.class);
55-
}
56-
57-
public String getAuthority(String token) {
58-
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload()
59-
.get(AUTHORITIES_CLAIM_NAME, String.class);
60-
}
61-
62-
public Boolean isExpired(String token) {
63-
// 여기서 토큰 형식 이상한 것도 걸러짐
64-
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration()
65-
.before(Date.from(Instant.now()));
66-
}
67-
68-
public Long getExpTime(String token) {
69-
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration()
70-
.getTime();
71-
}
72-
7351
public String createJwtAccessToken(CustomUserDetails customUserDetails) {
7452
Instant issuedAt = Instant.now();
7553
Instant expiration = issuedAt.plusMillis(accessExpMs);
@@ -132,14 +110,11 @@ public String resolveAccessToken(HttpServletRequest request) {
132110
String authorization = request.getHeader("Authorization");
133111

134112
if (authorization == null || !authorization.startsWith("Bearer ")) {
135-
136113
log.warn("[*] No token in req");
137-
138114
return null;
139115
}
140116

141117
log.info("[*] Token exists");
142-
143118
return authorization.split(" ")[1];
144119
}
145120

@@ -154,4 +129,35 @@ public boolean validateRefreshToken(String refreshToken) {
154129
}
155130
return true;
156131
}
132+
133+
public Long getId(String token) {
134+
return Long.parseLong(getClaims(token).getSubject());
135+
}
136+
137+
public String getEmail(String token) {
138+
return getClaims(token).get("email", String.class);
139+
}
140+
141+
public String getAuthority(String token) {
142+
return getClaims(token).get(AUTHORITIES_CLAIM_NAME, String.class);
143+
}
144+
145+
public Boolean isExpired(String token) {
146+
// 여기서 토큰 형식 이상한 것도 걸러짐
147+
return getClaims(token).getExpiration().before(Date.from(Instant.now()));
148+
}
149+
150+
public Long getExpTime(String token) {
151+
return getClaims(token).getExpiration().getTime();
152+
}
153+
154+
private Claims getClaims(String token) {
155+
try {
156+
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload();
157+
} catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
158+
throw new SecurityCustomException(SecurityErrorCode.INVALID_TOKEN, e);
159+
} catch (SignatureException e) {
160+
throw new SecurityCustomException(SecurityErrorCode.TOKEN_SIGNATURE_ERROR, e);
161+
}
162+
}
157163
}

src/main/java/com/sponus/sponusbe/global/common/exception/GlobalExceptionHandler.java

+12
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,16 @@ protected ResponseEntity<ApiResponse<Map<String, String>>> handleMethodArgumentN
6565
failedValidations);
6666
return ResponseEntity.status(ex.getStatusCode()).body(errorResponse);
6767
}
68+
//
69+
// @ExceptionHandler({SecurityCustomException.class})
70+
// public ResponseEntity<ApiResponse<String>> handleAuthenticationException(Exception e) {
71+
// log.error(">>>>> Security Server Error : ", e);
72+
// BaseErrorCode errorCode = SecurityErrorCode.INTERNAL_TOKEN_SERVER_ERROR;
73+
// ApiResponse<String> errorResponse = ApiResponse.onFailure(
74+
// errorCode.getCode(),
75+
// errorCode.getMessage(),
76+
// e.getMessage()
77+
// );
78+
// return ResponseEntity.internalServerError().body(errorResponse);
79+
// }
6880
}

src/main/java/com/sponus/sponusbe/global/config/SecurityConfig.java

+14-5
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
1515
import org.springframework.security.web.SecurityFilterChain;
1616
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
17-
import org.springframework.security.web.authentication.logout.LogoutFilter;
1817

18+
import com.sponus.sponusbe.auth.jwt.exception.JwtAccessDeniedHandler;
19+
import com.sponus.sponusbe.auth.jwt.exception.JwtAuthenticationEntryPoint;
1920
import com.sponus.sponusbe.auth.jwt.filter.CustomLoginFilter;
2021
import com.sponus.sponusbe.auth.jwt.filter.CustomLogoutHandler;
22+
import com.sponus.sponusbe.auth.jwt.filter.JwtAuthenticationFilter;
2123
import com.sponus.sponusbe.auth.jwt.filter.JwtExceptionFilter;
22-
import com.sponus.sponusbe.auth.jwt.filter.JwtFilter;
2324
import com.sponus.sponusbe.auth.jwt.util.JwtUtil;
2425
import com.sponus.sponusbe.auth.jwt.util.RedisUtil;
2526

@@ -30,6 +31,9 @@
3031
@RequiredArgsConstructor
3132
public class SecurityConfig {
3233

34+
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
35+
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
36+
3337
private final String[] swaggerUrls = {"/swagger-ui/**", "/v3/**"};
3438
private final String[] authUrls = {
3539
"/",
@@ -92,11 +96,16 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
9296
.addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class);
9397

9498
http
95-
.addFilterBefore(new JwtFilter(jwtUtil, redisUtil), CustomLoginFilter.class);
99+
.addFilterBefore(new JwtAuthenticationFilter(jwtUtil, redisUtil), CustomLoginFilter.class);
100+
101+
http
102+
.addFilterBefore(new JwtExceptionFilter(), JwtAuthenticationFilter.class);
96103

97-
// Exception Handle filter
98104
http
99-
.addFilterBefore(new JwtExceptionFilter(), LogoutFilter.class);
105+
.exceptionHandling(exception -> exception
106+
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
107+
.accessDeniedHandler(jwtAccessDeniedHandler)
108+
);
100109

101110
// 세션 사용 안함
102111
http

0 commit comments

Comments
 (0)