쿠키 탈취와 CSRF 방지를 위한 구현 방법
java
Copy
// Spring Security 설정
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyOff()))
.headers(headers -> headers
.contentSecurityPolicy("script-src 'self'"))
.cookie(cookie -> cookie
.secure(true)// HTTPS만 사용
.httpOnly(true)// JavaScript에서 접근 불가
.sameSite("Strict"));// 동일 도메인에서만 쿠키 전송
return http.build();
}
}
java
Copy
@Controller
public class AuthController {
@Autowired
private CsrfTokenRepository csrfTokenRepository;
// CSRF 토큰 생성 및 쿠키 저장
@GetMapping("/csrf-token")
public ResponseEntity<Map<String, String>> getCsrfToken(HttpServletRequest request, HttpServletResponse response) {
CsrfToken token = csrfTokenRepository.generateToken(request);
csrfTokenRepository.saveToken(token, request, response);
Map<String, String> body = new HashMap<>();
body.put("token", token.getToken());
return ResponseEntity.ok(body);
}
// CSRF 토큰 검증이 필요한 엔드포인트
@PostMapping("/api/data")
public ResponseEntity<?> processData(@RequestHeader("X-CSRF-TOKEN") String token) {
// 토큰 검증 로직
if (!isValidToken(token)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
return ResponseEntity.ok("처리 완료");
}
}
javascript
Copy
// API 요청 시 CSRF 토큰 포함
async function fetchWithCsrf(url, options = {}) {
// CSRF 토큰 가져오기
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('XSRF-TOKEN'))
?.split('=')[1];
const defaultHeaders = {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
};
const response = await fetch(url, {
...options,
credentials: 'include',// 쿠키 포함
headers: {
...defaultHeaders,
...options.headers
}
});
if (!response.ok) {
throw new Error('API request failed');
}
return response.json();
}
// 사용 예시
try {
const data = await fetchWithCsrf('/api/data', {
method: 'POST',
body: JSON.stringify({ key: 'value' })
});
} catch (error) {
console.error('Request failed:', error);
}
java
Copy
@Configuration
public class SessionConfig {
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
// 세션 설정
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring()
.requestMatchers("/resources/**");
}
// 세션 리스너
@Component
public class SessionListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent se) {
HttpSession session = se.getSession();
session.setMaxInactiveInterval(1800);// 30분
}
}
}
java
Copy
// HTML 이스케이프 유틸리티
public class SecurityUtil {
public static String escapeHtml(String html) {
if (html == null) {
return null;
}
return html.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\\"", """)
.replace("'", "'");
}
}
// 컨트롤러에서 사용
@PostMapping("/comment")
public ResponseEntity<?> addComment(@RequestBody String comment) {
String safeComment = SecurityUtil.escapeHtml(comment);
// 저장 로직
return ResponseEntity.ok().build();
}
java
Copy
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secretKey;
// 토큰 생성
public String createToken(String username) {
Claims claims = Jwts.claims().setSubject(username);
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + 1800000))// 30분
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}
java
Copy
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SecurityInterceptor());
}
public class SecurityInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
response.setHeader("X-XSS-Protection", "1; mode=block");
response.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
return true;
}
}
}
주요 보안 포인트: