인증 체크
API 경로 정보에 매핑되는 VO 클래스 작성
Section titled “API 경로 정보에 매핑되는 VO 클래스 작성”- 경로와 메소드로 구성된다
- 경로와 메소드를 비교하여 동일 API인지 여부를 판별하는 isSame 메소드를 포함한다.
- 변수가 바인딩된 경로에 대해 비교하는 기능이 없어 향후 추가할 필요가 있다.
ApiPath.kt @Schema(description = "API 경로와 메소드")data class ApiPath(@field:Schema(description = "경로")val path: String,@field:Schema(description = "HTTP 메소드")val method: HttpMethod,) {constructor( path: String,method: String) : this(path,HttpMethod.valueOf(method))fun isSame(apiPath: ApiPath): Boolean {return this.path == apiPath.path && this.method == apiPath.method}}
ApiPath 클래스에 대한 유닛 테스트 작성
Section titled “ApiPath 클래스에 대한 유닛 테스트 작성”- 데이터 클래스는 데이터 클래스 템플릿에 있는 테스트를 작성한다.
ApiPathTest.kt @DisplayName("Junit5 ApiPath 데이터 클래스 테스트")class ApiPathTest {@DisplayName("ApiPath 인스턴스 생성 테스트")@Testfun createInstanceTest() {//생성자를 통한 인스턴스 생성 테스트(모든 생성자에 대해서 테스트 작성 필요)val instance1 = ApiPath("/v1/auth/login", HttpMethod.POST)assertNotNull(instance1)assertEquals("/v1/auth/login", instance1.path)assertEquals(HttpMethod.POST, instance1.method)//생성자를 통한 인스턴스 생성 테스트(모든 생성자에 대해서 테스트 작성 필요)val instance2 = ApiPath("/v1/auth/login", "POST")assertNotNull(instance2)assertEquals("/v1/auth/login", instance1.path)assertEquals(HttpMethod.POST, instance1.method)}@DisplayName("데이터 비교 테스트")@Testfun equalsTest() {//생성자를 통한 인스턴스 생성(모든 생성자에 대해서 인스턴스 작성 필요)val instance1 = ApiPath("/v1/auth/login", HttpMethod.POST)val instance2 = ApiPath("/v1/auth/login", "POST")assertEquals(instance2, instance1)val different = ApiPath("/v1/auth/logout", HttpMethod.GET)assertNotEquals(different, instance1)}@DisplayName("데이터 수정 테스트")@Testfun updateTest() {val instance1 = ApiPath("/v1/auth/login", HttpMethod.POST)instance1.path = "/v1/auth/logout"assertEquals("/v1/auth/logout", instance1.path)instance1.method = HttpMethod.GETassertEquals(HttpMethod.GET, instance1.method)}@DisplayName("toString 메소드 정상 작동 테스트")@Testfun toStringTest() {//생성자를 통한 인스턴스 생성(모든 생성자에 대해서 인스턴스 작성 필요)val instance1 = ApiPath("/v1/auth/login", HttpMethod.POST)assertEquals("ApiPath(path=/v1/auth/login, method=POST)",instance1.toString())//생성자를 통한 인스턴스 생성(모든 생성자에 대해서 인스턴스 작성 필요)val instance2 = ApiPath("/v1/auth/login", "POST")assertEquals("ApiPath(path=/v1/auth/login, method=POST)",instance2.toString())}@DisplayName("copy 메소드 정상 작동 테스트")@Testfun copyTest() {val instance1 = ApiPath("/v1/auth/login", HttpMethod.POST)assertEquals(instance1.copy(), instance1)val instance2 = instance1.copy(path = "/v1/auth/logout",method = HttpMethod.GET,)assertEquals("/v1/auth/logout", instance2.path)assertEquals(HttpMethod.GET, instance2.method)}@DisplayName("hashCode 메소드 정상 작동 테스트")@Testfun hashCodeTest() {val instance1 = ApiPath("/v1/auth/login", HttpMethod.POST)assertEquals(instance1.copy().hashCode(), instance1.hashCode())val instance2 = instance1.copy(path = "/v1/auth/logout",method = HttpMethod.GET,)assertNotEquals(instance2.hashCode(), instance1.hashCode())}@DisplayName("isSame 메소드 정상 작동 테스트")@Testfun isSameTest() {val instance1 = ApiPath("/v1/auth/login", HttpMethod.POST)assertTrue { instance1.isSame(ApiPath("/v1/auth/login", HttpMethod.POST)) }assertFalse { instance1.isSame(ApiPath("/v2/auth/login", HttpMethod.GET)) }assertFalse { instance1.isSame(ApiPath("/v1", HttpMethod.POST)) }assertFalse { instance1.isSame(ApiPath("/v1/auth", HttpMethod.POST)) }assertFalse { instance1.isSame(ApiPath("/v1/auth/login/111", HttpMethod.POST)) }assertFalse { instance1.isSame(ApiPath("/v1/auth/login", HttpMethod.DELETE)) }}}
경로로 인증 대상여부를 확인할 수 있는 헬퍼 클래스 작성
Section titled “경로로 인증 대상여부를 확인할 수 있는 헬퍼 클래스 작성”- 경로와 메소드로 구성된다
PermitApiPathHelper.kt @Componentclass PermitApiPathHelper(private val envHelper: EnvironmentHelper) {private val permitPaths: List<ApiPath> = listOf(ApiPath("/v1/auth/login", HttpMethod.POST))fun isPermit(apiPath: ApiPath): Boolean {//테스트일 때는 로그아웃 API도 허용if (envHelper.isTest() && apiPath.path == "/v1/auth/logout") {return true}return permitPaths.any { apiPath.isSame(it) }}}
인터셉터 작성
Section titled “인터셉터 작성”- 세션 정보를 체크하여 로그인 여부를 확인한다.
AuthInterceptor.kt @Componentclass AuthInterceptor(private val permitApiPathHelper: PermitApiPathHelper,) : HandlerInterceptor {private val log = logger()override fun preHandle(request: HttpServletRequest,response: HttpServletResponse,handler: Any): Boolean {val path = request.requestURIval method = request.methodif ("OPTIONS" == method) {return true}if (permitApiPathHelper.isPermit(ApiPath(path, method))) {log.debug("Permitted Path: {} {}", path, method)return true}val sessionUser: SessionUser? = sessionUser()log.debug("sessionUser: {}", sessionUser)if (sessionUser === null) {throw BizException(ApiResultCode.NO_SESSION_USER)}return true}}
프레임워크에 인터셉터 등록
Section titled “프레임워크에 인터셉터 등록”- 스프링 부트 설정에 인터셉터 등록
WebConfig.kt @Beanfun addCorsMappings(authInterceptor: AuthInterceptor): WebMvcConfigurer {return object: WebMvcConfigurer {//Interceptoroverride fun addInterceptors(registry: InterceptorRegistry) {registry.addInterceptor(authInterceptor).order(1).addPathPatterns("/v1/**")}}}
- SpringBootTest 환경에서 테스트
AuthInterceptorTest.kt @DisplayName("API 요청시 인증정보 체크 인터셉터 테스트")class AuthInterceptorTest : AcceptanceTest() {@BeforeEachfun setUp() {//로그아웃Given {spec(jsonRequestSpecification)} When {post("/auth/logout")} Then {statusCode(HttpStatus.SC_OK)} Extract {println(body().asString())}}@DisplayName("인증정보가 없는 상태에서 로그인 테스트")@Testfun loginTest() {//로그인val param: RequestDto<LoginRequest> = RequestDto(LoginRequest("hong@test.com", "abcd4321", null))Given {spec(jsonRequestSpecification)body(param)} When {post("/auth/login")} Then {statusCode(HttpStatus.SC_OK)body("isSuccess", equalTo(true),"resultCode", equalTo("SUCCESS"),"payload", notNullValue(),"payload.sessionUser", notNullValue(),"payload.sessionUser.userId", equalTo(1000),"payload.sessionUser.email", equalTo("hong@test.com"),"payload.sessionUser.name", equalTo("홍길동"),)} Extract {println(body().asString())}}@DisplayName("로그인을 제외한 다른 API 호출시 UNAUTHORIZED 오류 발생 테스트")@Testfun unauthorizedTest() {//API 목록 조회val param: RequestDto<SelectApiListRequest> = RequestDto(SelectApiListRequest(null, null, null, null, null))Given {spec(jsonRequestSpecification)body(param)} When {post("/api/list")} Then {statusCode(HttpStatus.SC_UNAUTHORIZED)body("isSuccess", equalTo(false),"resultCode", equalTo("NO_SESSION_USER"),"payload", nullValue(),)} Extract {println(body().asString())}}}