์ธ์ฆ ํํฐ์์ ๋ฐ์ํ ์์ธ์ ๊ณตํต ์ฒ๋ฆฌ
โ ๏ธ๋ฌธ์
์ธ์ฆ ํํฐ๋ ๊ธฐ๋ณธ์ ์ผ๋ก Spring Security ํํฐ๋ก์ ๋์ํ๋ฉฐ, `DispatcherServlet` ์ด์ ์ ์คํ๋ฉ๋๋ค. ์ด๋ก ์ธํด `@RestControllerAdvice`์ `@ExceptionHandler`๋ฅผ ํตํ ์์ธ ์ฒ๋ฆฌ ๋ฐฉ์์ด ์ ์ฉ๋์ง ์์ต๋๋ค.
(@ExceptionHandler๋ DispatcherServlet ๋ด์์ ๋ฐ์ํ ์์ธ๋ง ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.)
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ApiException.class)
public ResponseEntity<ErrorResponse> handleApiException(ApiException e) {
ErrorCode errorCode = e.getErrorCode();
return ResponseEntity
.status(errorCode.getHttpStatus())
.body(new ErrorResponse(errorCode));
}
}
๋ฐ๋ผ์ ์ด ๋ฐฉ์์ผ๋ก๋ ์ธ์ฆ ํํฐ์์ ๋ฐ์ํ ์์ธ๋ฅผ ์ํ๋ ๋ฐฉ์์ผ๋ก ๊ณตํต ์๋ต ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
โ ํด๊ฒฐ
Spring Security๊ฐ ์ ๊ณตํ๋ `AuthenticationEntryPoint`๋ฅผ ๋์ ํ์ฌ ์ธ์ฆ ๊ณผ์ ์์ ๋ฐ์ํ๋ ์์ธ๋ฅผ ๋ณ๋๋ก ์ฒ๋ฆฌํ๋๋ก ๊ตฌ์ฑํ์ต๋๋ค. ์ด๋ฅผ ํตํด ์ธ์ฆ ํํฐ์์ ๋ฐ์ํ๋ ์์ธ๋ AuthenticationEntryPoint์์ ์ฒ๋ฆฌํ ์ ์๊ฒ ๋์์ต๋๋ค.
- AuthenticationEntryPointImpl
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"error\": \"AUTHENTICATION_FAIL\", \"message\": \"์๋ชป๋ ์ธ์ฆ ์ ๋ณด์
๋๋ค.\"}");
}
}
- SecurityConfig
private final AuthenticationEntryPointImpl authenticationEntryPoint;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
...
http.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(authenticationEntryPoint)
);
...
return http.build();
}
์ธ์ฆ ์์ธ์ ์ธ๋ถ ์ ๋ณด ์ฒ๋ฆฌ
โ ๏ธ๋ฌธ์
Spring Security๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ธ์ฆ ๊ด๋ จ ์์ธ๋ฅผ `AuthenticationException`์ผ๋ก ๊ฐ์ธ์ AuthenticationEntryPoint๋ก ์ ๋ฌํฉ๋๋ค. ์ด๋ก ์ธํด, ๋์ด์จ ์์ธ์ ์ธ๋ถ ์ ๋ณด๋ฅผ ํ์ธํ ์ ์๊ณ , ์์ธ ์ข ๋ฅ์ ๋ฐ๋ฅธ ์ ์ ํ ์๋ต์ ํ ์ ์์ต๋๋ค.
@Slf4j
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.info(authException.toString()); // InsufficientAuthenticationException
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"error\": \"AUTHENTICATION_FAIL\", \"message\": \"์๋ชป๋ ์ธ์ฆ ์ ๋ณด์
๋๋ค.\"}");
}
}
org.springframework.security.authentication.InsufficientAuthenticationException: Full authentication is required to access this resource
โ ํด๊ฒฐ
์ธ์ฆ ํํฐ์์ ๋ฐ์ํ ์์ธ๋ฅผ HttpServletRequest์ ์์ฑ์ ์ ์ฅํ ๋ค, AuthenticationEntryPoint์์ ์ด๋ฅผ ์กฐํํ์ฌ ์๋ตํ ์ ์๋๋ก ํ์ต๋๋ค.
- ์ธ์ฆ ํํฐ์์ ์์ธ ์ ์ฅ
@RequiredArgsConstructor
public class AuthFilter extends OncePerRequestFilter {
private final AuthService authService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER);
authService.verifyAuthorizationHeader(authorizationHeader);
String accessToken = authorizationHeader.split(" ")[1];
authService.verifyAccessToken(accessToken);
} catch (ApiException e) {
request.setAttribute("exception", e);
filterChain.doFilter(request, response);
return;
}
filterChain.doFilter(request, response);
}
}
- ์ ์ฅํ ์์ธ๋ฅผ AuthenticationEntryPoint์์ ์กฐํ ๋ฐ ์๋ต
@RequiredArgsConstructor
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Object exception = request.getAttribute("exception");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(new ErrorResponse(((ApiException) exception).getErrorCode())));
}
}
์ฐธ๊ณ ) AuthenticationException
AuthenticationException, ์ฆ ์ธ์ฆ ์์ธ๊ฐ ๋ฐ์ํ๋ฉด ๋ค์ ์ธ ๊ณผ์ ์ด ์งํ๋ฉ๋๋ค.
- SecurityContext์์ ์ธ์ฆ ์ ๋ณด ์ญ์
- ๊ธฐ์กด ์ธ์ฆ ์ ๋ณด๊ฐ ๋ ์ด์ ์ ํจํ์ง ์๋ค๊ณ ํ๋จํ์ฌ Authentication์ ์ด๊ธฐํํฉ๋๋ค.
- AuthenticationEntryPoint ํธ์ถ
- AuthenticationException์ด ๋ฐ์ํ๋ฉด ํํฐ๋ AuthenticationEntryPoint๋ฅผ ์คํํ์ฌ ์ธ์ฆ ์คํจ๋ฅผ ๊ณตํต์ผ๋ก ์ฒ๋ฆฌํฉ๋๋ค.
- ์ธ์ฆ ํ๋ก์ธ์ค์ ์์ฒญ ์ ๋ณด ์ ์ฅ ๋ฐ ๊ฒ์
- ๊ธฐ๋ณธ ๊ตฌํ์ฒด๋ HttpSessionRequestCache์ ๋๋ค.
- ex) ํ์ด์ง A์ ์ ๊ทผํ๋ ค๋ค ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ์ ๋ ๊ฒฝ์ฐ, ์ธ์ฆ์ ์๋ฃํ ํ, ๋ค์ ํ์ด์ง A๋ก ์ด๋ํ ์ ์๋๋ก ์ ๋ณด๋ฅผ ์ ์ฅํฉ๋๋ค.
ํ ํฐ ๊ฒ์ฆ ๊ณผ์ ์์ ๋ฐ์ํ๋ JwtException ์ฒ๋ฆฌ
โ ๏ธ๋ฌธ์
ํ ํฐ ๋ง๋ฃ ์ฌ๋ถ๋ฅผ ํ์ธํ๋ ์ฝ๋์์๋ ์ปค์คํ ์์ธ(ApiException)๋ฅผ ๋ฐ์์ํค๋๋ก ์ค๊ณํ์ง๋ง, JWT ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋ด๋ถ ๋ก์ง์์ ๋จผ์ `ExpiredJwtException`์ด ๋ฐ์ํ์ฌ ์๋ํ ๋๋ก ์ฒ๋ฆฌ๋์ง ์์์ต๋๋ค. ์ด๋ `parseSignedClaims()` ํธ์ถ ๊ณผ์ ์์ ์๋ช ๊ฒ์ฆ๊ณผ ๋์์ ํ ํฐ ๋ง๋ฃ ์ฌ๋ถ๋ฅผ ๊ฒ์ฆํ๊ธฐ ๋๋ฌธ์ด์์ต๋๋ค.
- JwtProvider
public Boolean ieExpired(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token) // ์ฌ๊ธฐ์ JwtException์ด ๋ฐ์
.getPayload()
.getExpiration()
.before(new Date());
}
- ํ ํฐ ๋ง๋ฃ ๊ฒ์ฆ ๋ก์ง
String accessToken = authorization.split(" ")[1];
if (jwtProvider.ieExpired(accessToken)) {
throw new ApiException(TOKEN_EXPIRED);
}
โ ํด๊ฒฐ
ํ ํฐ ๋ง๋ฃ ๊ฒ์ฆ์ ์ง์ ํ์ง ์๊ณ , JWT ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๊ธฐ๋ณธ ๊ฒ์ฆ ๋ก์ง์ ํ์ฉํ์ฌ ์์ธ๋ฅผ ๋ฐ์์ํค๋๋ก ์ฝ๋๋ฅผ ๋จ์ํํ์ต๋๋ค. ์ดํ, JwtException๊ณผ ApiException์ ๊ตฌ๋ถํ์ฌ ์ฒ๋ฆฌํ๋๋ก ์ฝ๋๋ฅผ ์์ ํ์ต๋๋ค.
- ํ ํฐ์ ํ์, ๋ง๋ฃ์ฌ๋ถ ๋ฑ๊ณผ ๊ด๋ จ๋ JwtException์ ์ง์ ์ฒ๋ฆฌํ์ง ์์ต๋๋ค.
public void verifyAccessToken(String accessToken) {
Claims payload = getPayload(accessToken);
String type = payload.get(CLAIM_TYPE, String.class);
if (!type.equals(CLAIM_TYPE_ACCESS)) {
throw new ApiException(INVALID_TOKEN_TYPE);
}
}
private Claims getPayload(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
}
- ์ฒ๋ฆฌ ๋์ ์์ธ์ JwtException๋ ์ถ๊ฐํฉ๋๋ค.
// ํค๋, ํ ํฐ ๊ฒ์ฆ
String accessToken;
try {
String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER);
authService.verifyAuthorizationHeader(authorizationHeader);
accessToken = authorizationHeader.split(" ")[1];
authService.verifyAccessToken(accessToken);
} catch (JwtException | ApiException e) {
request.setAttribute("exception", e);
filterChain.doFilter(request, response);
return;
}
- ์์ธ์ ์ข ๋ฅ์ ๋ฐ๋ผ ๋ถ๊ธฐํ์ฌ ๊ฐ๊ฐ์ ๋ง๋ ์ ์ ํ ์๋ต์ ํ ์ ์๋๋ก ํ์ต๋๋ค.
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Object exception = request.getAttribute("exception");
if (exception instanceof ExpiredJwtException) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(new ErrorResponse(TOKEN_EXPIRED)));
return;
}
// ExpiredJwtException์ ์ ์ธํ ๋๋จธ์ง JwtException ์ฒ๋ฆฌ
if (exception instanceof JwtException) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(new ErrorResponse(INVALID_TOKEN_FORMAT)));
return;
}
if (exception instanceof ApiException) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(new ErrorResponse(((ApiException) exception).getErrorCode())));
}
}
๊ฒฐ๊ณผ
์ด์ ์ธ์ฆ ๊ณผ์ ์์ ๋ฐ์ํ๋ ์์ธ๋ฅผ ๊ณตํต์ผ๋ก ์ฒ๋ฆฌํ ์ ์์ผ๋ฉฐ, ๊ฐ ์์ธ๋ฅผ ๊ตฌ๋ณํ์ฌ ์ ์ ํ ์๋ต์ ํ ์ ์๊ฒ ๋์์ต๋๋ค.
'ํ๋ก์ ํธ > NolGoat' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
ํจ์จ์ ์ธ ํ ํฐ ๊ด๋ฆฌ๋ฅผ ์ํ Redis ๋์ (1) | 2024.11.29 |
---|---|
์ธ์ฆ ๋ณด์ ๊ฐํ (0) | 2024.11.29 |
์ธ์ฆ์ด ํ์ํ URL ๊ณตํต ๊ด๋ฆฌ (0) | 2024.11.27 |
์กฐํ ๋ฐฉ์ ๊ฐ์ ๋ฐ ์ธ๋ฑ์ค ์์ (0) | 2024.11.26 |
MySQL ์กฐํ ์ฑ๋ฅ ๊ฐ์ (0) | 2024.11.26 |