• 포스트
  • 도구
  • 작업
  • 포스트
  • 도구
  • 작업
로그인회원가입

© 2005 kaisa.co.kr. All Rights Reserved.

소개개인정보처리방침7083620@hanmail.net
← 목록으로
2025-02-26

Spring Security 6 — 인증·인가·OAuth2·JWT

Spring Security 6 — 인증·인가·OAuth2·JWT

요약: SecurityFilterChain으로 API 인증·인가를 구성하고, JWT와 OAuth2 Resource Server를 구분해서 쓴다.


1. 왜 Security 6인가

인증(Authentication) 은 누구인지 확인하는 것이고, 인가(Authorization) 는 그 주체가 이 요청을 해도 되는지 판단하는 것이다.

Security 6는 WebSecurityConfigurerAdapter를 없애고 SecurityFilterChain Bean으로 체인을 선언한다. antMatchers는 requestMatchers로 바뀌었다. 메서드 보안은 @EnableMethodSecurity다.

JSON API는 세션 쿠키가 기본값이 아니다. CSRF·세션 정책을 API 기준으로 다시 정한다.


2. 세션 없는 API 골격

한 줄 정의: SecurityFilterChain은 요청이 컨트롤러에 닿기 전 필터 순서와 허용 규칙을 조립하는 설정이다.

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(sm ->
                sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health", "/api/auth/**",
                        "/v3/api-docs/**", "/swagger-ui/**")
                    .permitAll()
                .requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults());

        return http.build();
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

CSRF(Cross-Site Request Forgery) 는 브라우저가 쿠키를 자동으로 보낼 때 위조 요청을 막는 보호다. 쿠키 세션이 없으면 보통 끈다. 쿠키 기반 로그인 웹앱에서 끄면 구멍이 난다.

비밀번호는 PasswordEncoder로만 저장한다. httpBasic은 데모용이다.


3. UserDetailsService

한 줄 정의: 로그인 식별자(이메일 등)로 사용자와 권한을 로드하는 포트다.

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserAccountRepository repository;

    public CustomUserDetailsService(UserAccountRepository repository) {
        this.repository = repository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserAccount account = repository.findByEmail(username)
                .orElseThrow(() -> new UsernameNotFoundException(username));
        return User.builder()
                .username(account.getEmail())
                .password(account.getPasswordHash())
                .roles(account.getRole().name())
                .build();
    }
}

.roles(...)는 ROLE_ 접두사를 붙인다. DB에 이미 ROLE_USER로 저장했다면 .authorities(...)를 쓴다.


4. JWT

한 줄 정의: JWT(JSON Web Token) 는 서명된 JSON 클레임이다. 서버가 세션을 저장하지 않고, 클라이언트가 Authorization: Bearer로 보낸다.

흐름: POST /api/auth/login → access(+ refresh) → 필터가 검증 → SecurityContext에 Authentication.

public String createAccessToken(String subject, Collection<String> roles) {
    Instant now = Instant.now();
    return Jwts.builder()
            .subject(subject)
            .claim("roles", roles)
            .issuedAt(Date.from(now))
            .expiration(Date.from(now.plus(15, ChronoUnit.MINUTES)))
            .signWith(secretKey)
            .compact();
}

Access는 5~15분, Refresh는 HttpOnly 쿠키 또는 안전한 저장소에 두고 회전한다. 서명 키는 환경변수다. 라이브러리는 jjwt 또는 spring-security-oauth2-jose(Nimbus)다.

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtTokenProvider tokenProvider;

    public JwtAuthFilter(JwtTokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String token = resolveBearer(request);
        if (token != null && tokenProvider.valid(token)) {
            Authentication auth = tokenProvider.toAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        filterChain.doFilter(request, response);
    }
}

필터는 UsernamePasswordAuthenticationFilter 앞에 넣는다. 만료·서명 실패를 삼키고 통과시키면 익명으로 보호 자원에 닿는다.

자체 파서보다 Authorization Server가 발급한 JWT를 Resource Server로 검증하는 편이 안전하다.


5. 메서드 인가

URL 매처만으로는 리소스 소유권을 표현하기 어렵다.

@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) { }

@PreAuthorize("#email == authentication.name or hasRole('ADMIN')")
@GetMapping("/me/{email}")
public UserResponse me(@PathVariable String email) { }

소유권 검사는 서비스에서도 한 번 더 한다. 경로 변수만 믿으면 IDOR가 난다.


6. OAuth2 Resource Server / Login

OAuth2 Resource Server는 Keycloak·Auth0·Cognito·Spring Authorization Server가 준 JWT를 표준으로 검증한다.

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com/realms/demo
http.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));

브라우저 소셜 로그인은 OAuth2 Login(클라이언트)이다. API 서버의 Bearer 검증과 역할이 다르다.

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope: openid, profile, email

7. CORS와 운영

SPA가 다른 오리진이면 CorsConfigurationSource를 명시한다. * origin과 allowCredentials는 브라우저가 거부한다.

@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://app.example.com"));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE"));
    config.setAllowedHeaders(List.of("*"));
    config.setAllowCredentials(true);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", config);
    return source;
}

Actuator는 health 외 노출을 줄이고 인증한다. 로그인에는 rate limit을 둔다.


8. 테스트와 흔한 실수

@WebMvcTest(controllers = UserController.class)
@Import(SecurityConfig.class)
class UserControllerSecurityTest {

    @Autowired MockMvc mockMvc;

    @Test
    @WithMockUser(roles = "USER")
    void userCannotDelete() throws Exception {
        mockMvc.perform(delete("/api/users/1"))
                .andExpect(status().isForbidden());
    }
}
실수 대안
CSRF를 쿠키 세션 웹에서 disable 토큰/쿠키 설정 유지
JWT를 localStorage에만 장기 보관 짧은 TTL + refresh 회전
URL permit만 하고 소유권 미검사 @PreAuthorize + 서비스 가드
Swagger·Actuator 전부 permitAll 운영에서 제한
평문 비밀번호 BCrypt

연습

  1. /api/public/**만 허용하고 나머지는 인증 필수로 둔다.
  2. 회원가입 시 BCrypt로 저장한다.
  3. JWT 필터 또는 Resource Server issuer-uri로 Bearer를 검증한다.
  4. @PreAuthorize로 ADMIN 전용 삭제를 만든다.

댓글

0

아직 댓글이 없습니다. 첫 댓글을 남겨보세요.

이전 글Spring Data JPA와 영속성 계층다음 글테스트 — JUnit 5, MockMvc, Testcontainers
← 목록으로