Spring Security, JWT 인증 서버 구현 2

1c2·2025년 1월 22일
0

스프링 시큐리티

목록 보기
4/5

Jwt 토큰을 검증하는 코드를 만들어 보도록 하겠습니다.

먼저 application.yml에 다음 의존성을 추가해 줍니다.

jwt:
  header: Authorization
  secrete : ZXJkZ3RmaHlqdWlqbG9pb3V0eXJldHdyZWZzZGdnaGpsaW84NzY3NTY0NXllcnNnZmRnbmhtaixobGlvN2k2dTV5ZXRyZ3NkZmJuY2dtaGcsamx1eWlvN2l0NnU1eWU0dHdlYWY=
  access-token-validity-in-seconds: 360000

jwt토큰을 인코딩할 비밀키와 access-token 유효 기간을 등록해줬습니다.

이 값을 사용하기 위해 Properties를 추가해줍니다.

JwtProperties

package com.jwt.domain.login.jwt;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
    private String header;
    private String secret;
    private Long accessTokenValidityInSeconds;
}

JwtAccessDeniedHandler를 만들어줍니다. 이 핸들러는 Jwt의 인가과정에서의 예외를 처리해 줍니다.

JwtAccessDeniedHandler

package com.jwt.domain.login.jwt;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_FORBIDDEN, "FORBIDDEN");
    }
}

상태 코드는 다음과 같이 만들어 줍니다.

ResponseStatusCode

package com.jwt.web.json;

public class ResponseStatusCode {
    public static final int OK = 200;
    public static final int URL_NOT_FOUND = 404;
    public static final int EMAIL_NOT_VERIFIED = 410;
    public static final int WRONG_PARAMETER = 420;
    public static final int LOGIN_FAILED = 430;
    public static final int SERVER_ERROR = 500;

    // 토큰 응답 코드
    public static final int TOKEN_EXPIRED = 4011;
    public static final int TOKEN_IS_BLACKLIST = 4012;
    public static final int TOKEN_WRONG_SIGNATURE = 4013;
    public static final int TOKEN_HASH_NOT_SUPPORTED = 4014;
    public static final int NO_AUTH_HEADER = 4015;
    public static final int TOKEN_VALIDATION_TRY_FAILED = 4016;

    private ResponseStatusCode() {
    }
}

응답 객체를 만들어보겠습니다.

ApiResponseJson

package com.jwt.web.json;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.http.HttpStatus;

@Getter
@ToString
@NoArgsConstructor
public class ApiResponseJson {
    public HttpStatus httpStatus;
    public int code;
    public Object data;

    // 오류시 사용
    public ApiResponseJson(HttpStatus httpStatus, int code, Object data) {
        this.httpStatus = httpStatus;
        this.code = code;
        this.data = data;
    }

    //정상 동작시 사용
    public ApiResponseJson(HttpStatus httpStatus, Object data) {
        this.httpStatus = httpStatus;
        this.code = ResponseStatusCode.OK;
        this.data = data;
    }
}

Client가 받는 요청의 형식을 어느정도 통일해 줍니다.

이제 Jwt검증 과정에서 인증 예외가 발생하면 AuthenticationEntryPointcommence()함수가 호출됩니다. 인증 예외가 발생한 이유를 알려주도록 하겠습니다.

package com.jwt.domain.login.jwt;


import com.fasterxml.jackson.databind.ObjectMapper;
import com.jwt.domain.login.dto.TokenValidationResult;
import com.jwt.domain.login.jwt.token.TokenStatus;
import com.jwt.web.json.ApiResponseJson;
import com.jwt.web.json.ResponseStatusCode;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private static final String VALIDATION_RESULT_KEY = "result";
    private static final String ERROR_MESSAGE_KEY = "errMsg";
    private static final ObjectMapper objectMapper = new ObjectMapper();


    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        TokenValidationResult result = (TokenValidationResult) request.getAttribute(VALIDATION_RESULT_KEY);
        String errorMessage = result.tokenStatus().getMessage();
        int errorCode;

        switch (result.tokenStatus()) {
            case TOKEN_EXPIRED -> errorCode = ResponseStatusCode.TOKEN_EXPIRED;
            case TOKEN_IS_BLACKLIST -> errorCode = ResponseStatusCode.TOKEN_IS_BLACKLIST;
            case TOKEN_WRONG_SIGNATURE -> errorCode = ResponseStatusCode.TOKEN_WRONG_SIGNATURE;
            case TOKEN_HASH_NOT_SUPPORTED -> errorCode = ResponseStatusCode.TOKEN_HASH_NOT_SUPPORTED;
            case WRONG_AUTH_HEADER -> errorCode = ResponseStatusCode.NO_AUTH_HEADER;
            default -> {
                errorMessage = TokenStatus.TOKEN_VALIDATION_TRY_FAILED.getMessage();
                errorCode = ResponseStatusCode.TOKEN_VALIDATION_TRY_FAILED;
            }
        }

        sendError(response, errorMessage, errorCode);
    }

    private void sendError(HttpServletResponse response, String msg, int code) throws IOException {
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        ApiResponseJson responseJson = new ApiResponseJson(HttpStatus.valueOf(HttpServletResponse.SC_UNAUTHORIZED),
                code, Map.of(ERROR_MESSAGE_KEY, msg));

        String jsonToString = objectMapper.writeValueAsString(responseJson);
        response.getWriter().write(jsonToString);
    }
}

토큰 검증 결과를 TokenValidationResult DTO에 담아서 오류 메세지를 까보고 그에 맞는 Error를 발생하는 코드입니다.

이로써 Jwt 검증 과정에서 인증 예외 핸들러(JwtAuthenticationEntryPoint)와 인가 예외 핸들러(JwtAccessDeniedHandler)를 구현했습니다.

이 핸들러를 Config파일에 등록해줘서 Bean으로 사용할 수 있도록 합니다.

JwtConfig

package com.jwt.domain.config;

@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(JwtProperties.class)
public class JwtConfig {

    @Bean
    public JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint() {
        return new JwtAuthenticationEntryPoint();
    }

    @Bean
    public JwtAccessDeniedHandler jwtAccessDeniedHandler() {
        return new JwtAccessDeniedHandler();
    }
}

요 녀석들 구현 완료

0개의 댓글

관련 채용 정보