[api] cors 단순화

- user 제거
This commit is contained in:
2026-02-26 16:01:27 +09:00
parent 2d2747f4b9
commit 0651014274
5 changed files with 29 additions and 124 deletions
@@ -8,10 +8,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.stream.Collectors;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j @Slf4j
@Component @Component
@@ -20,8 +18,8 @@ public class CorsAllowedOriginsCache {
private final TestCorsMapper testCorsMapper; private final TestCorsMapper testCorsMapper;
// Thread-safe Map // Thread-safe List (volatile로 가시성 보장)
private Map<Integer, List<String>> corsMap = new ConcurrentHashMap<>(); private volatile List<String> allowedOrigins = new ArrayList<>();
/** /**
* 서버 시작 시 DB에서 전체 CORS 목록 로딩 * 서버 시작 시 DB에서 전체 CORS 목록 로딩
@@ -37,24 +35,16 @@ public class CorsAllowedOriginsCache {
*/ */
public synchronized void refresh() { public synchronized void refresh() {
try { try {
List<TestCorsOriginVo> allCors = testCorsMapper.selectTestCorsAllowedList(); List<String> origins = testCorsMapper.selectTestCorsAllowedList()
.stream()
Map<Integer, List<String>> newMap = new HashMap<>(); .map(TestCorsOriginVo::getAllowedOrigin)
.distinct()
for (TestCorsOriginVo cors : allCors) { .collect(Collectors.toList());
Integer userTokenIdx = cors.getUserTokenIdx();
String allowedOrigin = cors.getAllowedOrigin();
newMap.computeIfAbsent(userTokenIdx, k -> new ArrayList<>())
.add(allowedOrigin);
}
// 전체 교체 // 전체 교체
this.corsMap = new ConcurrentHashMap<>(newMap); this.allowedOrigins = origins;
log.info("CORS cache refreshed. Total tokens: {}, Total origins: {}", log.info("CORS cache refreshed. Total origins: {}", origins.size());
corsMap.size(),
corsMap.values().stream().mapToInt(List::size).sum());
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to refresh CORS cache", e); log.error("Failed to refresh CORS cache", e);
@@ -62,43 +52,18 @@ public class CorsAllowedOriginsCache {
} }
/** /**
* user_token_idx 기반 허용 도메인 목록 조회 * 허용 도메인 목록 조회
* @param userTokenIdx user_token_idx (null이면 기본값 0만 조회)
* @return 허용 도메인 리스트 * @return 허용 도메인 리스트
*/ */
public List<String> getAllowedOrigins(Integer userTokenIdx) { public List<String> getAllowedOrigins() {
List<String> result = new ArrayList<>(); return new ArrayList<>(allowedOrigins);
// 기본값 (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 캐시 상태 정보 (토큰별 도메인 목록) * @return 캐시 상태 정보
*/ */
public Map<String, Object> getCacheStatus() { public List<String> getCacheStatus() {
Map<String, Object> status = new HashMap<>(); return new ArrayList<>(allowedOrigins);
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,7 +1,6 @@
package com.alist.api.config.filter; package com.alist.api.config.filter;
import com.alist.api.config.cache.CorsAllowedOriginsCache; import com.alist.api.config.cache.CorsAllowedOriginsCache;
import com.alist.api.config.jwt.JwtTokenProvider;
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;
@@ -23,7 +22,6 @@ import java.util.List;
public class DynamicCorsFilter extends OncePerRequestFilter { public class DynamicCorsFilter extends OncePerRequestFilter {
private final CorsAllowedOriginsCache corsCache; private final CorsAllowedOriginsCache corsCache;
private final JwtTokenProvider jwtTokenProvider;
@Override @Override
protected void doFilterInternal(HttpServletRequest request, protected void doFilterInternal(HttpServletRequest request,
@@ -31,36 +29,22 @@ public class DynamicCorsFilter extends OncePerRequestFilter {
FilterChain filterChain) throws ServletException, IOException { FilterChain filterChain) throws ServletException, IOException {
String origin = request.getHeader("Origin"); String origin = request.getHeader("Origin");
Integer userTokenIdx = extractUserTokenIdx(request);
// 메모리에서 허용 도메인 조회 (DB 조회 없음!) // 메모리에서 허용 도메인 조회 (DB 조회 없음!)
List<String> allowedOrigins = corsCache.getAllowedOrigins(userTokenIdx); List<String> allowedOrigins = corsCache.getAllowedOrigins();
// Origin 허용 여부 체크 // Origin 허용 여부 체크
boolean isAllowed = false; boolean isAllowed = false;
if (origin != null) { if (origin != null && (allowedOrigins.contains("*") || allowedOrigins.contains(origin))) {
// 1. 정확히 일치하는 경우
if (allowedOrigins.contains("*") || allowedOrigins.contains(origin)) {
isAllowed = true; 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) {
if (isAllowed && log.isDebugEnabled()) { if (isAllowed && log.isDebugEnabled()) {
log.debug("CORS allowed - Origin: {}, UserTokenIdx: {}", origin, userTokenIdx); log.debug("CORS allowed - Origin: {}", origin);
} else if (!isAllowed) { } else if (!isAllowed) {
log.warn("CORS blocked - Origin: {}, UserTokenIdx: {}, AllowedOrigins: {}", log.warn("CORS blocked - Origin: {}, AllowedOrigins: {}", origin, allowedOrigins);
origin, userTokenIdx, allowedOrigins);
} }
} }
@@ -69,7 +53,8 @@ public class DynamicCorsFilter extends OncePerRequestFilter {
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");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With, Accept, Origin, Referer, Content-Disposition, Content-Length"); response.setHeader("Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Requested-With, Accept, Origin, Referer, Content-Disposition, Content-Length");
response.setHeader("Access-Control-Expose-Headers", "Authorization, Set-Cookie"); response.setHeader("Access-Control-Expose-Headers", "Authorization, Set-Cookie");
response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Max-Age", "3600");
} }
@@ -82,49 +67,4 @@ public class DynamicCorsFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
/**
* Request에서 user_token_idx 추출
* JWT 토큰 또는 API Key를 통해 추출
*/
private Integer extractUserTokenIdx(HttpServletRequest request) {
try {
String token = null;
// 1. Authorization Bearer 토큰
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
}
// 2. Cookie에서 accessToken
if (token == null && request.getCookies() != null) {
for (var cookie : request.getCookies()) {
if ("accessToken".equals(cookie.getName())) {
token = cookie.getValue();
break;
}
}
}
// 토큰이 있으면 검증 후 userTokenIdx 추출
if (token != null && jwtTokenProvider.validateToken(token)) {
String subject = jwtTokenProvider.getUserId(token);
// subject를 Integer로 변환 시도
try {
return Integer.parseInt(subject);
} catch (NumberFormatException e) {
// 숫자가 아닌 문자열이면 null 반환 (기본값 0으로 조회)
log.debug("Subject is not a number: {}", subject);
return null;
}
}
} catch (Exception e) {
log.debug("Failed to extract userTokenIdx: {}", e.getMessage());
}
return null; // 기본값 0만 조회
}
} }
@@ -11,6 +11,8 @@ import org.springframework.web.bind.annotation.PostMapping;
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;
import java.util.List;
@Tag(name = "03. 테스트 CRS 갱신", description = "테스트 CORS 설정 관리 API") @Tag(name = "03. 테스트 CRS 갱신", description = "테스트 CORS 설정 관리 API")
@RestController @RestController
@RequestMapping("/test") @RequestMapping("/test")
@@ -24,9 +26,9 @@ public class TestCorsAdminController {
description = "DB에서 CORS 허용 도메인 목록을 다시 로딩합니다. DB에 도메인 추가 후 호출하세요." description = "DB에서 CORS 허용 도메인 목록을 다시 로딩합니다. DB에 도메인 추가 후 호출하세요."
) )
@PostMapping("/reloadCors") @PostMapping("/reloadCors")
public ResponseEntity<ApiResponse<java.util.Map<String, Object>>> reloadCors() { public ResponseEntity<ApiResponse<List<String>>> reloadCors() {
corsCache.refresh(); corsCache.refresh();
java.util.Map<String, Object> cacheStatus = corsCache.getCacheStatus(); List<String> cacheStatus = corsCache.getCacheStatus();
return ApiResponse.entity(cacheStatus, ApiResponseCode.CODE_200); return ApiResponse.entity(cacheStatus, ApiResponseCode.CODE_200);
} }
} }
@@ -6,6 +6,5 @@ import lombok.Setter;
@Getter @Getter
@Setter @Setter
public class TestCorsOriginVo { public class TestCorsOriginVo {
private Integer userTokenIdx;
private String allowedOrigin; private String allowedOrigin;
} }
@@ -6,11 +6,10 @@
<select id="selectTestCorsAllowedList" resultType="com.alist.api.modules.auth.vo.TestCorsOriginVo"> <select id="selectTestCorsAllowedList" resultType="com.alist.api.modules.auth.vo.TestCorsOriginVo">
/* TestCorsMapper.selectTestCorsAllowedList */ /* TestCorsMapper.selectTestCorsAllowedList */
SELECT user_token_idx SELECT allowed_origin
, allowed_origin
FROM test_cors_allowed_list FROM test_cors_allowed_list
WHERE del_yn = 1 WHERE del_yn = 1
ORDER BY user_token_idx, cors_idx ORDER BY cors_idx
</select> </select>
</mapper> </mapper>