mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-06 08:15:57 +00:00
fix(auth): preserve protected invalid-session handling
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
parent
98a0e1d2f9
commit
346acbcfa3
3 changed files with 178 additions and 0 deletions
|
|
@ -7,14 +7,17 @@ import com.iflytek.skillhub.auth.oauth.OAuth2LoginSuccessHandler;
|
|||
import com.iflytek.skillhub.auth.oauth.SkillHubOAuth2AuthorizationRequestResolver;
|
||||
import com.iflytek.skillhub.auth.mock.MockAuthFilter;
|
||||
import com.iflytek.skillhub.auth.policy.RouteSecurityPolicyRegistry;
|
||||
import com.iflytek.skillhub.auth.session.ExpiredPublicSessionFilter;
|
||||
import com.iflytek.skillhub.auth.token.ApiTokenAuthenticationFilter;
|
||||
import com.iflytek.skillhub.auth.token.ApiTokenScopeFilter;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
|
|
@ -28,6 +31,7 @@ import org.springframework.security.web.access.AccessDeniedHandler;
|
|||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
|
||||
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.CsrfFilter;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
|
@ -135,6 +139,11 @@ public class SecurityConfig {
|
|||
)
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||
.invalidSessionStrategy((request, response) -> {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.getWriter().write("{\"code\":401,\"msg\":\"Session expired\"}");
|
||||
})
|
||||
)
|
||||
.exceptionHandling(exceptions -> exceptions
|
||||
.accessDeniedHandler(apiAccessDeniedHandler)
|
||||
|
|
@ -154,6 +163,7 @@ public class SecurityConfig {
|
|||
.invalidateHttpSession(true)
|
||||
.deleteCookies("SESSION")
|
||||
)
|
||||
.addFilterBefore(new ExpiredPublicSessionFilter(routeSecurityPolicyRegistry), CsrfFilter.class)
|
||||
.addFilterBefore(apiTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.addFilterAfter(apiTokenScopeFilter, ApiTokenAuthenticationFilter.class);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
package com.iflytek.skillhub.auth.session;
|
||||
|
||||
import com.iflytek.skillhub.auth.policy.RouteSecurityPolicyRegistry;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
* Treats an expired session cookie as absent only for routes that already allow anonymous access.
|
||||
*/
|
||||
public final class ExpiredPublicSessionFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final Set<String> SESSION_COOKIES = Set.of("SESSION", "JSESSIONID");
|
||||
|
||||
private final RouteSecurityPolicyRegistry routeSecurityPolicyRegistry;
|
||||
|
||||
public ExpiredPublicSessionFilter(RouteSecurityPolicyRegistry routeSecurityPolicyRegistry) {
|
||||
this.routeSecurityPolicyRegistry = routeSecurityPolicyRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
String requestPath = RouteSecurityPolicyRegistry.requestPath(request);
|
||||
boolean publicRoute = routeSecurityPolicyRegistry.accessLevel(request.getMethod(), requestPath)
|
||||
== RouteSecurityPolicyRegistry.AccessLevel.PERMIT_ALL;
|
||||
if (publicRoute && request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid()) {
|
||||
filterChain.doFilter(new SessionlessRequest(request), response);
|
||||
return;
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private static final class SessionlessRequest extends HttpServletRequestWrapper {
|
||||
|
||||
private SessionlessRequest(HttpServletRequest request) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequestedSessionId() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRequestedSessionIdValid() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRequestedSessionIdFromCookie() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cookie[] getCookies() {
|
||||
Cookie[] cookies = super.getCookies();
|
||||
if (cookies == null) {
|
||||
return null;
|
||||
}
|
||||
Cookie[] retained = Arrays.stream(cookies)
|
||||
.filter(cookie -> !SESSION_COOKIES.contains(cookie.getName()))
|
||||
.toArray(Cookie[]::new);
|
||||
return retained.length == 0 ? null : retained;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package com.iflytek.skillhub.auth.session;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import com.iflytek.skillhub.auth.policy.RouteSecurityPolicyRegistry;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
class ExpiredPublicSessionFilterTest {
|
||||
|
||||
private final ExpiredPublicSessionFilter filter =
|
||||
new ExpiredPublicSessionFilter(new RouteSecurityPolicyRegistry());
|
||||
|
||||
@Test
|
||||
void expiredSessionOnPublicRoute_shouldBeHiddenFromDownstreamSecurityFilters() throws Exception {
|
||||
MockHttpServletRequest request = expiredSessionRequest("GET", "/api/v1/skills");
|
||||
request.setCookies(
|
||||
new Cookie("SESSION", "expired"),
|
||||
new Cookie("JSESSIONID", "expired-servlet"),
|
||||
new Cookie("locale", "zh"));
|
||||
FilterChain chain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, new MockHttpServletResponse(), chain);
|
||||
|
||||
HttpServletRequest downstream = capturedRequest(chain);
|
||||
assertNull(downstream.getRequestedSessionId());
|
||||
assertArrayEquals(new String[]{"locale"},
|
||||
java.util.Arrays.stream(downstream.getCookies()).map(Cookie::getName).toArray(String[]::new));
|
||||
}
|
||||
|
||||
@Test
|
||||
void expiredSessionOnProtectedMethod_shouldRemainVisibleForUnauthorizedResponse() throws Exception {
|
||||
MockHttpServletRequest request = expiredSessionRequest("POST", "/api/v1/skills");
|
||||
FilterChain chain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, new MockHttpServletResponse(), chain);
|
||||
|
||||
assertSame(request, capturedRequest(chain));
|
||||
assertEquals("expired", request.getRequestedSessionId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validSessionOnPublicRoute_shouldRemainUnchanged() throws Exception {
|
||||
MockHttpServletRequest request = expiredSessionRequest("GET", "/api/v1/search");
|
||||
request.setRequestedSessionIdValid(true);
|
||||
FilterChain chain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, new MockHttpServletResponse(), chain);
|
||||
|
||||
assertSame(request, capturedRequest(chain));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedPrefix_shouldUseServletPathForPublicRouteDecision() throws Exception {
|
||||
MockHttpServletRequest request = expiredSessionRequest("GET", "/skillhub/api/v1/search");
|
||||
request.setContextPath("/skillhub");
|
||||
request.setServletPath("/api/v1/search");
|
||||
FilterChain chain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, new MockHttpServletResponse(), chain);
|
||||
|
||||
assertNull(capturedRequest(chain).getRequestedSessionId());
|
||||
}
|
||||
|
||||
private static MockHttpServletRequest expiredSessionRequest(String method, String path) {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setMethod(method);
|
||||
request.setRequestURI(path);
|
||||
request.setRequestedSessionId("expired");
|
||||
request.setRequestedSessionIdValid(false);
|
||||
return request;
|
||||
}
|
||||
|
||||
private static HttpServletRequest capturedRequest(FilterChain chain) throws Exception {
|
||||
ArgumentCaptor<ServletRequest> requestCaptor = ArgumentCaptor.forClass(ServletRequest.class);
|
||||
ArgumentCaptor<ServletResponse> responseCaptor = ArgumentCaptor.forClass(ServletResponse.class);
|
||||
verify(chain).doFilter(requestCaptor.capture(), responseCaptor.capture());
|
||||
return (HttpServletRequest) requestCaptor.getValue();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue