Spring Security 6.x에서 deprecated 경고를 무시하고 있었다면, 7.0으로 올릴 때 어떤 일이 벌어질까요?

Spring Security 7.0은 단순한 마이너 업데이트가 아닙니다. MFA 기본 지원, One-Time Token Login, DPoP 같은 현대적인 인증 메커니즘이 프레임워크 레벨에서 들어왔고, 6.x에서 deprecated 처리되었던 API들이 완전히 제거되었습니다. 이 글에서는 7.0의 핵심 변경점과 마이그레이션 시 주의할 포인트를 정리합니다.

7.0 한 줄 요약

Spring Security 7.0 = 6.x에서 deprecated된 것 제거 + 현대적 인증 메커니즘 내장 + Authorization Server 통합.

주요 변경점을 카테고리별로 나누면 이렇습니다.

카테고리내용
새 기능MFA, One-Time Token, DPoP, Passkeys
통합Spring Authorization Server 기본 포함
제거6.x deprecated API 전량 삭제
DSLLambda DSL 전면 적용, 메서드 체이닝 방식 변경

MFA(Multi-Factor Authentication) 기본 지원

6.x까지는 MFA를 구현하려면 커스텀 필터를 직접 만들어야 했습니다. 7.0에서는 mfa() DSL로 설정할 수 있습니다.

JAVA
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .formLogin(Customizer.withDefaults())
        .mfa(mfa -> mfa
            // TOTP 기반 MFA 활성화
            .totpAuthentication(totp -> totp
                .issuer("my-app")
                .codeLength(6)
            )
        );
    return http.build();
}

핵심 포인트를 정리하면 이렇습니다.

  • 1차 인증(아이디/비밀번호) 이후 2차 인증(TOTP) 단계가 자동으로 추가됩니다
  • TotpAuthenticationProvider가 내장되어, Google Authenticator 같은 TOTP 앱과 바로 연동됩니다
  • 2차 인증 페이지도 기본 제공되며, 커스터마이징 가능합니다

공부하다 보니 기존에 커스텀 필터로 MFA를 구현한 프로젝트가 있다면, 7.0 내장 MFA로 교체할지 기존 구현을 유지할지 결정해야 합니다. 기존 구현이 잘 동작하고 있다면 급하게 바꿀 필요는 없지만, 신규 프로젝트라면 내장 MFA가 훨씬 간편합니다.

One-Time Token Login (매직링크)

비밀번호 없이 이메일로 일회용 링크를 보내서 로그인하는 방식입니다. Slack이나 Notion의 매직링크를 떠올리면 됩니다.

JAVA
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .oneTimeTokenLogin(ott -> ott
            // 토큰 생성 후 이메일 전송 로직
            .tokenGenerationSuccessHandler(
                new SendLinkOneTimeTokenGenerationSuccessHandler("/login/ott")
            )
            // 토큰 저장소 설정 (기본: 인메모리)
            .tokenService(new JdbcOneTimeTokenService(dataSource))
        );
    return http.build();
}

동작 흐름은 간단합니다.

  1. 사용자가 이메일 주소를 입력합니다
  2. 서버가 일회용 토큰을 생성하고, 해당 이메일로 로그인 링크를 보냅니다
  3. 사용자가 링크를 클릭하면 토큰 검증 후 인증이 완료됩니다

프로덕션에서는 InMemoryOneTimeTokenService 대신 JdbcOneTimeTokenService를 사용해야 합니다. 서버가 재시작되면 인메모리 토큰이 사라지기 때문입니다.

커스텀 토큰 전송 핸들러

실제로는 이메일 전송 로직을 직접 구현해야 합니다.

JAVA
public class EmailOneTimeTokenHandler
        implements OneTimeTokenGenerationSuccessHandler {

    private final JavaMailSender mailSender;

    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       OneTimeToken oneTimeToken) throws IOException {
        // 매직링크 URL 생성
        String loginUrl = "https://my-app.com/login/ott?token="
                + oneTimeToken.getTokenValue();

        // 이메일 전송
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(oneTimeToken.getUsername());
        message.setSubject("로그인 링크");
        message.setText("아래 링크를 클릭하면 로그인됩니다:\n" + loginUrl);
        mailSender.send(message);

        // 안내 페이지로 리다이렉트
        response.sendRedirect("/login/ott/sent");
    }
}

DPoP (Demonstration of Proof-of-Possession)

DPoP는 OAuth2 액세스 토큰을 특정 클라이언트에 바인딩하는 메커니즘입니다. Bearer 토큰의 가장 큰 약점은 탈취되면 누구나 사용할 수 있다는 점인데, DPoP는 이 문제를 해결합니다.

