Skip to content

Commit c0f7467

Browse files
authored
feat: 优化open api认证方式, 优化白名单和租户管理功能 (#71)
1. 白名单支持租户隔离 2. 优化open api认证方式 3. 增加资产聚合视角 4. 优化租户管理打开位置和UI 5. 白名单相关代码重构
1 parent a56b461 commit c0f7467

File tree

201 files changed

+11531
-1883
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

201 files changed

+11531
-1883
lines changed

app/api/pom.xml

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<artifactId>cloudrec</artifactId>
77
<groupId>com.alipay</groupId>
8-
<version>0.2.0-SNAPSHOT</version>
8+
<version>0.2.1-SNAPSHOT</version>
99
<relativePath>../../pom.xml</relativePath>
1010
</parent>
1111
<artifactId>api</artifactId>
@@ -18,7 +18,7 @@
1818
<dependency>
1919
<groupId>com.alipay</groupId>
2020
<artifactId>application</artifactId>
21-
<version>0.2.0-SNAPSHOT</version>
21+
<version>0.2.1-SNAPSHOT</version>
2222
</dependency>
2323
<!-- Spring Boot Starter Test -->
2424
<dependency>
@@ -27,4 +27,24 @@
2727
<scope>test</scope>
2828
</dependency>
2929
</dependencies>
30+
31+
<build>
32+
<plugins>
33+
<!-- Maven Surefire Plugin for unified JUnit 5 testing -->
34+
<plugin>
35+
<groupId>org.apache.maven.plugins</groupId>
36+
<artifactId>maven-surefire-plugin</artifactId>
37+
<version>3.0.0-M7</version>
38+
<configuration>
39+
<!-- Enable JUnit Platform for JUnit 5 support -->
40+
<useJUnitPlatform>true</useJUnitPlatform>
41+
<includes>
42+
<include>**/*Test.java</include>
43+
<include>**/*Tests.java</include>
44+
</includes>
45+
<testFailureIgnore>false</testFailureIgnore>
46+
</configuration>
47+
</plugin>
48+
</plugins>
49+
</build>
3050
</project>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.alipay.api.config.filter;
18+
19+
import com.alipay.application.service.system.utils.CachedBodyHttpServletRequest;
20+
import jakarta.servlet.*;
21+
import jakarta.servlet.http.HttpServletRequest;
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
import org.springframework.core.annotation.Order;
25+
import org.springframework.stereotype.Component;
26+
27+
import java.io.IOException;
28+
29+
/**
30+
* Filter to cache request body for multiple reads
31+
* This filter wraps POST requests with JSON content type to allow
32+
* multiple components to read the request body without conflicts
33+
*/
34+
@Component
35+
@Order(1) // Execute early in the filter chain
36+
public class CachedBodyFilter implements Filter {
37+
38+
private static final Logger logger = LoggerFactory.getLogger(CachedBodyFilter.class);
39+
40+
@Override
41+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
42+
throws IOException, ServletException {
43+
44+
if (request instanceof HttpServletRequest httpRequest) {
45+
// Only wrap POST requests with JSON content type
46+
if ("POST".equalsIgnoreCase(httpRequest.getMethod())) {
47+
String contentType = httpRequest.getContentType();
48+
if (contentType != null && contentType.toLowerCase().contains("application/json")) {
49+
try {
50+
// Wrap the request to cache the body
51+
CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(httpRequest);
52+
logger.debug("Wrapped POST request with JSON content type for caching");
53+
chain.doFilter(cachedRequest, response);
54+
return;
55+
} catch (Exception e) {
56+
logger.warn("Failed to wrap request for body caching: {}", e.getMessage());
57+
// Fall through to process the original request
58+
}
59+
}
60+
}
61+
}
62+
63+
// For non-POST requests or requests without JSON content type, proceed normally
64+
chain.doFilter(request, response);
65+
}
66+
67+
@Override
68+
public void init(FilterConfig filterConfig) {
69+
logger.info("CachedBodyFilter initialized");
70+
}
71+
72+
@Override
73+
public void destroy() {
74+
logger.info("CachedBodyFilter destroyed");
75+
}
76+
}

app/api/src/main/java/com/alipay/api/config/filter/annotation/aop/OpenApiAspect.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public void processRequest(JoinPoint joinPoint) {
6262
throw new OpenAipNoAuthException(response.getMsg());
6363
}
6464

65-
String accessKey = request.getHeader(DigestSignUtils.accessKeyName);
65+
String accessKey = request.getHeader(DigestSignUtils.ACCESS_KEY_NAME);
6666
OpenApiAuthPO openApiAuthPO = openApiAuthMapper.findByAccessKey(accessKey);
6767
if (openApiAuthPO != null) {
6868
String userId = openApiAuthPO.getUserId();
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.alipay.api.config.filter.annotation.aop;
18+
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
/**
25+
* Rate limiting annotation for API endpoints
26+
* Uses sliding window algorithm to control request frequency
27+
*
28+
* @author jietian
29+
* @version 1.0
30+
* @since 2024
31+
*/
32+
@Retention(RetentionPolicy.RUNTIME)
33+
@Target(ElementType.METHOD)
34+
public @interface RateLimit {
35+
36+
/**
37+
* Maximum number of requests allowed per time window
38+
* Default: 10 requests
39+
*/
40+
int maxRequests() default 10;
41+
42+
/**
43+
* Time window in seconds
44+
* Default: 60 seconds (1 minute)
45+
*/
46+
int timeWindowSeconds() default 60;
47+
48+
/**
49+
* Rate limiting key strategy
50+
* IP: limit by client IP address
51+
* USER: limit by authenticated user ID
52+
* GLOBAL: global rate limiting for all requests
53+
* Default: IP
54+
*/
55+
KeyStrategy keyStrategy() default KeyStrategy.IP;
56+
57+
/**
58+
* Custom error message when rate limit is exceeded
59+
* Default: "Too many requests, please try again later"
60+
*/
61+
String message() default "Too many requests, please try again later";
62+
63+
/**
64+
* Key strategy enumeration
65+
*/
66+
enum KeyStrategy {
67+
IP, // Rate limit by IP address
68+
USER, // Rate limit by user ID
69+
GLOBAL // Global rate limit
70+
}
71+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.alipay.api.config.filter.annotation.aop;
18+
19+
import com.alipay.api.config.filter.service.RateLimitService;
20+
import com.alipay.application.share.vo.ApiResponse;
21+
import com.alipay.dao.context.UserInfoContext;
22+
import com.alipay.dao.context.UserInfoDTO;
23+
import jakarta.annotation.Resource;
24+
import jakarta.servlet.http.HttpServletRequest;
25+
import lombok.extern.slf4j.Slf4j;
26+
import org.apache.commons.lang3.StringUtils;
27+
import org.aspectj.lang.ProceedingJoinPoint;
28+
import org.aspectj.lang.annotation.Around;
29+
import org.aspectj.lang.annotation.Aspect;
30+
import org.springframework.http.HttpStatus;
31+
import org.springframework.stereotype.Component;
32+
import org.springframework.web.context.request.RequestContextHolder;
33+
import org.springframework.web.context.request.ServletRequestAttributes;
34+
35+
/**
36+
* Rate limiting aspect for intercepting methods annotated with @RateLimit
37+
* Implements rate limiting logic using sliding window algorithm
38+
*
39+
* @author jietian
40+
* @version 1.0
41+
* @since 2024
42+
*/
43+
@Aspect
44+
@Component
45+
@Slf4j
46+
public class RateLimitAspect {
47+
48+
@Resource
49+
private RateLimitService rateLimitService;
50+
51+
/**
52+
* Around advice for rate limiting
53+
* Intercepts method calls annotated with @RateLimit and applies rate limiting logic
54+
*
55+
* @param joinPoint the join point representing the intercepted method
56+
* @param rateLimit the rate limit annotation
57+
* @return the result of the method execution or rate limit error response
58+
* @throws Throwable if method execution fails
59+
*/
60+
@Around("@annotation(rateLimit)")
61+
public Object rateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
62+
try {
63+
// Generate rate limiting key based on strategy
64+
String rateLimitKey = generateRateLimitKey(rateLimit.keyStrategy(), joinPoint);
65+
66+
// Check if request is allowed
67+
boolean allowed = rateLimitService.isAllowed(
68+
rateLimitKey,
69+
rateLimit.maxRequests(),
70+
rateLimit.timeWindowSeconds()
71+
);
72+
73+
if (!allowed) {
74+
log.warn("Rate limit exceeded for key: {}, method: {}.{}",
75+
rateLimitKey,
76+
joinPoint.getTarget().getClass().getSimpleName(),
77+
joinPoint.getSignature().getName());
78+
79+
// Return rate limit exceeded response
80+
return createRateLimitResponse(rateLimit.message());
81+
}
82+
83+
// Proceed with method execution
84+
return joinPoint.proceed();
85+
86+
} catch (Exception e) {
87+
log.error("Error in rate limiting aspect for method: {}.{}",
88+
joinPoint.getTarget().getClass().getSimpleName(),
89+
joinPoint.getSignature().getName(), e);
90+
91+
// In case of error, allow the request to proceed
92+
return joinPoint.proceed();
93+
}
94+
}
95+
96+
/**
97+
* Generate rate limiting key based on the specified strategy
98+
*
99+
* @param strategy the key generation strategy
100+
* @param joinPoint the join point for method context
101+
* @return the generated rate limiting key
102+
*/
103+
private String generateRateLimitKey(RateLimit.KeyStrategy strategy, ProceedingJoinPoint joinPoint) {
104+
String methodName = joinPoint.getTarget().getClass().getSimpleName() + "." + joinPoint.getSignature().getName();
105+
106+
switch (strategy) {
107+
case IP:
108+
return "rate_limit:ip:" + getClientIpAddress() + ":" + methodName;
109+
case USER:
110+
return "rate_limit:user:" + getCurrentUserId() + ":" + methodName;
111+
case GLOBAL:
112+
return "rate_limit:global:" + methodName;
113+
default:
114+
return "rate_limit:ip:" + getClientIpAddress() + ":" + methodName;
115+
}
116+
}
117+
118+
/**
119+
* Get client IP address from HTTP request
120+
*
121+
* @return client IP address
122+
*/
123+
private String getClientIpAddress() {
124+
try {
125+
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
126+
HttpServletRequest request = attributes.getRequest();
127+
128+
// Check for IP address in various headers (for proxy scenarios)
129+
String ipAddress = request.getHeader("X-Forwarded-For");
130+
if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
131+
ipAddress = request.getHeader("Proxy-Client-IP");
132+
}
133+
if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
134+
ipAddress = request.getHeader("WL-Proxy-Client-IP");
135+
}
136+
if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
137+
ipAddress = request.getHeader("HTTP_CLIENT_IP");
138+
}
139+
if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
140+
ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
141+
}
142+
if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
143+
ipAddress = request.getRemoteAddr();
144+
}
145+
146+
// Handle multiple IPs in X-Forwarded-For header
147+
if (StringUtils.isNotBlank(ipAddress) && ipAddress.contains(",")) {
148+
ipAddress = ipAddress.split(",")[0].trim();
149+
}
150+
151+
return StringUtils.isNotBlank(ipAddress) ? ipAddress : "unknown";
152+
} catch (Exception e) {
153+
log.warn("Failed to get client IP address", e);
154+
return "unknown";
155+
}
156+
}
157+
158+
/**
159+
* Get current authenticated user ID
160+
*
161+
* @return user ID or "anonymous" if not authenticated
162+
*/
163+
private String getCurrentUserId() {
164+
try {
165+
UserInfoDTO userInfo = UserInfoContext.getCurrentUser();
166+
if (userInfo != null && StringUtils.isNotBlank(userInfo.getUserId())) {
167+
return userInfo.getUserId();
168+
}
169+
} catch (Exception e) {
170+
log.debug("Failed to get current user ID", e);
171+
}
172+
return "anonymous";
173+
}
174+
175+
/**
176+
* Create rate limit exceeded response
177+
*
178+
* @param message custom error message
179+
* @return API response indicating rate limit exceeded
180+
*/
181+
private ApiResponse<Object> createRateLimitResponse(String message) {
182+
return new ApiResponse<>(HttpStatus.TOO_MANY_REQUESTS.value(), message);
183+
}
184+
}

0 commit comments

Comments
 (0)