[api] swagger 셋팅중
- 메모리 방식으로 수정
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"mcp__jetbrains-file__EditFile"
|
"mcp__jetbrains-file__EditFile",
|
||||||
|
"mcp__jetbrains-file__WriteFile"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ package com.alist.api;
|
|||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||||
import org.springframework.cache.annotation.EnableCaching;
|
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@ConfigurationPropertiesScan
|
@ConfigurationPropertiesScan
|
||||||
@EnableCaching
|
|
||||||
public class ApiApplication {
|
public class ApiApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package com.alist.api.config.cache;
|
||||||
|
|
||||||
|
import com.alist.api.modules.auth.vo.TestCorsOriginVo;
|
||||||
|
import com.alist.api.modules.auth.mapper.TestCorsMapper;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CorsAllowedOriginsCache {
|
||||||
|
|
||||||
|
private final TestCorsMapper testCorsMapper;
|
||||||
|
|
||||||
|
// Thread-safe Map
|
||||||
|
private Map<Integer, List<String>> corsMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 서버 시작 시 DB에서 전체 CORS 목록 로딩
|
||||||
|
*/
|
||||||
|
@PostConstruct
|
||||||
|
public void loadFromDatabase() {
|
||||||
|
refresh();
|
||||||
|
log.info("CORS allowed origins loaded from database");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DB에서 전체 CORS 목록 다시 로딩
|
||||||
|
*/
|
||||||
|
public synchronized void refresh() {
|
||||||
|
try {
|
||||||
|
List<TestCorsOriginVo> allCors = testCorsMapper.selectTestCorsAllowedList();
|
||||||
|
|
||||||
|
Map<Integer, List<String>> newMap = new HashMap<>();
|
||||||
|
|
||||||
|
for (TestCorsOriginVo cors : allCors) {
|
||||||
|
Integer userTokenIdx = cors.getUserTokenIdx();
|
||||||
|
String allowedOrigin = cors.getAllowedOrigin();
|
||||||
|
|
||||||
|
newMap.computeIfAbsent(userTokenIdx, k -> new ArrayList<>())
|
||||||
|
.add(allowedOrigin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전체 교체
|
||||||
|
this.corsMap = new ConcurrentHashMap<>(newMap);
|
||||||
|
|
||||||
|
log.info("CORS cache refreshed. Total tokens: {}, Total origins: {}",
|
||||||
|
corsMap.size(),
|
||||||
|
corsMap.values().stream().mapToInt(List::size).sum());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to refresh CORS cache", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* user_token_idx 기반 허용 도메인 목록 조회
|
||||||
|
* @param userTokenIdx user_token_idx (null이면 기본값 0만 조회)
|
||||||
|
* @return 허용 도메인 리스트
|
||||||
|
*/
|
||||||
|
public List<String> getAllowedOrigins(Integer userTokenIdx) {
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
|
||||||
|
// 기본값 (0) 추가
|
||||||
|
List<String> defaultOrigins = corsMap.get(0);
|
||||||
|
if (defaultOrigins != null) {
|
||||||
|
result.addAll(defaultOrigins);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 특정 토큰값 추가
|
||||||
|
if (userTokenIdx != null && userTokenIdx != 0) {
|
||||||
|
List<String> tokenOrigins = corsMap.get(userTokenIdx);
|
||||||
|
if (tokenOrigins != null) {
|
||||||
|
result.addAll(tokenOrigins);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 캐시 상태 조회
|
||||||
|
* @return 캐시 상태 정보 (토큰별 도메인 목록)
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getCacheStatus() {
|
||||||
|
Map<String, Object> status = new HashMap<>();
|
||||||
|
|
||||||
|
int totalOrigins = corsMap.values().stream().mapToInt(List::size).sum();
|
||||||
|
|
||||||
|
status.put("totalTokens", corsMap.size());
|
||||||
|
status.put("totalOrigins", totalOrigins);
|
||||||
|
status.put("details", new HashMap<>(corsMap));
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
package com.alist.api.config.filter;
|
package com.alist.api.config.filter;
|
||||||
|
|
||||||
|
import com.alist.api.config.cache.CorsAllowedOriginsCache;
|
||||||
import com.alist.api.config.jwt.JwtTokenProvider;
|
import com.alist.api.config.jwt.JwtTokenProvider;
|
||||||
import com.alist.api.modules.auth.mapper.TestCorsMapper;
|
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
@@ -17,14 +19,11 @@ import java.util.List;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class DynamicCorsFilter extends OncePerRequestFilter {
|
public class DynamicCorsFilter extends OncePerRequestFilter {
|
||||||
private final TestCorsMapper testCorsMapper;
|
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
|
||||||
|
|
||||||
public DynamicCorsFilter(TestCorsMapper testCorsMapper, JwtTokenProvider jwtTokenProvider) {
|
private final CorsAllowedOriginsCache corsCache;
|
||||||
this.testCorsMapper = testCorsMapper;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
this.jwtTokenProvider = jwtTokenProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request,
|
protected void doFilterInternal(HttpServletRequest request,
|
||||||
@@ -34,25 +33,39 @@ public class DynamicCorsFilter extends OncePerRequestFilter {
|
|||||||
String origin = request.getHeader("Origin");
|
String origin = request.getHeader("Origin");
|
||||||
Integer userTokenIdx = extractUserTokenIdx(request);
|
Integer userTokenIdx = extractUserTokenIdx(request);
|
||||||
|
|
||||||
// DB에서 허용 도메인 조회
|
// 메모리에서 허용 도메인 조회 (DB 조회 없음!)
|
||||||
List<String> allowedOrigins = testCorsMapper.selectTestCorsAllowedList(userTokenIdx);
|
List<String> allowedOrigins = corsCache.getAllowedOrigins(userTokenIdx);
|
||||||
|
|
||||||
|
// Origin 허용 여부 체크
|
||||||
|
boolean isAllowed = false;
|
||||||
|
if (origin != null) {
|
||||||
|
// 1. 정확히 일치하는 경우
|
||||||
|
if (allowedOrigins.contains("*") || allowedOrigins.contains(origin)) {
|
||||||
|
isAllowed = true;
|
||||||
|
}
|
||||||
|
// 2. localhost는 모든 포트 허용
|
||||||
|
else if ((origin.startsWith("http://localhost:") || origin.equals("http://localhost"))
|
||||||
|
&& allowedOrigins.contains("http://localhost")) {
|
||||||
|
isAllowed = true;
|
||||||
|
}
|
||||||
|
else if ((origin.startsWith("https://localhost:") || origin.equals("https://localhost"))
|
||||||
|
&& allowedOrigins.contains("https://localhost")) {
|
||||||
|
isAllowed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CORS 체크 결과 로깅
|
// CORS 체크 결과 로깅
|
||||||
if (origin != null) {
|
if (origin != null) {
|
||||||
boolean isAllowed = allowedOrigins.contains("*") || allowedOrigins.contains(origin);
|
|
||||||
|
|
||||||
if (isAllowed && log.isDebugEnabled()) {
|
if (isAllowed && log.isDebugEnabled()) {
|
||||||
// 허용된 경우 DEBUG 레벨
|
|
||||||
log.debug("CORS allowed - Origin: {}, UserTokenIdx: {}", origin, userTokenIdx);
|
log.debug("CORS allowed - Origin: {}, UserTokenIdx: {}", origin, userTokenIdx);
|
||||||
} else if (!isAllowed) {
|
} else if (!isAllowed) {
|
||||||
// 차단된 경우 WARN 레벨 (보안 모니터링)
|
|
||||||
log.warn("CORS blocked - Origin: {}, UserTokenIdx: {}, AllowedOrigins: {}",
|
log.warn("CORS blocked - Origin: {}, UserTokenIdx: {}, AllowedOrigins: {}",
|
||||||
origin, userTokenIdx, allowedOrigins);
|
origin, userTokenIdx, allowedOrigins);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Origin이 허용 목록에 있거나 "*"가 있으면 CORS 헤더 설정
|
// CORS 헤더 설정
|
||||||
if (origin != null && (allowedOrigins.contains("*") || allowedOrigins.contains(origin))) {
|
if (isAllowed) {
|
||||||
response.setHeader("Access-Control-Allow-Origin", origin);
|
response.setHeader("Access-Control-Allow-Origin", origin);
|
||||||
response.setHeader("Access-Control-Allow-Credentials", "true");
|
response.setHeader("Access-Control-Allow-Credentials", "true");
|
||||||
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import org.springframework.web.bind.annotation.RequestBody;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@Tag(name = "98. 테스트 인증", description = "테스트 사용자 로그인 및 API Key 인증 관련 API")
|
@Tag(name = "01. 테스트 인증", description = "테스트 사용자 로그인 및 API Key 인증 관련 API")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/test")
|
@RequestMapping("/test")
|
||||||
public class TestAuthController {
|
public class TestAuthController {
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.alist.api.modules.auth;
|
||||||
|
|
||||||
|
import com.alist.api.common.response.ApiResponse;
|
||||||
|
import com.alist.api.common.response.ApiResponseCode;
|
||||||
|
import com.alist.api.config.cache.CorsAllowedOriginsCache;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@Tag(name = "03. 테스트 CRS 갱신", description = "테스트 CORS 설정 관리 API")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/test")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TestCorsAdminController {
|
||||||
|
|
||||||
|
private final CorsAllowedOriginsCache corsCache;
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "CORS 화이트리스트 갱신",
|
||||||
|
description = "DB에서 CORS 허용 도메인 목록을 다시 로딩합니다. DB에 도메인 추가 후 호출하세요."
|
||||||
|
)
|
||||||
|
@PostMapping("/reloadCors")
|
||||||
|
public ResponseEntity<ApiResponse<java.util.Map<String, Object>>> reloadCors() {
|
||||||
|
corsCache.refresh();
|
||||||
|
java.util.Map<String, Object> cacheStatus = corsCache.getCacheStatus();
|
||||||
|
return ApiResponse.entity(cacheStatus, ApiResponseCode.CODE_200);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
package com.alist.api.modules.auth.mapper;
|
package com.alist.api.modules.auth.mapper;
|
||||||
|
|
||||||
|
import com.alist.api.modules.auth.vo.TestCorsOriginVo;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.springframework.cache.annotation.Cacheable;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface TestCorsMapper {
|
public interface TestCorsMapper {
|
||||||
@Cacheable(value = "corsAllowedOrigins", key = "#userTokenIdx ?: 0")
|
/**
|
||||||
List<String> selectTestCorsAllowedList(Integer userTokenIdx);
|
* 전체 CORS 허용 도메인 목록 조회 (user_token_idx별로)
|
||||||
|
* @return CorsOriginDto 리스트
|
||||||
|
*/
|
||||||
|
List<TestCorsOriginVo> selectTestCorsAllowedList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.alist.api.modules.auth.vo;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class TestCorsOriginVo {
|
||||||
|
private Integer userTokenIdx;
|
||||||
|
private String allowedOrigin;
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ import org.springframework.web.bind.annotation.RequestBody;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@Tag(name = "99. 테스트 사용자 관리", description = "테스트 사용자 회원가입 및 관리 API")
|
@Tag(name = "02. 테스트 사용자 관리", description = "테스트 사용자 회원가입 및 관리 API")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/test")
|
@RequestMapping("/test")
|
||||||
|
|||||||
@@ -4,17 +4,13 @@
|
|||||||
|
|
||||||
<mapper namespace="com.alist.api.modules.auth.mapper.TestCorsMapper">
|
<mapper namespace="com.alist.api.modules.auth.mapper.TestCorsMapper">
|
||||||
|
|
||||||
<select id="selectTestCorsAllowedList" resultType="String">
|
<select id="selectTestCorsAllowedList" resultType="com.alist.api.modules.auth.vo.TestCorsOriginVo">
|
||||||
/* TestCorsMapper.selectAllowedOrigins */
|
/* TestCorsMapper.selectTestCorsAllowedList */
|
||||||
SELECT DISTINCT allowed_origin
|
SELECT user_token_idx
|
||||||
|
, allowed_origin
|
||||||
FROM test_cors_allowed_list
|
FROM test_cors_allowed_list
|
||||||
WHERE del_yn = 1
|
WHERE del_yn = 1
|
||||||
AND (
|
ORDER BY user_token_idx, cors_idx
|
||||||
user_token_idx = 0
|
|
||||||
<if test="userTokenIdx != null">
|
|
||||||
OR user_token_idx = #{userTokenIdx}
|
|
||||||
</if>
|
|
||||||
)
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
Reference in New Issue
Block a user