将API密钥鉴权逻辑抽取到一个单独的过滤器内

This commit is contained in:
2025-09-26 11:50:02 +08:00
parent 7d7810084e
commit 2c0b79e7d9
4 changed files with 146 additions and 11 deletions
@@ -0,0 +1,128 @@
package xyz.thewhitedog9487.WebAPI.Configuration.Security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import xyz.thewhitedog9487.WebAPI.Controller.ResponseData;
import xyz.thewhitedog9487.WebAPI.Data.Entity.AccessLog;
import xyz.thewhitedog9487.WebAPI.Data.Repository.AccessLogRepository;
import java.io.IOException;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@Slf4j
@Component
public class ApiKeyAuthenticationFilter extends OncePerRequestFilter {
@Autowired List<String> ApiKeyList;
@Autowired AccessLogRepository AccessLogRepository;
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
var ServletPath = request.getServletPath();
var PermitPrefix = List.of(
"/ip/",
"/v3/api-docs",
"/swagger-ui/" );
var FullyMatchList = List.of(
"/",
"/swagger-ui.html" );
for (String Prefix : PermitPrefix) {
if ( ServletPath.startsWith(Prefix) ) {
return true; } }
for (String FullyMatch : FullyMatchList) {
if ( ServletPath.equals(FullyMatch) ) {
return true; } }
return false; }
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String ApiKey = request.getHeader("X-API-Key");
if (ApiKey == null) {
log.warn("API密钥验证失败,未传递X-API-Key请求头");
var ResponseBody = new ResponseData(
HttpStatus.UNAUTHORIZED.value(),
"API密钥验证失败,未传递X-API-Key请求头");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(ResponseBody.ToJson());
var Log = new AccessLog(
null,
request.getRequestId(),
request.getHeader("CF-Connecting-IP".toLowerCase() ),
request.getHeader("X-Forwarded-For".toLowerCase() ),
request.getRemoteAddr(),
request.getHeader("CF-IPCountry".toLowerCase() ),
( request.getHeader("CF-IPCountry".toLowerCase() ) == null ) ? null : Locale.of(Locale.PRC.getLanguage(), request.getHeader("CF-IPCountry".toLowerCase() ), Locale.SIMPLIFIED_CHINESE.getVariant()).getISO3Country(),
request.getHeader("User-Agent"),
request.getMethod(),
request.getScheme(),
request.getProtocol(),
request.getRequestURL().toString(),
request.getQueryString(),
Collections.list( request.getHeaderNames() )
.stream()
.map( name -> name + ": " + Collections.list(request.getHeaders(name)) )
.reduce( ( a, b ) -> a + "\n" + b )
.orElse(""),
Instant.now(),
HttpStatus.UNAUTHORIZED.value(),
ResponseBody.ToJson());
AccessLogRepository.save(Log);
return; }
if (ApiKeyList.contains(ApiKey) == true) {
var Auth = new UsernamePasswordAuthenticationToken(
"",
ApiKey,
List.of(new SimpleGrantedAuthority("API")));
SecurityContextHolder.getContext().setAuthentication(Auth);
filterChain.doFilter(request, response);
} else {
log.warn("API密钥验证失败,密钥:{}", ApiKey);
var ResponseBody = new ResponseData(
HttpStatus.UNAUTHORIZED.value(),
"API密钥验证失败",
Map.of("提供的密钥:", ApiKey));
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(ResponseBody.ToJson());
var Log = new AccessLog(
null,
request.getRequestId(),
request.getHeader("CF-Connecting-IP".toLowerCase() ),
request.getHeader("X-Forwarded-For".toLowerCase() ),
request.getRemoteAddr(),
request.getHeader("CF-IPCountry".toLowerCase() ),
( request.getHeader("CF-IPCountry".toLowerCase() ) == null ) ? null : Locale.of(Locale.PRC.getLanguage(), request.getHeader("CF-IPCountry".toLowerCase() ), Locale.SIMPLIFIED_CHINESE.getVariant()).getISO3Country(),
request.getHeader("User-Agent"),
request.getMethod(),
request.getScheme(),
request.getProtocol(),
request.getRequestURL().toString(),
request.getQueryString(),
Collections.list( request.getHeaderNames() )
.stream()
.map( name -> name + ": " + Collections.list(request.getHeaders(name)) )
.reduce( ( a, b ) -> a + "\n" + b )
.orElse(""),
Instant.now(),
HttpStatus.UNAUTHORIZED.value(),
ResponseBody.ToJson() );
AccessLogRepository.save(Log);
return; } } }
@@ -1,25 +1,36 @@
package xyz.thewhitedog9487.WebAPI.Configuration;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import xyz.thewhitedog9487.WebAPI.Configuration.Security.ApiKeyAuthenticationFilter;
@Configuration
class SpringSecurityConfiguration {
@Autowired ApiKeyAuthenticationFilter ApiKeyAuthenticationFilter;
/**
* @see ApiKeyAuthenticationFilter#shouldNotFilter(HttpServletRequest)
*/
@Bean
SecurityFilterChain CustomSecurityFilterChain(HttpSecurity Security) throws Exception {
Security
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(AuthorizationManagerRequestMatcherRegistry -> {
AuthorizationManagerRequestMatcherRegistry
.requestMatchers("/ip/**", "/message/**")
.requestMatchers("/ip/**")
.permitAll()
.requestMatchers("/", "/v3/api-docs/**","swagger-ui/**", "/swagger-ui.html")
.permitAll()
.requestMatchers("/message/**")
.authenticated()
.anyRequest()
.denyAll(); });
.denyAll(); })
.addFilterBefore(ApiKeyAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return Security.build(); }
}
@@ -14,7 +14,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
@@ -117,7 +116,6 @@ class Message {
@PostMapping("/discord")
ResponseEntity<ResponseData> DiscordPush(
@Parameter(description = "用于身份验证的API密钥", required = true, example = "ds1858dscc8745sfwe")
@RequestHeader(value = "X-API-Key", required = true) String ApiKey,
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "包含频道ID和消息内容的JSON对象",
required = true,
@@ -125,12 +123,6 @@ class Message {
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = PostMessageData.class) ) )
@RequestBody PostMessageData RequestBody){
if ( ApiKeyList.contains(ApiKey) == false ) {
log.warn("API密钥验证失败,密钥:{}", ApiKey);
return new ResponseEntity<>(new ResponseData(
HttpStatus.UNAUTHORIZED.value(),
"API密钥验证失败",
Map.of("提供的密钥", ApiKey)), HttpStatus.UNAUTHORIZED); }
var ChannelID = Snowflake.of(RequestBody.ChannelID);
log.info("准备向{}发送消息: {}", ChannelID.asString(), RequestBody.Content);
@@ -5,8 +5,8 @@ import discord4j.core.GatewayDiscordClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import xyz.thewhitedog9487.WebAPI.Configuration.Security.ApiKeyAuthenticationFilter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -46,4 +46,8 @@ class GlobalSharedBean {
log.error("API密钥文件不存在且无法创建API密钥文件。", e1);
System.exit(-1); }
return null; } }
@Bean
ApiKeyAuthenticationFilter ApiKeyAuthenticationFilter(){
return new ApiKeyAuthenticationFilter(); }
}