PLAINTEXT
일반 Bearer 토큰:
  토큰 탈취 → 공격자가 그대로 사용 가능

DPoP 토큰:
  토큰 탈취 → 클라이언트의 개인키가 없으면 사용 불가

리소스 서버 설정

JAVA
@Bean
SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
    http
        .oauth2ResourceServer(oauth2 -> oauth2
            .jwt(jwt -> jwt
                // DPoP 검증 활성화
                .dpop(Customizer.withDefaults())
            )
        );
    return http.build();
}

클라이언트 설정

JAVA
@Bean
SecurityFilterChain clientFilterChain(HttpSecurity http) throws Exception {
    http
        .oauth2Client(oauth2 -> oauth2
            .authorizationCodeGrant(code -> code
                // DPoP 바인딩 활성화
                .dpop(dpop -> dpop
                    .keyPairProvider(new InMemoryDPoPKeyPairProvider())
                )
            )
        );
    return http.build();
}

DPoP가 적용되면 액세스 토큰 요청 시 클라이언트가 공개키로 서명한 DPoP proof를 함께 보냅니다. 리소스 서버는 토큰과 proof를 함께 검증하므로, 토큰만 탈취해서는 API를 호출할 수 없습니다.

PKCE for Confidential Clients

기존에는 PKCE(Proof Key for Code Exchange)가 퍼블릭 클라이언트(SPA, 모바일)에서만 권장되었습니다. 7.0부터는 ** 기밀 클라이언트(서버 사이드 앱)에서도 PKCE가 기본 적용 **됩니다.

JAVA
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .oauth2Login(oauth2 -> oauth2
            .authorizationEndpoint(auth -> auth
                // 기밀 클라이언트에서도 PKCE 자동 적용 (7.0 기본값)
                .authorizationRequestResolver(
                    new PkceOAuth2AuthorizationRequestResolver(
                        clientRegistrationRepository
                    )
                )
            )
        );
    return http.build();
}

이 변경의 배경은 간단합니다. OAuth 2.1 명세에서 모든 클라이언트 유형에 PKCE를 권장하고 있기 때문입니다. Authorization Code를 가로채는 공격은 기밀 클라이언트에서도 발생할 수 있으므로, 추가 보호 계층으로서 PKCE가 의미 있습니다.

Passkeys/WebAuthn

7.0에서는 Passkeys(WebAuthn) 기반 비밀번호 없는 인증도 기본 지원됩니다. 이 부분은 내용이 많아 별도 글에서 다루겠지만, 설정의 핵심만 보면 이렇습니다.

JAVA
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .webAuthn(webAuthn -> webAuthn
            // Relying Party 설정
            .rpName("My Application")
            .rpId("my-app.com")
            .allowedOrigins("https://my-app.com")
        );
    return http.build();
}

Passkeys는 디바이스의 생체인증(지문, Face ID)이나 PIN을 사용하므로, 비밀번호 유출 위험이 원천적으로 사라집니다. MFA와 결합하면 보안 수준이 크게 올라갑니다.

Spring Authorization Server 내장

6.x까지는 Spring Authorization Server가 별도 프로젝트(spring-security-oauth2-authorization-server)로 존재했습니다. 7.0부터는 Spring Security 프로젝트에 통합됩니다.

JAVA
@Bean
SecurityFilterChain authorizationServerFilterChain(HttpSecurity http)
        throws Exception {
    // Authorization Server 기본 설정 적용
    OAuth2AuthorizationServerConfigurer authorizationServer =
        OAuth2AuthorizationServerConfigurer.authorizationServer();

    http
        .securityMatcher(authorizationServer.getEndpointsMatcher())
        .with(authorizationServer, authServer -> authServer
            // OIDC 활성화
            .oidc(Customizer.withDefaults())
        )
        .authorizeHttpRequests(authorize -> authorize
            .anyRequest().authenticated()
        );

    return http.build();
}

기존에 spring-security-oauth2-authorization-server 의존성을 별도로 추가했다면, 7.0에서는 spring-boot-starter-oauth2-authorization-server로 통합된 스타터를 사용합니다.

GRADLE
// 6.x
implementation 'org.springframework.security:spring-security-oauth2-authorization-server:1.3.x'

// 7.0 — Spring Boot 스타터로 통합
implementation 'org.springframework.boot:spring-boot-starter-oauth2-authorization-server'

Deprecated API 제거

7.0의 가장 큰 변경점은 6.x에서 deprecated된 API가 전부 제거 되었다는 것입니다. 대표적인 제거 항목을 정리합니다.

제거된 메서드들

