diff --git a/src/main/java/xyz/thewhitedog9487/WebAPI/Configuration/Security/ApiKeyAuthenticationFilter.java b/src/main/java/xyz/thewhitedog9487/WebAPI/Configuration/Security/ApiKeyAuthenticationFilter.java new file mode 100644 index 0000000..dabae6e --- /dev/null +++ b/src/main/java/xyz/thewhitedog9487/WebAPI/Configuration/Security/ApiKeyAuthenticationFilter.java @@ -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 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; } } } + diff --git a/src/main/java/xyz/thewhitedog9487/WebAPI/Configuration/SpringSecurityConfiguration.java b/src/main/java/xyz/thewhitedog9487/WebAPI/Configuration/SpringSecurityConfiguration.java index 65bdee4..e89d8d6 100644 --- a/src/main/java/xyz/thewhitedog9487/WebAPI/Configuration/SpringSecurityConfiguration.java +++ b/src/main/java/xyz/thewhitedog9487/WebAPI/Configuration/SpringSecurityConfiguration.java @@ -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(); } } diff --git a/src/main/java/xyz/thewhitedog9487/WebAPI/Controller/Message.java b/src/main/java/xyz/thewhitedog9487/WebAPI/Controller/Message.java index 8b5637c..2ef24c2 100644 --- a/src/main/java/xyz/thewhitedog9487/WebAPI/Controller/Message.java +++ b/src/main/java/xyz/thewhitedog9487/WebAPI/Controller/Message.java @@ -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 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); diff --git a/src/main/java/xyz/thewhitedog9487/WebAPI/GlobalSharedBean.java b/src/main/java/xyz/thewhitedog9487/WebAPI/GlobalSharedBean.java index 87fd387..95dbb27 100644 --- a/src/main/java/xyz/thewhitedog9487/WebAPI/GlobalSharedBean.java +++ b/src/main/java/xyz/thewhitedog9487/WebAPI/GlobalSharedBean.java @@ -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(); } }