Spring Security 7.0 — 주요 변경점과 마이그레이션 가이드
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 전량 삭제 |
| DSL | Lambda DSL 전면 적용, 메서드 체이닝 방식 변경 |
MFA(Multi-Factor Authentication) 기본 지원
6.x까지는 MFA를 구현하려면 커스텀 필터를 직접 만들어야 했습니다. 7.0에서는 mfa() DSL로 설정할 수 있습니다.
@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의 매직링크를 떠올리면 됩니다.
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.oneTimeTokenLogin(ott -> ott
// 토큰 생성 후 이메일 전송 로직
.tokenGenerationSuccessHandler(
new SendLinkOneTimeTokenGenerationSuccessHandler("/login/ott")
)
// 토큰 저장소 설정 (기본: 인메모리)
.tokenService(new JdbcOneTimeTokenService(dataSource))
);
return http.build();
}
동작 흐름은 간단합니다.
- 사용자가 이메일 주소를 입력합니다
- 서버가 일회용 토큰을 생성하고, 해당 이메일로 로그인 링크를 보냅니다
- 사용자가 링크를 클릭하면 토큰 검증 후 인증이 완료됩니다
프로덕션에서는 InMemoryOneTimeTokenService 대신 JdbcOneTimeTokenService를 사용해야 합니다. 서버가 재시작되면 인메모리 토큰이 사라지기 때문입니다.
커스텀 토큰 전송 핸들러
실제로는 이메일 전송 로직을 직접 구현해야 합니다.
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는 이 문제를 해결합니다.
일반 Bearer 토큰:
토큰 탈취 → 공격자가 그대로 사용 가능
DPoP 토큰:
토큰 탈취 → 클라이언트의 개인키가 없으면 사용 불가
리소스 서버 설정
@Bean
SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
http
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
// DPoP 검증 활성화
.dpop(Customizer.withDefaults())
)
);
return http.build();
}
클라이언트 설정
@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가 기본 적용 **됩니다.
@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) 기반 비밀번호 없는 인증도 기본 지원됩니다. 이 부분은 내용이 많아 별도 글에서 다루겠지만, 설정의 핵심만 보면 이렇습니다.
@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 프로젝트에 통합됩니다.
@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로 통합된 스타터를 사용합니다.
// 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")) |
마이그레이션 예시
// ---- 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> 람다를 인자로 받는 방식 **으로 통일되었습니다.
// 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이기 때문입니다.
# 빌드 시 deprecated 경고 확인
./gradlew compileJava -Xlint:deprecation
2단계 — requestMatchers 전환
// 이것들을 전부 찾아서
.antMatchers("/api/**")
.mvcMatchers("/api/**")
.regexMatchers("/api/.*")
// 이것으로 변경
.requestMatchers("/api/**")
3단계 — Lambda DSL 전환
// and() 체이닝을 모두 제거하고 Lambda 방식으로 전환
http
.httpBasic(Customizer.withDefaults())
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/")
);
4단계 — 의존성 업데이트 및 테스트
Spring Boot 4.0으로 올리면 Security 7.0이 자동으로 따라옵니다. 의존성 업데이트 후에는 인증/인가 관련 테스트를 반드시 전체 실행해야 합니다.
// build.gradle
plugins {
id 'org.springframework.boot' version '4.0.0'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
}
마이그레이션 요약 테이블
| 단계 | 작업 | 비고 |
|---|---|---|
| 1 | deprecated 경고 전량 해결 | -Xlint:deprecation |
| 2 | antMatchers → requestMatchers | 정규식 매처 포함 |
| 3 | and() 제거, Lambda DSL 전환 | Customizer.withDefaults() 활용 |
| 4 | Spring 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가 내장되면서 별도 의존성 관리가 필요 없어졌습니다. 단, 기존에 별도 버전을 사용하던 프로젝트는 의존성 충돌을 확인해야 합니다