6.x (deprecated)7.0 대체 방법
and() 메서드 체이닝Lambda DSL 사용
authorizeRequests()authorizeHttpRequests()
antMatchers()requestMatchers()
mvcMatchers()requestMatchers()
regexMatchers()requestMatchers()
access("hasRole('ADMIN')") (SpEL 문자열)access(AuthorizationManagers.hasRole("ADMIN"))

마이그레이션 예시

JAVA
// ---- 6.x (deprecated 방식) ----
http
    .authorizeRequests()                    // deprecated
        .antMatchers("/admin/**")           // deprecated
            .hasRole("ADMIN")
        .antMatchers("/api/**")             // deprecated
            .authenticated()
        .anyRequest().permitAll()
    .and()                                  // deprecated
    .formLogin();

// ---- 7.0 (새로운 방식) ----
http
    .authorizeHttpRequests(authorize -> authorize
        .requestMatchers("/admin/**").hasRole("ADMIN")
        .requestMatchers("/api/**").authenticated()
        .anyRequest().permitAll()
    )
    .formLogin(Customizer.withDefaults());

and() 메서드가 제거되면서 모든 설정이 Lambda DSL 기반으로 바뀌었습니다. 사실 6.x에서도 Lambda DSL을 쓸 수 있었기 때문에, 미리 전환해 두었다면 7.0 마이그레이션이 훨씬 수월합니다.

Lambda DSL — 유일한 설정 방식

7.0에서는 Lambda DSL이 ** 유일한 설정 방식 **이 되었습니다. 패턴은 일관적입니다. ** 모든 설정 메서드가 Customizer<T> 람다를 인자로 받는 방식 **으로 통일되었습니다.

JAVA
// 6.x — 메서드 체이닝 (7.0에서 제거)
http.csrf().ignoringRequestMatchers("/api/**");
http.cors().configurationSource(corsConfigurationSource());
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

// 7.0 — Lambda DSL (유일한 방식)
http.csrf(csrf -> csrf.ignoringRequestMatchers("/api/**"));
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

6.x → 7.0 마이그레이션 체크리스트

마이그레이션을 진행할 때 순서대로 확인하면 좋은 항목들입니다.

1단계 — deprecated 경고 해결

가장 먼저 할 일은 6.x에서 발생하는 ** 모든 deprecated 경고를 해결 **하는 것입니다. 7.0에서 제거되는 API가 정확히 deprecated 경고를 발생시키는 API이기 때문입니다.

BASH
# 빌드 시 deprecated 경고 확인
./gradlew compileJava -Xlint:deprecation

2단계 — requestMatchers 전환

JAVA
// 이것들을 전부 찾아서
.antMatchers("/api/**")
.mvcMatchers("/api/**")
.regexMatchers("/api/.*")

// 이것으로 변경
.requestMatchers("/api/**")

3단계 — Lambda DSL 전환

JAVA
// and() 체이닝을 모두 제거하고 Lambda 방식으로 전환
http
    .httpBasic(Customizer.withDefaults())
    .formLogin(form -> form
        .loginPage("/login")
        .permitAll()
    )
    .logout(logout -> logout
        .logoutSuccessUrl("/")
    );

4단계 — 의존성 업데이트 및 테스트

Spring Boot 4.0으로 올리면 Security 7.0이 자동으로 따라옵니다. 의존성 업데이트 후에는 인증/인가 관련 테스트를 반드시 전체 실행해야 합니다.

GRADLE
// build.gradle
plugins {
    id 'org.springframework.boot' version '4.0.0'
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
}

마이그레이션 요약 테이블

단계작업비고
1deprecated 경고 전량 해결-Xlint:deprecation
2antMatchersrequestMatchers정규식 매처 포함
3and() 제거, Lambda DSL 전환Customizer.withDefaults() 활용
4Spring Boot 4.0 + Security 7.0 의존성Authorization Server 통합 확인
5전체 테스트 실행특히 인증/인가 테스트
6새 기능 도입 검토 (MFA, DPoP 등)선택 사항

기억할 포인트

  • 7.0 마이그레이션의 80%는 6.x deprecated 해결 입니다. 미리 해두면 버전 올리는 날 고생이 줄어듭니다
  • MFA, One-Time Token, DPoP는 전부 선택 기능입니다. 기존 인증 방식이 잘 동작하고 있다면 급하게 도입할 필요는 없습니다
  • Lambda DSL이 유일한 설정 방식이 되었으므로, and() 체이닝을 쓰고 있다면 지금 바로 전환하는 것을 권장합니다
  • Authorization Server가 내장되면서 별도 의존성 관리가 필요 없어졌습니다. 단, 기존에 별도 버전을 사용하던 프로젝트는 의존성 충돌을 확인해야 합니다
댓글 로딩 중...