Skip to content

Commit 55c7227

Browse files
feat(openapi): add a filter to allow portal call openapi endpoints (#5474)
Co-authored-by: Zhangjian He <[email protected]>
1 parent e81070c commit 55c7227

File tree

4 files changed

+475
-5
lines changed

4 files changed

+475
-5
lines changed

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilter.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ public class ConsumerAuthenticationFilter implements Filter {
5757
.expireAfterAccess(1, TimeUnit.HOURS)
5858
.maximumSize(RATE_LIMITER_CACHE_MAX_SIZE).build();
5959

60-
public ConsumerAuthenticationFilter(ConsumerAuthUtil consumerAuthUtil, ConsumerAuditUtil consumerAuditUtil) {
60+
private static final String PORTAL_USER_AUTHENTICATED = "PORTAL_USER_AUTHENTICATED";
61+
62+
public ConsumerAuthenticationFilter(ConsumerAuthUtil consumerAuthUtil,
63+
ConsumerAuditUtil consumerAuditUtil) {
6164
this.consumerAuthUtil = consumerAuthUtil;
6265
this.consumerAuditUtil = consumerAuditUtil;
6366
}
@@ -73,6 +76,10 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain
7376
HttpServletRequest request = (HttpServletRequest) req;
7477
HttpServletResponse response = (HttpServletResponse) resp;
7578

79+
if (Boolean.TRUE.equals(request.getAttribute(PORTAL_USER_AUTHENTICATED))) {
80+
chain.doFilter(req, resp);
81+
return;
82+
}
7683
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
7784
ConsumerToken consumerToken = consumerAuthUtil.getConsumerToken(token);
7885

@@ -84,9 +91,11 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain
8491
Integer rateLimit = consumerToken.getRateLimit();
8592
if (null != rateLimit && rateLimit > 0) {
8693
try {
87-
ImmutablePair<Long, RateLimiter> rateLimiterPair = getOrCreateRateLimiterPair(consumerToken.getToken(), rateLimit);
94+
ImmutablePair<Long, RateLimiter> rateLimiterPair = getOrCreateRateLimiterPair(
95+
consumerToken.getToken(), rateLimit);
8896
long warmupToMillis = rateLimiterPair.getLeft() + WARMUP_MILLIS;
89-
if (System.currentTimeMillis() > warmupToMillis && !rateLimiterPair.getRight().tryAcquire()) {
97+
if (System.currentTimeMillis() > warmupToMillis && !rateLimiterPair.getRight()
98+
.tryAcquire()) {
9099
response.sendError(TOO_MANY_REQUESTS, "Too Many Requests, the flow is limited");
91100
return;
92101
}
@@ -109,7 +118,8 @@ public void destroy() {
109118
//nothing
110119
}
111120

112-
private ImmutablePair<Long, RateLimiter> getOrCreateRateLimiterPair(String key, Integer limitCount) {
121+
private ImmutablePair<Long, RateLimiter> getOrCreateRateLimiterPair(String key,
122+
Integer limitCount) {
113123
try {
114124
return LIMITER.get(key, () ->
115125
ImmutablePair.of(System.currentTimeMillis(), RateLimiter.create(limitCount)));
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
* Copyright 2025 Apollo Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
package com.ctrip.framework.apollo.portal.filter;
18+
19+
import java.io.IOException;
20+
import java.util.Arrays;
21+
22+
import javax.servlet.Filter;
23+
import javax.servlet.FilterChain;
24+
import javax.servlet.FilterConfig;
25+
import javax.servlet.ServletException;
26+
import javax.servlet.ServletRequest;
27+
import javax.servlet.ServletResponse;
28+
import javax.servlet.http.Cookie;
29+
import javax.servlet.http.HttpServletRequest;
30+
import javax.servlet.http.HttpServletResponse;
31+
32+
import org.slf4j.Logger;
33+
import org.slf4j.LoggerFactory;
34+
import org.springframework.core.env.Environment;
35+
import org.springframework.security.core.Authentication;
36+
import org.springframework.security.core.context.SecurityContextHolder;
37+
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
38+
39+
/**
40+
* Filter to handle Portal user session validation for OpenAPI requests. This filter runs before
41+
* ConsumerAuthenticationFilter to detect and handle: 1. Authenticated Portal users - allow them to
42+
* access OpenAPI 2. Expired Portal sessions - handle based on authentication mode: - auth/ldap:
43+
* redirect to /signin (form login page) - oidc: return 401 (let frontend handle or trigger OAuth2
44+
* flow) - those three login methods' experiences are the same as before
45+
* <p>
46+
* This keeps the ConsumerAuthenticationFilter focused solely on Consumer Token validation.
47+
*/
48+
public class PortalUserSessionFilter implements Filter {
49+
50+
private static final Logger logger = LoggerFactory.getLogger(PortalUserSessionFilter.class);
51+
52+
private static final String SESSION_COOKIE_NAME = "SESSION";
53+
private static final String OIDC_PROFILE = "oidc";
54+
private static final String PORTAL_USER_AUTHENTICATED = "PORTAL_USER_AUTHENTICATED";
55+
private static final LoginUrlAuthenticationEntryPoint LOGIN_ENTRY_POINT =
56+
new LoginUrlAuthenticationEntryPoint("/signin");
57+
58+
private final Environment environment;
59+
60+
public PortalUserSessionFilter(Environment environment) {
61+
this.environment = environment;
62+
}
63+
64+
@Override
65+
public void init(FilterConfig filterConfig) throws ServletException {
66+
// nothing
67+
}
68+
69+
@Override
70+
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws
71+
IOException, ServletException {
72+
HttpServletRequest request = (HttpServletRequest) req;
73+
HttpServletResponse response = (HttpServletResponse) resp;
74+
75+
// Check if the user is an authenticated Portal user
76+
if (isAuthenticatedPortalUser(request)) {
77+
// Portal user is authenticated, allow access to OpenAPI
78+
logger.debug("Authenticated portal user accessing OpenAPI: {}", request.getRequestURI());
79+
request.setAttribute(PORTAL_USER_AUTHENTICATED, true);
80+
chain.doFilter(req, resp);
81+
return;
82+
}
83+
84+
// Check if there's a SESSION cookie but user is not authenticated
85+
// This indicates the session has expired
86+
if (hasSessionCookie(request)) {
87+
logger.info(
88+
"Request has SESSION cookie but user is not authenticated - session is expired. URI: {}",
89+
request.getRequestURI());
90+
91+
handleSessionExpired(request, response);
92+
return;
93+
}
94+
95+
// Neither authenticated Portal user nor expired session
96+
// Continue to next filter (ConsumerAuthenticationFilter) for token validation
97+
chain.doFilter(req, resp);
98+
}
99+
100+
@Override
101+
public void destroy() {
102+
// nothing
103+
}
104+
105+
/**
106+
* Determines whether the current request is from an authenticated Portal user by checking Spring
107+
* Security's SecurityContext.
108+
*
109+
* @param request the HTTP request
110+
* @return true if authenticated Portal user, false otherwise
111+
*/
112+
private boolean isAuthenticatedPortalUser(HttpServletRequest request) {
113+
try {
114+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
115+
116+
// Check if there is authentication information and it has been authenticated
117+
if (authentication != null && authentication.isAuthenticated()) {
118+
// Exclude anonymous users
119+
String principal = authentication.getName();
120+
if (principal != null && !"anonymousUser".equals(principal)) {
121+
logger.debug("Authenticated portal user: {} accessing OpenAPI: {}",
122+
principal, request.getRequestURI());
123+
return true;
124+
}
125+
}
126+
} catch (Exception e) {
127+
logger.debug("Failed to get authentication from SecurityContext", e);
128+
}
129+
130+
return false;
131+
}
132+
133+
/**
134+
* Checks if the request has a SESSION cookie. This is used to detect expired sessions.
135+
*
136+
* @param request the HTTP request
137+
* @return true if SESSION cookie exists, false otherwise
138+
*/
139+
private boolean hasSessionCookie(HttpServletRequest request) {
140+
Cookie[] cookies = request.getCookies();
141+
if (cookies != null) {
142+
for (Cookie cookie : cookies) {
143+
if (SESSION_COOKIE_NAME.equals(cookie.getName())) {
144+
return true;
145+
}
146+
}
147+
}
148+
return false;
149+
}
150+
151+
/**
152+
* Handles expired session based on authentication mode. - auth/ldap: redirect to /signin (form
153+
* login page) - oidc: return 401 (maintains original behavior, frontend can handle)
154+
*
155+
* @param request the HTTP request
156+
* @param response the HTTP response
157+
*/
158+
private void handleSessionExpired(HttpServletRequest request,
159+
HttpServletResponse response) throws IOException, ServletException {
160+
if (isOidcProfile()) {
161+
// OIDC mode: return 401 to maintain original behavior
162+
logger.debug("OIDC mode: returning 401 for expired session");
163+
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Session expired");
164+
} else {
165+
// Auth/LDAP mode: reuse LoginUrlAuthenticationEntryPoint for consistent redirect handling
166+
logger.debug(
167+
"Auth/LDAP mode: delegating to LoginUrlAuthenticationEntryPoint for login redirect");
168+
LOGIN_ENTRY_POINT.commence(request, response, null);
169+
}
170+
}
171+
172+
/**
173+
* Checks if the current active profile is OIDC.
174+
*
175+
* @return true if OIDC profile is active, false otherwise
176+
*/
177+
private boolean isOidcProfile() {
178+
if (environment != null) {
179+
return Arrays.asList(environment.getActiveProfiles()).contains(OIDC_PROFILE);
180+
}
181+
return false;
182+
}
183+
184+
}

apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthFilterConfiguration.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@
1919
import com.ctrip.framework.apollo.openapi.filter.ConsumerAuthenticationFilter;
2020
import com.ctrip.framework.apollo.openapi.util.ConsumerAuditUtil;
2121
import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil;
22+
import com.ctrip.framework.apollo.portal.filter.PortalUserSessionFilter;
2223
import com.ctrip.framework.apollo.portal.filter.UserTypeResolverFilter;
2324
import org.springframework.boot.web.servlet.FilterRegistrationBean;
2425
import org.springframework.context.annotation.Bean;
2526
import org.springframework.context.annotation.Configuration;
27+
import org.springframework.core.env.Environment;
2628

2729
@Configuration
2830
public class AuthFilterConfiguration {
2931

30-
private static final int OPEN_API_AUTH_ORDER = -99;
32+
private static final int OPEN_API_AUTH_ORDER = -98;
33+
3134
@Bean
3235
public FilterRegistrationBean<ConsumerAuthenticationFilter> openApiAuthenticationFilter(
3336
ConsumerAuthUtil consumerAuthUtil,
@@ -51,4 +54,23 @@ public FilterRegistrationBean<UserTypeResolverFilter> authTypeResolverFilter() {
5154
return authTypeResolverFilter;
5255
}
5356

57+
/**
58+
* Portal user session filter for OpenAPI requests. This filter runs BEFORE
59+
* ConsumerAuthenticationFilter to: 1. Allow authenticated Portal users to access OpenAPI 2.
60+
* Redirect expired Portal sessions to login page (consistent with Portal endpoints)
61+
* <p>
62+
* Order: OPEN_API_AUTH_ORDER - 1 (runs first)
63+
*/
64+
@Bean
65+
public FilterRegistrationBean<PortalUserSessionFilter> portalUserSessionFilter(
66+
Environment environment) {
67+
FilterRegistrationBean<PortalUserSessionFilter> filter = new FilterRegistrationBean<>();
68+
69+
filter.setFilter(new PortalUserSessionFilter(environment));
70+
filter.addUrlPatterns("/openapi/*");
71+
filter.setOrder(OPEN_API_AUTH_ORDER
72+
- 1); // Run before ConsumerAuthenticationFilter after springSecurityFilterChain
73+
74+
return filter;
75+
}
5476
}

0 commit comments

Comments
 (0)