Spring Security 소개
Spring Security는 강력하고 매우 유연하게 커스터마이징 할 수 있는 인증(Authentication) 및 접근 제어(Access-Control) 프레임워크입니다. Spring 기반 애플리케이션을 안전하게 보호하기 위한 사실상의 표준이라고 할 수 있습니다. Spring Security의 진정한 강점은 사용자 정의한 요구 사항을 충족할 수 있도록 쉽게 커스터마이징 할 수 있다는 점입니다.
주요 특징은 다음과 같습니다.
- 포괄적이고 확장 가능한 인증(Authentication) 및 권한 부여(Authorization) 지원: 다양한 인증 방식과 권한 관리 기능을 제공하며, 필요에 따라 쉽게 확장하고 커스터마이징 할 수 있습니다.
- 세션 고정 공격, 클릭재킹(clickjacking), 교차 사이트 요청 위조(CSRF: Cross-Site Request Forgery) 등과 같은 공격으로부터 보호: 웹 애플리케이션에서 발생할 수 있는 다양한 보안 위협에 대한 기본적인 보호 기능을 내장하고 있습니다.
- Servlet API 통합: Servlet 기반의 웹 애플리케이션과 원활하게 통합되어 Spring Security의 기능을 쉽게 적용할 수 있습니다.
- Spring Web MVC와의 선택적 통합: Spring Web MVC 프레임워크와 함께 사용하여 더욱 강력하고 편리한 웹 보안 기능을 구현할 수 있습니다.
- 위에서 언급된 내용 외에도 다양한 고급 보안 기능을 제공하여 애플리케이션의 보안 수준을 높일 수 있습니다.
스프링 시큐리티에 대해서 더 자세히 알아보기 전에 배워두면 좋은 보안의 기본 6 원칙입니다.
6 Security Principles
- Trust nothing (아무것도 믿지 마세요.)
- 모든 리퀘스트를 검증해야 합니다.
- 시스템으로 들어오는 모든 데이터들을 validation 해야 합니다.
- Assign least privileges (최소한의 권한만 부여하기)
- 시스템을 디자인할 때부터 보안 요구사항을 고려하는 것이 좋습니다.
- 사용자 역할(role)과 그 권한을 명확하게 정의해두어야 합니다.
- 어떤 레벨(application, database, server)에서든 가능한 최소한의 권한만을 할당해야 합니다.
- Have complete Mediation
- Complete Mediation: 모든 정보에 대한 모든 접근은 승인된 접근이어야 한다는 원칙.
- 모든 접근 시도는 단일하고 중앙 집중화된 보안 게이트를 통과해야 합니다.
- 잘 구현된 보안 필터를 적용하고, 각 사용자의 역할과 접근 권한을 테스트하세요.
- Have defense in depth
- 여러 계층의 보안 메커니즘을 적용하여 하나의 보안 계층이 실패하더라도 다른 계층이 방어할 수 있도록 합니다. Transport, Network, Infrastructure, OS, App 계층 등
- Have Economy of Mechanism (메커니즘의 경제성 확보)
- 보안 아키텍처는 단순해야 합니다. 단순한 시스템이 보호하기 더 쉽습니다.
- Ensure openness of design (설계의 개방성 보장)
- 보안 설계는 OAuth, JWT(JSON 웹 토큰)와 같은 보안 표준을 사용하여 공개적으로 이루어져야 합니다. 보안 결함을 식별하고 수정하기 더 쉽습니다.
Spring MVC는 다음과 같이 작동합니다.
Request => Dispatcher Servlet => Controller(s)
- Dispatcher Servlet은 최전방 컨트롤러로서 작동합니다.
- Dispatcher Servlet은 리퀘스트를 적절한 컨트롤러로 보내는(라우팅) 역할을 합니다.
그렇다면 Spring Security는 어떻게 작동할까요?
Request => Spring Security => Dispatcher Servlet => Controller(s)
- Spring security는 들어오는 모든 리퀘스트를 인터셉트합니다.
- 오직 검증된 요청(리퀘스트)만이 dispatcher servlet으로 전달됩니다.
- 이것은 Security Principle 3번, complete mediation 조건을 충족합니다.
- Spring Security는 여러 겹의 필터(filter chain)를 실행시킵니다. 대략 다음과 같습니다. "보안 검문소 라인"이라고 볼 수 있습니다.
- Authentication(인증)
- Authorization(권한부여)
- CORS(Cross Origin Resource Sharing)
- CSRF(Cross Site Request Forgery)
- Default login/logout page
- Translating exceptions into HTTP Responses(Exception Translation filter)
- 이 필터 체인의 순서는 아주 중요합니다.
- 기본 확인 filters - CORS, CSRF, …
- Authentication filter
- Authorization filter
Cross-Origin Request Sharing(CORS)는 무엇이며 왜 필요할까요?
사용자가 http://my-frontend.com이라는 웹사이트를 보고 있는데 이 웹사이트의 JavaScript 코드가 http://my-backend.com/api/data라는 다른 엔드포인트에 데이터를 요청하려고 하면, 브라우저는 기본적으로 이 요청을 차단합니다. 즉 브라우저가 현재 웹페이지의 출처(Origin)와 다른 출처의 리소스에 대한 AJAX 호출을 허용하지 않습니다. 이는 악의적인 웹사이트가 사용자의 민감한 정보를 다른 도메인의 서버로 몰래 전송하는 것을 방지하기 위한 중요한 보안 메커니즘입니다.
하지만 때로는 의도적으로 다른 도메인 간에 리소스 공유가 필요한 경우가 있습니다. 예를 들어, 프런트엔드 웹 애플리케이션은 별도의 백엔드 API 서버와 통신해야 할 수 있습니다. 이때 CORS 메커니즘이 필요합니다.
CORS는 웹 서버가 어떤 출처의 웹 앱이 자신의 리소스에 접근하는 것을 허용할지 브라우저에게 알려주는 표준입니다. 즉, 브라우저가 다른 출처의 리소스에 AJAX 요청을 보내기 전에 서버에게 "이 요청을 보내도 괜찮은가요?"라고 먼저 물어보고, 서버가 허락하면 요청을 진행하는 방식입니다.
Spring Boot에서는 크게 두 가지 방법으로 CORS를 설정할 수 있습니다.
1. 전역 설정 (Global Configuration): 모든 REST 컨트롤러에 적용
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // 특정 URL 패턴에 대해 CORS 설정
.allowedOrigins("<http://my-frontend.com>", "<https://another-frontend.net>") // 허용할 출처 명시
.allowedMethods("GET", "POST", "PUT", "DELETE") // 허용할 HTTP 메서드 명시
.allowedHeaders("*") // 허용할 요청 헤더 명시
.allowCredentials(true) // 인증 정보 (쿠키, HTTP 인증 등) 허용 여부
.maxAge(3600); // Preflight 요청 결과 캐싱 시간 (초)
}
}
2. 로컬 설정 (Local Configuration): 특정 REST 컨트롤러 또는 메서드에 적용
@RestController
@CrossOrigin(origins = "<http://yet-another-frontend.com>") // 클래스 레벨에서 특정 출처 허용
public class MyController {
@GetMapping("/public")
public String publicEndpoint() {
return "This endpoint is publicly accessible.";
}
@GetMapping("/private")
@CrossOrigin // 메서드 레벨에서 모든 출처 허용
public String privateEndpoint() {
return "This endpoint has default CORS settings (allowing all origins).";
}
@GetMapping("/admin")
@CrossOrigin(origins = "<https://admin-panel.com>", methods = { "GET", "POST" }) // 메서드 레벨에서 특정 출처 및 메서드 허용
public String adminEndpoint() {
return "This endpoint is only accessible from the admin panel.";
}
}
Cross-Site Request Forgery(CSRF)는 무엇인가요?
사용자가 로그아웃 하지 않았을 시 유해 웹사이트가 브라우저 쿠키 정보를 훔쳐가서 사용자의 의도와는 다른 일들을 벌이는 공격입니다.
주의: 세션을 사용하는 게 아니라면 CSRF는 고려 사항이 아닙니다. 만당 stateless API를 사용한다면 CSRF에 대해서 걱정할 필요가 없고 설정을 꺼두어도 됩니다.
설정 끄는 법: SecurityConfiguration 클래스의 SecurityFilterChain bean을 오버라이딩 하면 됩니다.
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // Disable CSRF
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated() // Require authentication for all requests
)
.formLogin() // Enable form login
.and()
.httpBasic(); // Enable basic authentication
return http.build();
}
}
어떻게 CSRF를 예방할 수 있나요?
- Synchronizer token pattern(동기화 토큰 패턴) 사용하기
- 업데이트 (POST, PUT 등) 요청을 보내려면 이전 요청에서 받은 CSRF 토큰을 함께 포함해야 합니다. 서버는 요청과 함께 전달된 CSRF 토큰이 이전 요청에서 발급한 토큰과 일치하는지 확인하여 정상적인 요청인지 판단합니다.
- SameSite 쿠키 (Set-Cookie: SameSite=Strict)
- SameSite=Strict로 설정된 쿠키는 동일한 웹사이트 내에서 발생한 요청에만 전송됩니다.
- 다른 웹사이트에서 해당 웹사이트로 요청을 보낼 때 (예: <form> 태그를 이용한 POST 요청), SameSite=Strict 쿠키는 전송되지 않습니다.
- 설정 방법: application.properties 파일에 server.servlet.session.cookie.same-site=strict를 추가합니다.
Spring Security 기본 설정값 살펴보기
- 전부 authentication이 필요합니다.
- Form Authentication 이 사용됩니다.
- Basic Authentication이 사용됩니다.
- 테스트 유저 "user"가 생성됩니다.
- CSRF 보호가 활성화됩니다.
- CORS 요청은 거부합니다.
- X-Frame-Options의 값은 0입니다. Frames 사용은 불가능하도록 되어있습니다.
Form Authentication과 Basic Authentication은 어떤 차이인가요?
- Form-based Authentication
- 보통 웹에서 사용
- 세션 쿠키인 JSESSIONID 사용
- 기본 로그인/로그아웃 페이지 url과 그에 맞는 UI를 제공합니다.
- Basic Authentication
- 사용자 이름과 비밀번호를 Base64 인코딩한 값을 Authorization: Basic … 헤더로 추가여 리퀘스트를 보냅니다.
- REST API의 가장 기본적인 보안 옵션이지만 쉽게 디코딩할 수 있고, 무제한 시간 동안 valid 하기 때문에 안전하지 않습니다. 또한 authorization 정보(접근권한)는 포함하고 있지 않습니다.
클릭재킹(Clickjacking)과 Frame
클릭재킹은 웹 사용자를 속여서 자신이 의도하지 않은 행동 (예: 버튼 클릭, 링크 클릭, 정보 입력 등)을 수행하도록 만드는 악의적인 기법입니다. 공격자는 투명하거나 거의 보이지 않는 Frame (주로 <iframe>) 을 이용하여 사용자가 보고 있는 정상적인 웹페이지 위에 겹쳐 놓습니다. 사용자는 겉으로 보이는 콘텐츠 (예: 동영상 재생 버튼)를 클릭하려고 하지만, 실제로는 그 위에 겹쳐진 투명한 프레임 속의 공격 대상 웹사이트의 버튼이나 링크를 클릭하게 됩니다.
이러한 이유로 Spring Security가 Frame을 기본적으로 차단하는 것이며, 기본적으로 X-Frame-Options 헤더를 DENY 또는 SAMEORIGIN으로 설정하여 클릭재킹 공격을 방지합니다. 즉, 애플리케이션의 페이지가 다른 웹사이트의 프레임 내에 임베딩되는 것을 막아, 공격자가 투명한 프레임을 겹쳐 사용자를 속이는 것을 어렵게 만듭니다.
인코딩(Encoding), 해싱(Hashing), 암호화(Encryption) 비교
인코딩 (Encoding)
데이터를 한 형태에서 다른 형태로 변환하는 과정입니다. 키나 비밀번호를 사용하지 않습니다. 가역적이기 때문에 원래의 데이터로 다시 되돌릴 수 있습니다. 데이터 보안보다는 압축, 스트리밍, 효율적인 데이터 전송/저장과 같은 목적으로 사용됩니다. 예시: Base64, Wav, MP3
해싱 (Hashing)
데이터를 고정된 길이의 해시 문자열로 변환하는 과정입니다. 단방향 프로세스이므로, 해시된 결과로부터 원래의 데이터를 되돌릴 수 없습니다. 주로 데이터의 무결성 검증에 사용됩니다. 예시: bcrypt, scrypt 사용 사례: 데이터베이스에 비밀번호의 해시 값을 저장합니다. 사용자가 로그인할 때 입력한 비밀번호를 해시하여 데이터베이스에 저장된 해시 값과 비교합니다.
암호화 (Encryption)
키 또는 비밀번호를 사용하여 데이터를 암호화하는 과정입니다. 데이터를 안전하게 보호하는 것이 목표입니다. 암호화된 데이터를 해독하려면 키 또는 비밀번호가 필요합니다. 가역적이며, 암호화에 사용된 키나 비밀번호를 알면 원래의 데이터를 복원할 수 있습니다. 예시: RSA
비밀번호 저장
SHA-256과 같은 기존의 해시 알고리즘은 더 이상 안전하지 않습니다. 현대적인 시스템은 초당 수십억 번의 해시 계산을 수행할 수 있기 때문입니다.
권장 사항: 검증에 1초의 "작업량(Work factor)"이 소요되는 적응형 단방향 함수를 사용하세요. 예시: bcrypt, scrypt, argon2 등
- 적응형 (adaptive): 함수가 시간이 지남에 따라 변화하는 환경이나 조건에 맞춰 자동으로 조정될 수 있음을 의미합니다. 예를 들어, 컴퓨팅 파워가 증가함에 따라 해시 계산에 더 많은 시간이 소요되도록 자동으로 조정될 수 있습니다.
- 단방향 함수 (one-way function): 암호학에서 사용되는 함수로, 계산하기는 쉽지만 역으로 계산하기는 매우 어려운 특성을 가진 함수입니다. 비밀번호를 해시하는 데 적합합니다.
- 작업량 (Work factor): 비밀번호를 검증하는 데 필요한 계산 시간 또는 자원을 의미합니다. 1초의 작업량은 현재 수준의 컴퓨팅 파워에서 안전하다고 여겨지는 값입니다.
Spring Security에서의 암호화
Spring Security는 비밀번호의 단방향 변환을 수행하기 위해서 PasswordEncoder라는 인터페이스를 제공합니다. (이름이 Encoder여서 다소 혼란스러울 수 있지만, 암호화뿐만 아니라 해싱에도 사용됩니다.)
권장 사항: BCryptPasswordEncoder를 사용하는 것입니다. BCrypt는 강력하고 널리 사용되는 적응형 해시 함수 중 하나입니다.
사용자 인증 정보(Credential)는 다음과 같은 곳에 저장할 수 있습니다.
- 인-메모리 (In-memory): 애플리케이션 실행 중에 메모리에 저장하는 방식입니다. 예를 들어, application.properties 파일에 설정할 수 있으며, 주로 테스트 목적으로 사용됩니다. 또는 InMemoryUserDetailsManager를 사용하여 코드에 정의할 수 있습니다.
- 데이터베이스 (Database): JDBC나 JPA와 같은 기술을 이용하여 관계형 데이터베이스에 저장하는 방식입니다. 실제 운영 환경에서 가장 일반적인 방법입니다.
- LDAP (Lightweight Directory Access Protocol): 디렉터리 서비스 및 인증을 위한 개방형 프로토콜입니다. 중앙 집중식 사용자 관리가 필요한 환경에서 사용됩니다.
JSON 웹 토큰(JWT)의 등장
Basic Authentication의 한계
- 만료 시간이 없습니다.
- 사용자 상세 정보를 포함하지 않습니다.
- 쉽게 인코딩 될 수 있습니다.
그렇다면 커스텀 토큰 시스템은 어떤가요?
- 잠재적인 보안 결함이 있을 수 있습니다.
- 서비스 제공자와 소비자 모두 커스터마이징 된 토큰 구조를 이해해야 합니다.
JSON 웹 토큰(JWT)은 서비스 프로바이더/컨슈머 간에 안전하게 정보를 교환하기 위한 개방형 산업 표준입니다. 사용자 디테일과 권한 정보(authorization)를 포함할 수 있습니다. JSON 웹 토큰은 크게 세 부분(Header, Payload, Signature)으로 나뉘는데 이 중 Payload는 만료 날짜 (expire date)나 발급 시간(when was issued)등에 대한 정보를 가지고 있습니다. Signature는 헤더와 페이로드를 특정 알고리즘과 secret을 사용하여 암호화한 값입니다. 이 서명을 통해 토큰의 무결성을 검증할 수 있습니다.
대칭(Symmetric) 키 암호화
암호화와 복호화에 동일한 키를 사용하는 암호화 알고리즘입니다.
비대칭(Asymmetric) 키 암호화 == public key 방식
- 두 개의 키(퍼블릭 키와 프라이빗 키)를 사용하는 암호화 알고리즘입니다.
- 공개 키로 데이터를 암호화하고 개인 키로 암호화된 데이터를 복호화합니다.
- 공개 키는 모든 사람에게 공유하고 개인 키는 안전하게 보관합니다.
JSON 웹 토큰 작동 방식
- JSON 웹 토큰 생성: 다음 정보를 인코딩합니다.
- 사용자 credential
- 데이터 (페이로드)
- RSA 키 쌍: JWT를 생성하기 위해 RSA 키 쌍이 필요합니다. (여기서 RSA는 비대칭 키 암호화 알고리즘의 예시입니다. JWT 서명 알고리즘으로 HS512와 같은 대칭 키 알고리즘도 사용될 수 있습니다.) JWT 생성을 위한 리소스 (API 엔드포인트)를 만들어야 합니다.
- JWT 전송: 요청 헤더에 JWT를 포함하여 전송합니다. 형식: Authorization: Bearer ${JWT_TOKEN}
- JWT 검증: RSA 키 쌍의 공개 키를 사용하여 토큰을 디코딩하고 서명을 검증합니다. (HS512와 같은 대칭 키 알고리즘을 사용했다면 동일한 비밀 키로 검증합니다.)
JWT 보안 설정
Spring Boot의 OAuth2 Resource Server를 사용하여 JWT Authentication을 설정할 수 있습니다.
- KeyPairGenerator나 openssl을 사용하여 키 쌍을 생성합니다.
- 생성된 키 쌍을 사용하여 자바에서 사용할 수 있는 RSA Key 객체를 생성합니다. java.security.interfaces.RSAPrivateKey, RSAPrivateKey
- JSON 웹 토큰의 인코딩 (서명) 및 디코딩 (검증)에 필요한 키를 제공하는 역할을 하는 객체인 JSON 웹 키 소스(JWKSource, JSON Web Key Source)를 생성합니다. 이를 위해서는 다음과 같은 단계를 거칩니다.
- JWKSet 생성: JSON 웹 키(JWK, JSON Web Key) 객체들을 담는 집합입니다. JWK(JSON 웹 키)는 암호화 키를 JSON 형식으로 표현하는 표준화된 방법입니다. 생성한 RSA 키를 사용하여 JWK 객체를 만듭니다. 여러 개의 키를 관리해야 할 경우 (예: 키 순환) 여러 개의 JWK 객체를 JWKSet에 담을 수 있습니다.
- JWKSource 생성: 생성된 JWKSet을 이용하여 JWKSource 객체를 만듭니다. JWKSource는 필요에 따라 JWKSet에서 적절한 키를 선택하여 제공하는 역할을 합니다. 열쇠 꾸러미라고 생각할 수 있습니다.
- Public 키를 이용한 디코딩 (검증): 클라이언트로부터 받은 JWT의 서명을 검증할 때는 JWKSource에서 제공하는 공개 키를 사용합니다. 브라우저는 JWT를 서버에 보낼 때 서명을 포함하여 보내는데, 서버는 이 서명이 해당 JWT가 개인 키로 서명되었고 변조되지 않았음을 증명하는지 공개 키를 통해 확인합니다.
- JWKSource를 이용한 인코딩 (서명): JWT를 새로 생성하고 서명할 때는 JWKSource에서 제공하는 개인 키를 사용합니다. 서버만이 개인 키에 접근할 수 있으므로, 클라이언트가 임의로 유효한 JWT를 생성하는 것을 방지할 수 있습니다.
JWKSource와 JWKSet은 JWT 기반 인증 시스템에서 키 관리의 유연성과 보안성을 크게 향상하는 중요한 개념입니다. 특히 여러 서비스가 JWT를 공유하고 검증해야 하는 마이크로서비스 아키텍처 환경에서 중앙 집중적인 키 관리와 일관된 토큰 검증을 가능하게 해 줍니다. 개인 키는 안전하게 보관하고, 공개 키는 JWT를 검증하는 서비스들에게 공유하여 JWT의 무결성과 신뢰성을 보장하는 데 사용됩니다.
JWT Resource: 처음으로 JSON 웹 토큰을 획득하려면?
일반적으로 Basic Authentication 요청을 통해 이루어집니다.
- 클라이언트는 사용자 아이디와 비밀번호를 담은 기본 인증 요청을 JWT 리소스라고 불리는 특정 API 엔드포인트로 보냅니다. 이 요청은 보통 HTTP Authorization 헤더에 "Basic [인코딩 된 아이디:비밀번호]" 형식으로 담겨 전송됩니다.
- JWT 리소스는 이 요청을 받으면 다음과 같은 과정을 거칩니다.
- 제공된 아이디에 해당하는 사용자가 존재하는지 확인합니다.
- 제공된 비밀번호가 해당 사용자의 비밀번호와 일치하는지 확인합니다. 이 과정에서 비밀번호는 일반적으로 해싱되어 저장되어 있으므로, 입력된 비밀번호를 해시하여 저장된 해시 값과 비교합니다.
- 인증에 성공하면, JWT를 생성합니다. 이 JWT에는 사용자의 정보 (예: 아이디, 권한 등)가 페이로드에 담기고, 서버의 개인 키 (비대칭 키 방식 사용 시) 또는 비밀 키 (대칭 키 방식 사용 시)로 서명됩니다.
- JWT 리소스는 생성된 JWT를 응답으로 클라이언트에게 반환합니다. 이 응답은 보통 JSON 형태이며, JWT는 특정 필드 (예: "access_token")에 담겨서 전달됩니다.
- 이제 클라이언트는 이 JWT를 사용하여 이후의 요청들을 인증할 수 있습니다. 각 요청의 헤더 (일반적으로 Authorization 헤더)에 "Bearer [JWT 토큰 값]" 형식으로 JWT를 포함하여 서버에 보냅니다. 서버는 이 JWT를 검증하여 요청을 보낸 사용자가 누구인지, 어떤 권한을 가지고 있는지 확인하고 요청을 처리합니다.
Spring Security의 Authentication 관련 컴포넌트
- AuthenticationManager: authentication의 전반적인 책임을 담당하는 인터페이스입니다. 실제 인증 로직은 이 인터페이스를 구현한 클래스에서 이루어집니다. 이 AuthenticationManager는 여러 개의 AuthenticationProvider와 상호작용하여 다양한 방식의 인증을 처리할 수 있습니다. 마치 여러 종류의 자물쇠를 열 수 있는 마스터 키와 같은 역할을 한다고 생각할 수 있습니다.
- AuthenticationProvider: 특정 유형의 인증을 실제로 수행하는 역할을 합니다. 예를 들어,
- JwtAuthenticationProvider: JWT 기반 인증
- DaoAuthenticationProvider: 아이디/비밀번호 기반 인증
- UserDetailsService: 사용자 이름과 같은 정보를 기반으로 데이터베이스나 LDAP과 같은 저장소에서 사용자 정보를 가져오는 역할을 합니다. AuthenticationProvider는 UserDetailsService를 사용하여 인증에 필요한 사용자 상세 정보를 얻습니다.
- Authentication: 현재 인증 주체 (Principal), 자격 증명 (Credentials), 그리고 권한 (Authorities)을 나타내는 객체입니다. 인증 과정의 다양한 상태를 담고 있습니다.
- Principal: 인증된 사용자에 대한 상세 정보 (예: 사용자 객체, 아이디 등)를 담고 있습니다. "누구인가?"에 대한 정보입니다.
- Credentials: 사용자의 자격 증명 정보 (예: 비밀번호)를 담고 있습니다. 인증 시 사용되는 정보입니다.
- Authorities: 인증된 사용자가 가지고 있는 권한 (역할, 스코프 등) 목록을 담고 있습니다. "무엇을 할 수 있는가?"에 대한 정보입니다.
인증(Authentication) 결과는 어디에 저장될까요?
인증이 성공적으로 완료되면, 그 결과(인증된 Authentication 객체)는 SecurityContextHolder에 저장됩니다. 리퀘스트가 도착할 때, Authentication 객체는 credentials 만을 포함하고 있습니다. 그러나 AuthenticationManager 가 인증 과정을 끝낸 후에 Principal과 Authorities 정보를 마저 포함합니다.
- SecurityContextHolder: SecurityContext 객체를 보관하는 일종의 보관소 역할을 합니다. 스레드 로컬 (ThreadLocal) 방식으로 구현되어 있어, 각 요청을 처리하는 스레드마다 독립적인 SecurityContext를 유지할 수 있습니다. 각 요청을 처리하는 동안 임시로 신분증을 보관하는 지갑과 같습니다.
- SecurityContext: 현재 보안 콘텍스트를 나타냄. Authentication 객체(현재 사용자의 인증 정보와 권한 정보)를 가지고 있습니다.
- Authentication: 앞서 설명한 인증 객체입니다.
- GrantedAuthority: Authentication 객체 내의 권한 정보. 사용자가 어떤 역할이나 스코프를 가지고 있는지 표현합니다.
Authentication 과정 요약
- 요청이 도착하면, Spring Security 필터 체인의 인증 관련 필터가 실행됩니다. 이 필터는 요청에서 인증 정보를 추출합니다 (예: JWT 토큰, 아이디/비밀번호).
- 추출된 인증 정보 (처음에는 주로 자격 증명)를 담은 Authentication 객체가 생성됩니다.
- AuthenticationManager에게 인증을 시도하도록 요청합니다.
- AuthenticationManager는 적절한 AuthenticationProvider를 선택하여 인증을 위임합니다.
- AuthenticationProvider는 UserDetailsService를 통해 사용자 정보를 로딩하고, 제공된 자격 증명과 로딩된 사용자 정보를 비교하여 인증을 수행합니다.
- 인증에 성공하면, AuthenticationProvider는 Principal과 Authorities 정보를 담은 완전히 채워진 Authentication 객체를 생성합니다.
- 이 성공적인 Authentication 객체는 SecurityContextHolder의 SecurityContext에 저장됩니다.
- 이후의 요청 처리 과정에서 Spring Security는 SecurityContextHolder에서 인증 정보를 확인하여 사용자의 권한을 검사하고 필요한 작업을 수행합니다.
지금까지는 Authentication의 과정을 살펴보았습니다. 이제부터는 authentication이 이루어지고 난 뒤 Authorization 과정에 대해서 살펴봅시다.
Spring Security 권한 부여 (Authorization) 작동 방식
1. 전역 보안 설정 (Global Security): authorizeHttpRequests
WebSecurityConfigurerAdapter를 상속받은 설정 클래스 내에서 configure(HttpSecurity http) 메서드를 오버라이드하여 설정합니다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authz) -> authz
.requestMatchers("/admin/**").hasRole("ADMIN") // "/admin/"으로 시작하는 경로는 "ADMIN" 역할을 가진 사용자만 접근 가능
.requestMatchers("/users").hasRole("USER") // "/users" 경로는 "USER" 역할을 가진 사용자만 접근 가능
.requestMatchers("/public").permitAll() // "/public" 경로는 모든 사용자 접근 가능
.anyRequest().authenticated() // 그 외의 모든 요청은 인증된 사용자만 접근 가능
);
// ... 다른 설정 (폼 로그인, 로그아웃 등)
}
}
2. 메서드 보안 (Method Security): @EnableMethodSecurity
특정 메서드가 실행되기 전후에 권한 검사를 수행하는 방식입니다. 이를 사용하면 컨트롤러의 특정 핸들러 메서드나 서비스 레이어의 메서드에 대해 세밀한 접근 제어를 적용할 수 있습니다. 설정 클래스에 @EnableMethodSecurity 어노테이션을 사용하면 됩니다.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 메서드 보안 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ... 기존 설정
}
활성화 후에는 다음과 같은 여러 가지 어노테이션을 사용하여 다양한 방식으로 특정 메서드에 권한 규칙을 적용할 수 있습니다.
- @PreAuthorize("표현식"): 메서드 실행 전에 권한 검사를 수행합니다. 표현식이 true를 반환하면 메서드가 실행됩니다. Spring EL (SpEL)을 사용하여 다양한 조건으로 권한 검사를 수행할 수 있습니다.
@GetMapping("/users/{username}")
@PreAuthorize("hasRole('USER') and #username == authentication.name")
public User getUserDetails(@PathVariable String username) {
// 현재 사용자가 'USER' 역할을 가지고 있고, 요청된 username이 현재 인증된 사용자의 이름과 같은 경우에만 접근 허용
return userService.getUserByUsername(username);
}
@PostMapping("/admin/create")
@PreAuthorize("hasAuthority('WRITE_ADMIN')")
public ResponseEntity<String> createUser(User newUser) {
// 'WRITE_ADMIN' 권한을 가진 사용자만 접근 허용
userService.createUser(newUser);
return ResponseEntity.ok("User created successfully");
}
- @PostAuthorize("표현식"): 메서드 실행 후에 권한 검사를 수행합니다. 표현식이 true를 반환하면 결과를 반환하고, false를 반환하면 예외가 발생합니다. 주로 반환 값에 기반하여 권한을 제어할 때 사용합니다.
@GetMapping("/profile")
@PostAuthorize("returnObject.username == authentication.name")
public User getProfile() {
// 메서드 실행 후 반환된 User 객체의 username이 현재 인증된 사용자의 이름과 같은 경우에만 결과 반환
return userService.getCurrentUser();
}
- JSR-250 어노테이션 (@RolesAllowed): JSR-250 표준에서 제공하는 어노테이션으로, 특정 역할을 가진 사용자만 메서드 접근을 허용합니다. 사용하려면 @EnableMethodSecurity 어노테이션에 jsr250Enabled = true 속성을 설정해야 합니다.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true) // JSR-250 어노테이션 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
}
@GetMapping("/admin/dashboard")
@RolesAllowed({"ADMIN"})
public String adminDashboard() {
// "ADMIN" 역할을 가진 사용자만 접근 허용
return "Admin Dashboard";
}
@GetMapping("/data")
@RolesAllowed({"ADMIN", "USER"})
public String getData() {
// "ADMIN" 또는 "USER" 역할을 가진 사용자만 접근 허용
return "Data";
}
마무리
이렇게 Spring Security 필터 체인과 Authentication(인증) 필터, Authorization(권한부여) 필터 작동방식, CORS(Cross Origin Resource Sharing) 및 CSRF(Cross Site Request Forgery)에 대해서 살펴보았습니다.
Spring Security는 처음에는 어렵게 느껴질 수 있지만, 웹 보안을 위해 꼭 필요한 프레임워크입니다. 포기하지 말고 꾸준히 학습하시면, 여러분의 웹 애플리케이션을 더욱 안전하게 만들 수 있을 거예요! 궁금한 점이나 이해가 안 되는 부분은 언제든지 댓글로 질문해 주세요! 여러분의 피드백은 언제나 환영합니다.
'개발 이모저모' 카테고리의 다른 글
알아두면 은근히 쓸 곳 많은 파이썬 기본기능: 코테 활용 꿀팁 (0) | 2025.03.29 |
---|---|
시스템 디자인 인터뷰: 배달 시스템 디자인하기 (1) | 2025.03.01 |
동시성 프로그래밍: Python 코루틴과 Go 고루틴 (3) | 2025.02.02 |
SQLAlchemy 2.0 - Major Migration Guide (2) | 2025.01.19 |
NGINX, Proxy, Load Balance (0) | 2025.01.04 |