ํ”„๋กœ์ ํŠธ/NolGoat

Spring Security ์ธ์ฆ ํ•„ํ„ฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌํ•˜๊ธฐ

yongh๐Ÿ™‚ 2024. 11. 27. 04:35

์ธ์ฆ ํ•„ํ„ฐ์—์„œ ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ์˜ ๊ณตํ†ต ์ฒ˜๋ฆฌ

๋ฌธ์ œ

์ธ์ฆ ํ•„ํ„ฐ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ 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();
}

 

์ธ์ฆ ์˜ˆ์™ธ์˜ ์„ธ๋ถ€ ์ •๋ณด ์ฒ˜๋ฆฌ

๋ฌธ์ œ 

์ง€๊ธˆ์ฒ˜๋Ÿผ ํ•„ํ„ฐ์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด AuthenticationEntryPoint๋กœ ๋„˜์–ด๊ฐˆ ๋•Œ ๊ธฐ์กด ์˜ˆ์™ธ์˜ ์ •๋ณด ์ €์žฅ ์—†์ด AuthenticationException์œผ๋กœ ๊ฐ์‹ธ์„œ AuthenticationException์„ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. (์ •ํ™•ํžˆ๋Š” InsufficientAuthenticationException์ž…๋‹ˆ๋‹ค.) ์ด๋กœ ์ธํ•ด, ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ์˜ ์„ธ๋ถ€ ์ •๋ณด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์—†๊ณ , ์˜ˆ์™ธ ์ข…๋ฅ˜์— ๋”ฐ๋ฅธ ์ ์ ˆํ•œ ์‘๋‹ต์„ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

@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, ์ฆ‰ ์ธ์ฆ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋‹ค์Œ ์„ธ ๊ณผ์ •์ด ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค.

  1. SecurityContext์—์„œ ์ธ์ฆ ์ •๋ณด ์‚ญ์ œ
    • ๊ธฐ์กด ์ธ์ฆ ์ •๋ณด๊ฐ€ ๋” ์ด์ƒ ์œ ํšจํ•˜์ง€ ์•Š๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ Authentication์„ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
  2. AuthenticationEntryPoint ํ˜ธ์ถœ
    • AuthenticationException์ด ๋ฐœ์ƒํ•˜๋ฉด ํ•„ํ„ฐ๋Š” AuthenticationEntryPoint๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ์ธ์ฆ ์‹คํŒจ๋ฅผ ๊ณตํ†ต์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  3. ์ธ์ฆ ํ”„๋กœ์„ธ์Šค์˜ ์š”์ฒญ ์ •๋ณด ์ €์žฅ ๋ฐ ๊ฒ€์ƒ‰
    • ๊ธฐ๋ณธ ๊ตฌํ˜„์ฒด๋Š” 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) {
        setErrorResponse(response, TOKEN_EXPIRED);
        return;
    }
    // ExpiredJwtException์„ ์ œ์™ธํ•œ ๋‚˜๋จธ์ง€ JwtException ์ฒ˜๋ฆฌ
    if (exception instanceof JwtException) {
        setErrorResponse(response, INVALID_TOKEN_FORMAT);
        return;
    }
    if (exception instanceof ApiException) {
        setErrorResponse(response, ((ApplicationException)exception).getErrorCode());
    }
}

private void setErrorResponse(HttpServletResponse response, ErrorCode errorCode) throws IOException {
	response.setStatus(errorCode.getHttpStatus().value());
	response.setContentType("application/json;charset=UTF-8");

	ErrorResponse errorResponse = new ErrorResponse(errorCode);
	response.getWriter().print(objectMapper.writeValueAsString(errorResponse));
}

 

๊ฒฐ๊ณผ

์ด์ œ ์ธ์ฆ ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋ฅผ ๊ณตํ†ต์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๊ฐ ์˜ˆ์™ธ๋ฅผ ๊ตฌ๋ณ„ํ•˜์—ฌ ์ ์ ˆํ•œ ์‘๋‹ต์„ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.