🔸 개념
allowCredentials 사용🔸 기본 원리
Access-Control-Allow-Origin 헤더로 응답🔸 코드
1. 전역 허용
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 모든 URL
.allowedOrigins("http://localhost:3000") // 허용할 Origin
.allowedMethods("GET", "POST", "PUT", "DELETE") // 허용할 HTTP 메서드
.allowCredentials(true); // 쿠키/인증정보 허용
}
}
@CrossOrigin 사용 코드@CrossOrigin(origins = "http://localhost:3000")
@RestController
public class BoardController {
@GetMapping("/boards")
public List<Board> getBoards() {
return boardService.findAll();
}
}
---
@RestController
public class UserController {
@CrossOrigin(origins = "http://localhost:3000")
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginDto loginDto) {
return ResponseEntity.ok().build();
}
}
---
기타 다른 설정도 가능!
@CrossOrigin(
origins = "http://localhost:3000",
allowedHeaders = "*",
methods = {RequestMethod.GET, RequestMethod.POST},
allowCredentials = "true"
)
---
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors() // CorsConfigurationSource 빈이 있을 경우 자동 연동
.and()
.csrf().disable();
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("http://localhost:3000");
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true); // 인증정보 허용 (ex. 쿠키)
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
[요청 발생]
↓
[Spring Security FilterChain]
├─ CORS 정책 검사 (corsFilter)
├─ 인증, 인가 처리
↓
[DispatcherServlet]
↓
[Controller 또는 WebMvcConfigurer]
🔸 개념
🔸 기본 원리
🔸 코드
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf() // 기본적으로 활성화됨
.and()
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll()
);
return http.build();
}
}
---
http.csrf().disable();
-> 비활성화
🔸 개념
사용자 인증 정보를 json형태로 담아 서명한 토큰
🔸 기본 원리
Authorization: Bearer <JWT> 헤더에 넣어 보냄🔸 코드
흐름 : 로그인 요청 -> 토큰 발급 -> 요청마다 헤더에 담아 전송 -> 필터에서 토큰 검사
사용하는 파일 :
JwtUtil.java - JWT 생성, 검증 유틸
JwtAuthFilter.java - 요청마다 토큰 꺼내서 검증
JwtLoginController.java - 로그인 시 토큰 발급
SecurityConfig.java - 필터 등록 및 설정
JwtUtil.java
@Component
public class JwtUtil {
private final String secretKey = "mysecret"; // 환경변수로 관리 권장
private final long expiration = 1000L * 60 * 60; // 1시간
public String createToken(String username, String role) {
Claims claims = Jwts.claims().setSubject(username);
claims.put("role", role);
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + expiration))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
public boolean validateToken(String token) { 유효성 검사
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return true;
} catch (JwtException e) {
return false;
}
}
public String getUsername(String token) {
return Jwts.parser().setSigningKey(secretKey)
.parseClaimsJws(token).getBody().getSubject();
}
}
}
---
JwtAuthFilter.java
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
public JwtAuthFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null && jwtUtil.validateToken(token)) {
String username = jwtUtil.getUsername(token);
String role = jwtUtil.getUserRole(token); // 권한까지 추가
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(username, null,
Collections.emptyList()); // 여기서 권한 리스트도 추가 가능
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (bearer != null && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}
---
JwtLoginController.java
@RestController
@RequestMapping("/api")
public class JwtLoginController {
private final JwtUtil jwtUtil;
public JwtLoginController(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest dto) {
// 실제로는 AuthenticationManager를 사용해야 함 (여긴 예시)
if ("user".equals(dto.getUsername()) && "pass".equals(dto.getPassword())) {
String token = jwtUtil.generateToken(dto.getUsername(), "ROLE_USER");
return ResponseEntity.ok(Map.of("token", token));
}
return ResponseEntity.status(401).body("Invalid credentials");
}
static class LoginRequest {
private String username;
private String password;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
}
---
SecurityConfig.java
@Configuration
public class SecurityConfig {
private final JwtUtil jwtUtil;
public SecurityConfig(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/login").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class)
.build();
}
}
❗ 프론트 : 오리진이 다르면 프론트 쪽에서 credentials 허용 해주어야 제대로 쿠키 처리 가능.
❗ 백엔드 : @CrossOrigin(origins = "http://localhost:5173", allowCredentials = "true")
시큐리티 o 백엔드 서버
시큐리티 + jwt (bearer)
-> jwt 헤더 : csrf는 안전 xss 위험
xss 개발자만 조심하면 됨
코드 작성이 편함
= 많이 사용
시큐리티 + jwt (쿠키)
-> httponly 속성 + 쿠키에 저장되면 js로 값을 제어할 수 없음
이유 : xss공격을 막기 위해서.
세션, 로컬스토리지 중요한 값 탈취 가능성
csrf는 위험 xss 안전