From aada2e257bbdbb613fa03db8e458ed6ffbf118cd Mon Sep 17 00:00:00 2001 From: Lee Jongyoung Date: Tue, 27 Oct 2026 19:57:04 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20URL=20?= =?UTF-8?q?=EB=A7=A4=EC=B9=AD=EC=9D=84=20PathPatternRequestMatcher?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmm/interceptor/AuthenticInterceptor.java | 18 ++- .../interceptor/AuthenticInterceptorTest.java | 148 ++++++++++++++++++ 2 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 src/test/java/egovframework/com/cmm/interceptor/AuthenticInterceptorTest.java diff --git a/src/main/java/egovframework/com/cmm/interceptor/AuthenticInterceptor.java b/src/main/java/egovframework/com/cmm/interceptor/AuthenticInterceptor.java index fb4a6c304..be2fa4044 100644 --- a/src/main/java/egovframework/com/cmm/interceptor/AuthenticInterceptor.java +++ b/src/main/java/egovframework/com/cmm/interceptor/AuthenticInterceptor.java @@ -3,7 +3,7 @@ import java.util.Collections; import java.util.List; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndViewDefiningException; @@ -28,6 +28,7 @@ * 2011.09.07 서준식 인증이 필요없는 URL을 패스하는 로직 추가 * 2017.08.31 장동한 인증된 사용자 체크로직 변경 및 관리자 권한 체크 로직 추가 * 2021.08.27 신용호 dummy모드 사용시 "60. 권한관리" 접근오류 수정 + * 2026.10.27 Spring Security 6.5 권장 PathPatternRequestMatcher로 전환 * */ @@ -37,12 +38,17 @@ public class AuthenticInterceptor implements HandlerInterceptor { /** 관리자 접근 권한 패턴 목록 */ private List adminAuthPatternList; + /** 설정 시 파싱한 관리자 접근 권한 패턴 */ + private List adminAuthPatternMatchers; + public List getAdminAuthPatternList() { return adminAuthPatternList; } public void setAdminAuthPatternList(List adminAuthPatternList) { this.adminAuthPatternList = Collections.unmodifiableList(adminAuthPatternList); + PathPatternRequestMatcher.Builder matcherBuilder = PathPatternRequestMatcher.withDefaults(); + this.adminAuthPatternMatchers = adminAuthPatternList.stream().map(matcherBuilder::matcher).toList(); } /** @@ -63,13 +69,11 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons List authList = EgovUserDetailsHelper.getAuthorities(); //관리자인증여부 boolean adminAuthUrlPatternMatcher = false; - //AntPathRequestMatcher - AntPathRequestMatcher antPathRequestMatcher = null; - //관리자가 아닐때 체크함 - for(String adminAuthPattern : adminAuthPatternList){ - antPathRequestMatcher = new AntPathRequestMatcher(adminAuthPattern); - if(antPathRequestMatcher.matches(request)){ + //관리자 URL 패턴 체크 + for(PathPatternRequestMatcher adminAuthPatternMatcher : adminAuthPatternMatchers){ + if(adminAuthPatternMatcher.matches(request)){ adminAuthUrlPatternMatcher = true; + break; } } //관리자 권한 체크 diff --git a/src/test/java/egovframework/com/cmm/interceptor/AuthenticInterceptorTest.java b/src/test/java/egovframework/com/cmm/interceptor/AuthenticInterceptorTest.java new file mode 100644 index 000000000..27666959d --- /dev/null +++ b/src/test/java/egovframework/com/cmm/interceptor/AuthenticInterceptorTest.java @@ -0,0 +1,148 @@ +package egovframework.com.cmm.interceptor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.web.servlet.ModelAndViewDefiningException; +import org.springframework.web.util.ServletRequestPathUtils; + +import egovframework.com.cmm.service.EgovUserDetailsService; +import egovframework.com.cmm.util.EgovUserDetailsHelper; +import jakarta.servlet.http.HttpServletMapping; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.MappingMatch; + +/** DB나 Spring 컨텍스트 없이 관리자 URL 매칭과 권한별 리다이렉트를 검증한다. */ +class AuthenticInterceptorTest { + + private EgovUserDetailsService previousUserDetailsService; + + @BeforeEach + void saveUserDetailsService() { + previousUserDetailsService = new EgovUserDetailsHelper().getEgovUserDetailsService(); + } + + @AfterEach + void restoreUserDetailsService() { + new EgovUserDetailsHelper().setEgovUserDetailsService(previousUserDetailsService); + } + + @ParameterizedTest(name = "{0}, role={1}, authenticated={2}") + @CsvSource(nullValues = "ALLOW", value = { + "/sec/ram/EgovAuthorList.do, ROLE_USER, true, redirect:/uat/uia/egovLoginUsr.do?auth_error=1", + "/sec/ram/EgovAuthorList.do, ROLE_ADMIN, true, ALLOW", + "/sec/ram/.do, ROLE_USER, true, redirect:/uat/uia/egovLoginUsr.do?auth_error=1", + "/sec/ram/.do, ROLE_ADMIN, true, ALLOW", + "/cop/bbs/SelectBBSMasterInfs.do, ROLE_USER, true, ALLOW", + "/cop/bbs/SelectBBSMasterInfs.do, ROLE_ADMIN, true, ALLOW", + "/sec/ram/nested/EgovAuthorList.do, ROLE_USER, true, ALLOW", + "/sec/ram/nested/EgovAuthorList.do, ROLE_ADMIN, true, ALLOW", + "/SEC/ram/EgovAuthorList.do, ROLE_USER, true, ALLOW", + "/SEC/ram/EgovAuthorList.do, ROLE_ADMIN, true, ALLOW", + "/sec/ram/EgovAuthorList.do/, ROLE_USER, true, ALLOW", + "/sec/ram/EgovAuthorList.do/, ROLE_ADMIN, true, ALLOW", + "/sec/ram/EgovAuthorList.do.json, ROLE_USER, true, ALLOW", + "/sec/ram/EgovAuthorList.do.json, ROLE_ADMIN, true, ALLOW", + "/sec/ram/EgovAuthorList.do, ROLE_USER, false, redirect:/uat/uia/egovLoginUsr.do", + "/sec/ram/EgovAuthorList.do, ROLE_ADMIN, false, redirect:/uat/uia/egovLoginUsr.do", + "/cop/bbs/SelectBBSMasterInfs.do, ROLE_USER, false, redirect:/uat/uia/egovLoginUsr.do", + "/cop/bbs/SelectBBSMasterInfs.do, ROLE_ADMIN, false, redirect:/uat/uia/egovLoginUsr.do" + }) + void urlAuthorizationPreservesExistingDecisions(String path, String role, boolean authenticated, + String expectedRedirect) throws Exception { + new EgovUserDetailsHelper().setEgovUserDetailsService(new EgovUserDetailsService() { + @Override + public Object getAuthenticatedUser() { + return null; + } + + @Override + public List getAuthorities() { + return List.of(role); + } + + @Override + public Boolean isAuthenticated() { + return authenticated; + } + }); + AuthenticInterceptor interceptor = new AuthenticInterceptor(); + // 실제 관리자 패턴 앞에 불일치 패턴을 두어 목록의 OR 매칭도 확인한다. + interceptor.setAdminAuthPatternList(List.of("/not-admin/**", "/sec/ram/*.do")); + + for (String contextPath : List.of("", "/egov")) { + for (String method : List.of("GET", "POST")) { + for (boolean parsedPath : List.of(false, true)) { + HttpServletRequest request = request(contextPath, path, method); + if (parsedPath) { + ServletRequestPathUtils.parseAndCache(request); + } + String scenario = method + " " + contextPath + path + ", cached=" + parsedPath; + if (expectedRedirect == null) { + assertTrue(interceptor.preHandle(request, null, null), scenario); + } else { + ModelAndViewDefiningException exception = assertThrows(ModelAndViewDefiningException.class, + () -> interceptor.preHandle(request, null, null), scenario); + assertEquals(expectedRedirect, exception.getModelAndView().getViewName(), scenario); + } + } + } + } + } + + // spring-test의 Servlet 6.0 전용 Mock 대신 Servlet 5.0에서도 동작하는 요청 스텁을 사용한다. + private static HttpServletRequest request(String contextPath, String path, String httpMethod) { + Map attributes = new HashMap<>(); + HttpServletMapping mapping = new HttpServletMapping() { + @Override + public String getMatchValue() { + return ""; + } + + @Override + public String getPattern() { + return "/"; + } + + @Override + public String getServletName() { + return "dispatcher"; + } + + @Override + public MappingMatch getMappingMatch() { + return MappingMatch.DEFAULT; + } + }; + return (HttpServletRequest) Proxy.newProxyInstance(AuthenticInterceptorTest.class.getClassLoader(), + new Class[] { HttpServletRequest.class }, (proxy, method, args) -> { + switch (method.getName()) { + case "getMethod": return httpMethod; + case "getContextPath": return contextPath; + case "getRequestURI": return contextPath + path; + case "getServletPath": return path; + case "getPathInfo": return null; + case "getQueryString": return "pageIndex=1"; + case "getHttpServletMapping": return mapping; + case "getAttribute": return attributes.get(args[0]); + case "setAttribute": + attributes.put((String) args[0], args[1]); + return null; + case "removeAttribute": + attributes.remove(args[0]); + return null; + default: throw new UnsupportedOperationException(method.getName()); + } + }); + } +}