-
Notifications
You must be signed in to change notification settings - Fork 298
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
858 additions
and
1 deletion.
There are no files selected for viewing
54 changes: 54 additions & 0 deletions
54
api-signature/ballcat-spring-boot-starter-apisignature/pom.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
<parent> | ||
<groupId>org.ballcat</groupId> | ||
<artifactId>ballcat-parent</artifactId> | ||
<version>${revision}</version> | ||
<relativePath>../../ballcat-parent</relativePath> | ||
</parent> | ||
|
||
<artifactId>ballcat-spring-boot-starter-apisignature</artifactId> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>org.ballcat</groupId> | ||
<artifactId>ballcat-common-core</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.ballcat</groupId> | ||
<artifactId>ballcat-common-util</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.springframework</groupId> | ||
<artifactId>spring-web</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-autoconfigure</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>jakarta.servlet</groupId> | ||
<artifactId>jakarta.servlet-api</artifactId> | ||
<scope>provided</scope> | ||
</dependency> | ||
<!-- spring boot 配置所需依赖 --> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-configuration-processor</artifactId> | ||
<optional>true</optional> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter-test</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.ballcat</groupId> | ||
<artifactId>ballcat-web</artifactId> | ||
</dependency> | ||
</dependencies> | ||
|
||
</project> |
41 changes: 41 additions & 0 deletions
41
...pring-boot-starter-apisignature/src/main/java/org/ballcat/apisignature/ApiKeyManager.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
/* | ||
* Copyright 2023-2024 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.ballcat.apisignature; | ||
|
||
/** | ||
* API Key 管理器。 用于根据 Access Key 获取用户主体信息,以及根据用户主体获取对应的 secretKey。 | ||
* | ||
* @author Hccake | ||
* @since 2.0.0 | ||
*/ | ||
public interface ApiKeyManager { | ||
|
||
/** | ||
* 根据传入的 Access Key 获取用户主体信息 | ||
* @param accessKey Access Key | ||
* @return subject | ||
*/ | ||
Object getSubject(String accessKey); | ||
|
||
/** | ||
* 根据用户主体获取到对应的 secretKey. | ||
* @param subject 用户主体 | ||
* @return 如果找到对应的 secretKey,则返回该 secretKey;否则返回 null | ||
*/ | ||
String getSecretKey(Object subject); | ||
|
||
} |
35 changes: 35 additions & 0 deletions
35
...ot-starter-apisignature/src/main/java/org/ballcat/apisignature/ApiSignatureException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/* | ||
* Copyright 2023-2024 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.ballcat.apisignature; | ||
|
||
/** | ||
* API 签名异常. | ||
* | ||
* @author Hccake | ||
* @since 2.0.0 | ||
*/ | ||
public class ApiSignatureException extends RuntimeException { | ||
|
||
public ApiSignatureException(String message) { | ||
super(message); | ||
} | ||
|
||
public ApiSignatureException(String message, Throwable cause) { | ||
super(message, cause); | ||
} | ||
|
||
} |
227 changes: 227 additions & 0 deletions
227
...-boot-starter-apisignature/src/main/java/org/ballcat/apisignature/ApiSignatureFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
/* | ||
* Copyright 2023-2024 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.ballcat.apisignature; | ||
|
||
import java.io.IOException; | ||
import java.io.UnsupportedEncodingException; | ||
import java.nio.charset.Charset; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
import javax.servlet.FilterChain; | ||
import javax.servlet.ServletException; | ||
import javax.servlet.http.HttpServletRequest; | ||
import javax.servlet.http.HttpServletResponse; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.apache.commons.lang3.StringUtils; | ||
import org.ballcat.common.model.result.ApiResult; | ||
import org.ballcat.common.util.JsonUtils; | ||
import org.ballcat.web.util.RepeatBodyRequestWrapper; | ||
import org.springframework.util.AntPathMatcher; | ||
import org.springframework.util.Assert; | ||
import org.springframework.web.filter.OncePerRequestFilter; | ||
import org.springframework.web.util.UrlPathHelper; | ||
import org.springframework.web.util.WebUtils; | ||
|
||
/** | ||
* API 签名校验过滤器。 | ||
* | ||
* @author hccake | ||
*/ | ||
@Slf4j | ||
@RequiredArgsConstructor | ||
public class ApiSignatureFilter extends OncePerRequestFilter { | ||
|
||
private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher(); | ||
|
||
private static final UrlPathHelper URL_PATH_HELPER = new UrlPathHelper(); | ||
|
||
private final ApiSignatureProperties config; | ||
|
||
private final ApiKeyManager apiKeyManager; | ||
|
||
private final NonceStore nonceStore; | ||
|
||
@Override | ||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | ||
throws ServletException, IOException { | ||
|
||
String lookupPathForRequest = URL_PATH_HELPER.getLookupPathForRequest(request); | ||
|
||
// 如果在排除 url 中,则直接跳过 | ||
List<String> excludeUrlPattens = this.config.getExcludeUrlPattens(); | ||
for (String excludeUrlPatten : excludeUrlPattens) { | ||
if (ANT_PATH_MATCHER.match(excludeUrlPatten, lookupPathForRequest)) { | ||
filterChain.doFilter(request, response); | ||
return; | ||
} | ||
} | ||
|
||
// 如果不在排除 url 中,且不在包含 url 规则中,也直接跳过 | ||
List<String> includeUrlPattens = this.config.getIncludeUrlPattens(); | ||
Optional<String> isInclude = includeUrlPattens.stream() | ||
.filter(x -> ANT_PATH_MATCHER.match(x, lookupPathForRequest)) | ||
.findAny(); | ||
if (!isInclude.isPresent()) { | ||
filterChain.doFilter(request, response); | ||
return; | ||
} | ||
|
||
// 校验时间戳,由于服务器时钟同步问题,时间戳的有效期校验为上下 5 分钟 | ||
String timestamp = request.getHeader(this.config.getTimestampHeader()); | ||
long timestampDiffThreshold = this.config.getTimestampDiffThreshold(); | ||
if (StringUtils.isEmpty(timestamp) || !StringUtils.isNumeric(timestamp) | ||
|| Math.abs(System.currentTimeMillis() - Long.parseLong(timestamp)) > timestampDiffThreshold) { | ||
sendErrorResponse(response, "invalid timestamp."); | ||
return; | ||
} | ||
|
||
// 校验请求随机数, 如果随机数重复,说明请求重放 | ||
String nonce = request.getHeader(this.config.getNonceHeader()); | ||
if (nonce == null || nonce.length() != 32 || repeatedNonce(nonce)) { | ||
sendErrorResponse(response, "invalid nonce."); | ||
return; | ||
} | ||
|
||
// 校验 ACCESS_KEY | ||
String accessKey = request.getHeader(this.config.getAccessKeyHeader()); | ||
if (StringUtils.isEmpty(accessKey)) { | ||
sendErrorResponse(response, "invalid access key."); | ||
return; | ||
} | ||
|
||
// 获取 ACCESS_KEY 对应的主体信息 | ||
Object subject = getSubject(accessKey); | ||
if (subject == null) { | ||
sendErrorResponse(response, "invalid subject."); | ||
return; | ||
} | ||
|
||
// 获取对应的 SECRET_KEY | ||
String secretKey = getSecretKey(subject); | ||
if (StringUtils.isEmpty(accessKey)) { | ||
sendErrorResponse(response, "invalid access key."); | ||
return; | ||
} | ||
|
||
// 没有签名需要直接返回 | ||
String signature = request.getHeader(this.config.getSignatureHeader()); | ||
if (StringUtils.isEmpty(signature)) { | ||
sendErrorResponse(response, "invalid signature."); | ||
return; | ||
} | ||
|
||
// 拼接不包含域名的完整请求 url,需要包含 queryString | ||
String requestURI = request.getRequestURI(); | ||
String queryString = request.getQueryString(); | ||
String uriPrefix = this.config.getUriPrefix(); | ||
if (uriPrefix != null) { | ||
requestURI = uriPrefix + requestURI; | ||
} | ||
|
||
// 根据请求参数生成服务端签名,和请求签名做比对 | ||
String requestPayload; | ||
try { | ||
// 包装 request, 以便重复获取 body | ||
if (!(request instanceof RepeatBodyRequestWrapper)) { | ||
request = new RepeatBodyRequestWrapper(request); | ||
} | ||
requestPayload = getRequestPayload(request); | ||
} | ||
catch (Exception e) { | ||
log.error("generate signature error", e); | ||
sendErrorResponse(response, "generate signature error."); | ||
return; | ||
} | ||
|
||
// 生成服务端签名, 和请求签名做比对 | ||
SignatureBuilder signatureBuilder = new SignatureBuilder().setHttpMethod(request.getMethod()) | ||
.setRequestURI(requestURI) | ||
.setQueryString(queryString) | ||
.setRequestPayload(requestPayload) | ||
.setTimestamp(timestamp) | ||
.setNonce(nonce) | ||
.setAccessKey(accessKey) | ||
.setSecretKey(secretKey); | ||
String serverSignature; | ||
try { | ||
serverSignature = signatureBuilder.build(); | ||
} | ||
catch (Exception ex) { | ||
log.error("generate signature error", ex); | ||
sendErrorResponse(response, "generate signature error."); | ||
return; | ||
} | ||
|
||
if (!signature.equalsIgnoreCase(serverSignature)) { | ||
sendErrorResponse(response, "invalid signature."); | ||
return; | ||
} | ||
|
||
try { | ||
SubjectHolder.setSubject(subject); | ||
filterChain.doFilter(request, response); | ||
} | ||
finally { | ||
SubjectHolder.clear(); | ||
} | ||
} | ||
|
||
protected Object getSubject(String accessKey) { | ||
return this.apiKeyManager.getSubject(accessKey); | ||
} | ||
|
||
protected String getSecretKey(Object subject) { | ||
return this.apiKeyManager.getSecretKey(subject); | ||
} | ||
|
||
/** | ||
* 检查随机串是否重复。 | ||
* @param nonce 随机串,32位 | ||
* @return 重复则返回 true。 | ||
*/ | ||
protected boolean repeatedNonce(String nonce) { | ||
long nonceTimeout = this.config.getNonceTimeout(); | ||
TimeUnit nonceTimeoutUnit = this.config.getNonceTimeoutUnit(); | ||
return !this.nonceStore.storeIfAbsent(nonce, nonceTimeout, nonceTimeoutUnit); | ||
} | ||
|
||
private static void sendErrorResponse(HttpServletResponse response, String errorMessage) throws IOException { | ||
log.warn("api signature verification failed:" + errorMessage); | ||
ApiResult<Void> apiResult = ApiResult.failed(401, errorMessage); | ||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); | ||
response.getWriter().write(JsonUtils.toJson(apiResult)); | ||
response.setContentType("application/json;charset=UTF-8"); | ||
response.setCharacterEncoding(StandardCharsets.UTF_8.name()); | ||
} | ||
|
||
private static String getRequestPayload(HttpServletRequest request) throws UnsupportedEncodingException { | ||
RepeatBodyRequestWrapper wrapper = WebUtils.getNativeRequest(request, RepeatBodyRequestWrapper.class); | ||
Assert.notNull(wrapper, "get request body error."); | ||
if (wrapper.getCharacterEncoding() != null) { | ||
return new String(wrapper.getBodyByteArray(), wrapper.getCharacterEncoding()); | ||
} | ||
else { | ||
return new String(wrapper.getBodyByteArray(), Charset.defaultCharset()); | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.