백엔드 21일차 - 스프링 시큐리티 : CORS, CSRF, JWT

parang·2025년 5월 13일

LG CNS AM Inspire Camp 2기

목록 보기
32/50

✅ CORS

🔸 개념

  • 브라우저 보안 정책으로 인한 다른 도메인의 요청 차단
  • 리액트 + 스프링으로 프로젝트를 구축할 때 에러가 발생한다.
  • 오리진(포트)가 다르기 때문
  • 쿠키 사용이라면 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);  // 쿠키/인증정보 허용
    }
}

  1. @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"
)
  1. 스프링 시큐리티에서 설정
---
@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]

✅ CSRF

🔸 개념

  • 사용자의 인증 정보를 탈취하여 요청을 보내는 공격 방식.

🔸 기본 원리

  • 서버는 사용자가 의도한 요청인지 검증 방법이 없음 따라서, CSRF 토큰을 발급하고, 요청마다 이 토큰을 함께 제출

🔸 코드


@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();
-> 비활성화

✅ JWT란?

🔸 개념
사용자 인증 정보를 json형태로 담아 서명한 토큰

🔸 기본 원리

  • 로그인 성공 시 서버가 JWT를 클라이언트에 발급하고, 이후 클라이언트는 이 토큰을 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();
    }
}

프론트 + 백엔드 CORS 실전

  1. 시큐리티 x 백엔드

❗ 프론트 : 오리진이 다르면 프론트 쪽에서 credentials 허용 해주어야 제대로 쿠키 처리 가능.

❗ 백엔드 : @CrossOrigin(origins = "http://localhost:5173", allowCredentials = "true")

  1. 시큐리티 o 백엔드 서버

  2. 시큐리티 + jwt (bearer)
    -> jwt 헤더 : csrf는 안전 xss 위험

  3. xss 개발자만 조심하면 됨

  4. 코드 작성이 편함
    = 많이 사용

  5. 시큐리티 + jwt (쿠키)
    -> httponly 속성 + 쿠키에 저장되면 js로 값을 제어할 수 없음
    이유 : xss공격을 막기 위해서.
    세션, 로컬스토리지 중요한 값 탈취 가능성
    csrf는 위험 xss 안전

profile
파랑입니다.

0개의 댓글