Spring

[Spring] Spring Security - 1

kahnco 2024. 9. 3. 18:05
반응형

이번 시간에는 Spring Security 에 대해서 자세하게 알아보는 시간을 가져보도록 하겠습니다.

지금부터 서술될 내용들은 https://docs.spring.io/spring-security/reference/index.html 문서를 번역 및 첨언한 것들입니다.


개요

Spring Security는 인증, 인가 및 일반적인 공격으로부터의 보호를 제공하는 프레임워크입니다. Spring Security는 명령형(Imperative) 애플리케이션과 반응형(Reactive) 애플리케이션 모두를 안전하게 보호하는데 뛰어난 지원을 제공하며, Spring 기반 애플리케이션 보안의 사실상 표준으로 자리잡고 있습니다.

 

Spring Security는 애플리케이션의 보안을 강화하기 위해 다양한 기능을 제공합니다. 여기에는 인증(authentication), 인가(authorization), 세션 관리, CSRF(Cross-Site Request Forgery) 보호, CORS(Cross-Origin Resource Sharing) 지원, HTTP 기본 및 폼 기반 로그인, 비밀번호 암호화 등이 포함됩니다. 자세한 기능 목록은 참조 문서의 "Features" 섹션에서 확인할 수 있습니다.


인증(Authentication)

Spring Security는 인증(Authentication) 을 포괄적으로 지원하는 강력한 기능을 제공합니다. 인증이란 특정 자원에 접근하려는 사용자의 신원을 확인하는 과정입니다. 일반적인 인증 방법은 사용자가 아이디(Username)비밀번호(Password)를 입력하도록 요구하는 것입니다. 인증이 완료되면 해당 사용자의 신원을 확인하게 되고, 이후에 인가(Authorization)를 통해 사용자가 어떤 자원에 접근할 수 있는지 결정할 수 있습니다.

 

Spring Security는 사용자의 인증을 위해 내장된 지원을 제공합니다. 이 섹션에서는 서블릿(Servlet)과 WebFlux 환경 모두에서 적용할 수 있는 일반적인 인증 지원에 대해 설명합니다.

 

주요 개념

  • 인증(Authentication): 사용자의 신원을 확인하는 과정입니다. 예를 들어, 사용자가 시스템에 로그인할 때 입력하는 아이디와 비밀번호가 인증의 주요 요소입니다.
  • 인가(Authorization): 인증이 완료된 사용자가 시스템 내에서 어떤 작업을 수행할 수 있는지를 결정하는 과정입니다. 인가는 주로 사용자의 역할(Role)에 따라 결정됩니다.

 

Spring Security에서의 인증 과정

  • 인증 정보 수집: 사용자가 로그인 양식에서 입력한 아이디와 비밀번호 등의 정보를 수집합니다.
  • 인증 프로세스: Spring Security는 이 정보를 기반으로 사용자의 신원을 검증합니다. 이는 내부적으로 AuthenticationManager와 UserDetailsService 등의 구성 요소를 통해 처리됩니다.
  • 인증 성공/실패 처리: 인증이 성공하면, 해당 사용자는 시스템에 접근할 수 있는 권한을 부여받습니다. 반대로 인증에 실패하면 적절한 오류 메시지를 전달받습니다.

 

Servlet 및 WebFlux 환경에서의 인증

 

Spring Security는 명령형(Imperative) 애플리케이션을 위한 서블릿(Servlet) 환경과, 반응형(Reactive) 애플리케이션을 위한 WebFlux 환경 모두에서 인증을 지원합니다. 각각의 환경에 특화된 인증 메커니즘이 존재하며, 이에 대한 세부 사항은 해당 환경별 문서를 참고해야 합니다.

 

서블릿 기반 애플리케이션에서는 전통적인 HTTP 세션과 폼 로그인을 사용하는 인증 방식이 주로 사용됩니다. 반면, WebFlux에서는 비동기 및 비차단 방식의 인증 메커니즘이 제공됩니다.


Spring Security에서의 인가(Authorization)

인가(Authorization)특정 자원에 누가 접근할 수 있는지를 결정하는 과정입니다. Spring Security는 강력한 인가 기능을 제공하여 애플리케이션의 보안을 다층적으로 보호합니다. 인가는 요청 기반(Request-Based Authorization)과 메서드 기반(Method-Based Authorization)으로 나뉘며, Servlet과 WebFlux 환경 모두에서 적용할 수 있습니다.

 

요청 기반 인가(Request Based Authorization)

 

요청 기반 인가는 웹 리소스에 대한 접근을 HTTP 요청을 기준으로 제어합니다. 예를 들어, 사용자가 특정 URL에 접근하려고 할 때, Spring Security는 해당 사용자가 해당 리소스에 접근할 권한이 있는지를 확인합니다. 이 방식은 주로 웹 애플리케이션에서 URL 경로에 따라 권한을 설정하는 데 사용됩니다.

 

예시: 요청 기반 인가 설정 (Servlet 환경)

http
    .authorizeRequests()
        .antMatchers("/admin/**").hasRole("ADMIN")
        .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
        .antMatchers("/public/**").permitAll()
        .anyRequest().authenticated();
  • /admin/**: "ADMIN" 역할을 가진 사용자만 접근 가능
  • /user/**: "USER" 또는 "ADMIN" 역할을 가진 사용자만 접근 가능
  • /public/**: 모든 사용자가 접근 가능 (로그인 불필요)
  • 나머지 모든 요청은 인증이 필요함

예시: 요청 기반 인가 설정 (WebFlux 환경)

http
    .authorizeExchange()
        .pathMatchers("/admin/**").hasRole("ADMIN")
        .pathMatchers("/user/**").hasAnyRole("USER", "ADMIN")
        .pathMatchers("/public/**").permitAll()
        .anyExchange().authenticated();

 

메서드 기반 인가(Method Based Authorization)

메서드 기반 인가는 특정 메서드에 대해 접근 권한을 설정하는 방식입니다. 서비스 레벨 또는 비즈니스 로직에서 메서드 실행 전후로 보안 검사를 할 수 있어 세밀한 보안 제어가 가능합니다. 주로 @PreAuthorize, @PostAuthorize, @Secured, @RolesAllowed와 같은 애너테이션을 사용합니다.

 

예시: 메서드 기반 인가

@Service
public class MyService {

    @PreAuthorize("hasRole('ADMIN')")
    public void adminOnlyMethod() {
        // 이 메서드는 ADMIN 역할만 호출 가능
    }

    @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
    public void userOrAdminMethod() {
        // USER 또는 ADMIN 역할만 호출 가능
    }

    @PostAuthorize("returnObject.owner == authentication.name")
    public MyObject findMyObject() {
        // 메서드 실행 후 반환된 객체의 소유자 확인
        return myObject;
    }
}
  • @PreAuthorize: 메서드 실행 전에 사용자 권한을 확인합니다.
  • @PostAuthorize: 메서드 실행 후 결과에 따라 권한을 확인할 수 있습니다.

이 방식은 요청 기반 인가와 달리 서비스 계층에서 비즈니스 로직을 보호하는 데 매우 유용합니다.


익스플로잇(Exploits) 방어

Spring Security는 일반적인 보안 취약점을 악용하는 익스플로잇으로부터 애플리케이션을 보호합니다. 가능한 경우 이러한 보호 기능은 기본적으로 활성화됩니다. 이 섹션에서는 Spring Security가 방어하는 다양한 익스플로잇 유형을 설명합니다.

 

크로스 사이트 요청 위조(CSRF, Cross-Site Request Forgery)

CSRF는 악성 웹사이트가 사용자를 속여, 사용자가 원치 않는 요청을 다른 사이트로 전송하게 만드는 공격입니다. 이를 방지하기 위해 Spring Security는 CSRF 토큰을 사용합니다. 이 토큰은 사용자가 제출하는 각 요청에 포함되며, 서버는 해당 토큰이 유효한지 확인함으로써 공격을 차단합니다.

 

CSRF 방지 활성화

Spring Security는 기본적으로 CSRF 보호 기능을 활성화합니다. 이를 비활성화하고 싶다면 다음과 같이 설정할 수 있습니다:

http.csrf().disable();

 

그러나, 일반적으로는 CSRF 보호를 유지하는 것이 권장됩니다.

 

세션 고정(Session Fixation)

세션 고정 공격은 공격자가 사용자의 세션 ID를 미리 설정하여, 사용자가 로그인한 후에도 같은 세션을 사용하게 만듭니다. Spring Security는 로그인 시 새로운 세션을 생성하여 기존 세션 ID를 무효화함으로써 이러한 공격을 방지합니다.

 

세션 고정 보호 활성화

기본적으로 Spring Security는 세션 고정 공격에 대한 보호를 제공합니다. 필요에 따라 보호 설정을 사용자 정의할 수 있습니다:

http.sessionManagement()
    .sessionFixation().migrateSession();  // 새로운 세션 ID 생성

 

클릭재킹(Clickjacking)

클릭재킹은 사용자가 클릭하는 요소 위에 보이지 않는 프레임을 오버레이하여, 사용자가 원치 않는 작업을 수행하게 하는 공격입니다. Spring Security는 X-Frame-Options 헤더를 사용하여 클릭재킹 공격을 방지합니다.

 

클릭재킹 보호 설정

Spring Security는 기본적으로 DENY 또는 SAMEORIGIN 옵션을 사용해 X-Frame-Options를 설정합니다. 이 설정은 클릭재킹 공격을 방어합니다.

http.headers().frameOptions().deny();  // 프레임 내에서 페이지가 표시되지 않도록 설정

 

XSS(크로스 사이트 스크립팅)

XSS는 악성 스크립트가 웹 페이지에 삽입되어 사용자 브라우저에서 실행되는 공격입니다. Spring Security는 XSS 방어를 위해 콘텐츠 보안 정책(CSP, Content Security Policy) 과 같은 다양한 헤더를 설정할 수 있습니다.

 

XSS 방지 설정

Spring Security는 다양한 HTTP 헤더를 통해 XSS 공격을 차단합니다. 기본적으로 브라우저가 악성 스크립트를 실행하지 않도록 합니다.

http.headers()
    .contentSecurityPolicy("script-src 'self'");  // 외부 스크립트 차단

 

HTTP 응답 분할(HTTP Response Splitting)

HTTP 응답 분할 공격은 애플리케이션이 사용자 입력을 제대로 검사하지 않아 발생하는 공격입니다. Spring Security는 이러한 공격을 방지하기 위해 응답 헤더에서 사용자 입력을 자동으로 검증하고, 불법적인 문자를 필터링합니다.


보안 HTTP 응답 헤더

Spring Security는 웹 애플리케이션의 보안을 강화하기 위해 다양한 보안 HTTP 응답 헤더를 제공합니다. 기본적으로 Spring Security는 안전한 기본값을 제공하며, 필요에 따라 헤더를 제거, 수정하거나 사용자 지정할 수 있습니다.

 

기본 보안 HTTP 응답 헤더

 

Spring Security는 다음과 같은 보안 관련 HTTP 응답 헤더를 기본적으로 추가합니다:

  1. Cache-Control: no-cache, no-store, max-age=0, must-revalidate
  2. Pragma: no-cache
  3. Expires: 0
  4. X-Content-Type-Options: nosniff
  5. Strict-Transport-Security: max-age=31536000; includeSubDomains (HTTPS 요청에만 추가됨)
  6. X-Frame-Options: DENY
  7. X-XSS-Protection: 0

이 기본 헤더들은 사용자의 민감한 정보를 보호하고 일반적인 공격으로부터 웹 애플리케이션을 방어하는 데 도움을 줍니다.

 

1. Cache-Control

Spring Security는 기본적으로 캐시를 비활성화하여 사용자의 민감한 정보가 브라우저 캐시에 저장되지 않도록 합니다. 이는 사용자가 로그아웃한 후에도 다른 사용자가 백 버튼을 눌러 민감한 정보를 다시 볼 수 없도록 방지합니다.

 

2. X-Content-Type-Options

이 헤더는 브라우저가 콘텐츠 스니핑(content sniffing) 을 하지 못하도록 합니다. 과거에는 브라우저가 콘텐츠 유형을 추측하여 사용자 경험을 개선하려 했지만, 이는 XSS(크로스 사이트 스크립팅) 공격의 취약점을 제공했습니다. Spring Security는 X-Content-Type-Options: nosniff 헤더를 추가하여 이러한 공격을 방지합니다.

 

3. HTTP Strict Transport Security (HSTS)

HSTS는 웹사이트가 오직 HTTPS 프로토콜로만 접근되도록 보장합니다. 이를 통해 사용자가 HTTP로 웹사이트에 접근할 경우에도 자동으로 HTTPS로 리다이렉션됩니다. Spring Security는 HTTPS 요청에 대해 Strict-Transport-Security 헤더를 추가하여, 브라우저가 해당 도메인을 HSTS 호스트로 인식하고 안전하게 통신할 수 있도록 합니다.

 

4. X-Frame-Options

이 헤더는 클릭재킹(clickjacking) 공격을 방지하기 위해 페이지가 iframe 내에 렌더링되지 않도록 합니다. Spring Security는 기본적으로 X-Frame-Options: DENY를 설정하여 페이지가 iframe 내에서 표시되지 않도록 합니다.

 

5. X-XSS-Protection

XSS 필터는 브라우저에서 반사된 XSS 공격을 감지하고 차단하는 기능이지만, 최신 브라우저에서는 이 필터가 더 이상 권장되지 않으며, Spring Security는 기본적으로 X-XSS-Protection: 0을 설정하여 이 기능을 비활성화합니다.

 

기타 보안 헤더

1. Content Security Policy (CSP)

CSP는 웹 애플리케이션에서 스크립트와 같은 리소스가 로드될 수 있는 출처를 명시하여 콘텐츠 주입 공격을 방지합니다. 예를 들어, 다음과 같은 CSP 헤더를 추가하여 특정 출처에서만 스크립트를 로드하도록 제한할 수 있습니다:

Content-Security-Policy: script-src https://trustedscripts.example.com

 

2. Referrer Policy

Referrer Policy는 사용자가 이전에 방문한 페이지의 정보를 제어하는 메커니즘입니다. Spring Security는 기본적으로 Referrer-Policy: same-origin을 설정하여 같은 출처에서만 referrer 정보를 제공하도록 합니다.

 

3. Feature Policy / Permissions Policy

이 정책은 특정 웹 API나 브라우저 기능을 활성화, 비활성화 또는 수정하는 데 사용됩니다. 예를 들어, 다음과 같은 헤더를 사용하여 위치 정보 접근을 자신만 허용하도록 설정할 수 있습니다:

Feature-Policy: geolocation 'self' Permissions-Policy: geolocation=(self)

 

4. Clear-Site-Data

이 헤더는 로그아웃 시 쿠키, 캐시, 로컬 스토리지 등의 브라우저 데이터를 삭제하는 기능을 제공합니다:

Clear-Site-Data: "cache", "cookies", "storage", "executionContexts"

 

사용자 정의 헤더

Spring Security는 표준 보안 헤더 외에도 사용자 정의 헤더를 추가할 수 있는 기능을 제공합니다. 이를 통해 애플리케이션 요구 사항에 맞는 보안 구성을 더욱 세밀하게 제어할 수 있습니다.


HTTP 통신 보안

모든 HTTP 기반 통신, 특히 정적 리소스를 포함한 모든 요청은 TLS(Transport Layer Security) 를 사용하여 보호되어야 합니다. Spring Security는 자체적으로 HTTP 연결을 처리하지 않지만, HTTPS 사용을 지원하기 위한 다양한 기능을 제공합니다.

 

HTTPS로 리디렉션

Spring Security는 클라이언트가 HTTP를 사용할 경우 이를 HTTPS로 리디렉션할 수 있도록 설정할 수 있습니다. 이 기능은 ServletWebFlux 환경 모두에서 사용할 수 있습니다.

http
    .requiresChannel()
    .anyRequest()
    .requiresSecure();

 

이 설정은 모든 요청을 HTTPS로 리디렉션합니다.

 

엄격한 전송 보안 (Strict Transport Security, HSTS)

Spring Security는 HTTP Strict Transport Security(HSTS) 를 지원하며, 이를 기본적으로 활성화합니다. HSTS는 브라우저가 특정 도메인에 대해 HTTPS를 강제로 사용하도록 하여, 중간자 공격(MITM) 의 위험을 줄입니다.

 

프록시 서버 구성

애플리케이션이 프록시 서버로드 밸런서 뒤에 배포될 경우, 애플리케이션이 프록시 서버의 존재를 인지할 수 있도록 적절히 구성하는 것이 중요합니다. 예를 들어, 로드 밸런서가 https://example.com/ 요청을 받아 내부 애플리케이션 서버로 전달할 때, 잘못된 설정이 있으면 애플리케이션 서버는 이를 https://192.168.0.107:8080로 요청한 것으로 잘못 처리할 수 있습니다.

 

이를 해결하기 위해, RFC 7239에 따라 X-Forwarded 헤더를 설정하여 로드 밸런서 사용을 명시해야 합니다. 애플리케이션 서버는 이러한 헤더를 인식하도록 구성해야 합니다. 예를 들어:

  • Tomcat: RemoteIpValve 사용
  • Jetty: ForwardedRequestCustomizer 사용

또는 Spring 사용자는 다음과 같은 필터를 사용할 수 있습니다:

  • Servlet 스택: ForwardedHeaderFilter
  • Reactive 스택: ForwardedHeaderTransformer

Integrations


Cryptography

 

비밀번호 저장 및 관리

Spring Security에서는 PasswordEncoder 인터페이스를 통해 비밀번호를 안전하게 저장하기 위한 일방향 변환을 수행합니다. 일방향 변환은 비밀번호를 안전하게 저장하는 데 유용하지만, 데이터베이스 인증에 필요한 자격 증명처럼 양방향 변환이 필요한 경우에는 적합하지 않습니다. 일반적으로 PasswordEncoder는 사용자로부터 제공된 비밀번호와 저장된 비밀번호를 비교하기 위한 용도로 사용됩니다.

 

비밀번호 저장의 역사

초기에는 비밀번호를 평문으로 저장하는 방식이 사용되었습니다. 저장소 자체에 접근하는 데 자격 증명이 필요하다고 가정했기 때문에 비밀번호가 안전하다고 여겨졌습니다. 그러나 SQL 인젝션 같은 공격 기법을 통해 대규모 데이터 덤프가 이루어지며, 많은 사용자의 비밀번호가 유출되었습니다. 이후 보안 전문가들은 비밀번호를 안전하게 보호해야 한다는 필요성을 인식하게 되었습니다.

 

해싱(Hashing)의 등장

개발자들은 비밀번호를 일방향 해시 함수(예: SHA-256)를 사용하여 저장하는 방법을 채택하기 시작했습니다. 사용자가 인증할 때는 입력한 비밀번호를 해시화한 값과 저장된 해시 값을 비교하는 방식입니다. 이렇게 하면 데이터 유출 시에도 해시 값만 노출되므로, 비밀번호를 역추적하기 어렵기 때문에 비교적 안전했습니다. 그러나 해커들은 레인보우 테이블을 만들어 비밀번호를 쉽게 추정할 수 있게 되었습니다.

 

Salted Password

레인보우 테이블의 효과를 완화하기 위해 **솔트(Salt)**라는 랜덤 바이트를 비밀번호와 함께 해시 함수의 입력으로 사용하게 되었습니다. 각 사용자의 비밀번호마다 고유한 솔트를 생성하여, 솔트와 비밀번호의 조합을 해시화했습니다. 솔트는 비밀번호와 함께 저장되며, 인증 시에는 저장된 솔트와 사용자가 입력한 비밀번호의 해시 값이 비교됩니다. 이를 통해 레인보우 테이블의 효용성이 감소했습니다.

 

현대적 비밀번호 보호 방식

오늘날에는 SHA-256 같은 암호화 해시가 더 이상 안전하지 않다고 여겨집니다. 최신 하드웨어로 수십억 개의 해시 계산을 초당으로 수행할 수 있어, 비밀번호를 빠르게 추측할 수 있기 때문입니다. 이제 개발자들은 적응형 일방향 함수(예: bcrypt, PBKDF2, scrypt, argon2)를 사용하여 비밀번호를 저장하도록 권장받습니다. 이러한 함수들은 의도적으로 많은 CPU 및 메모리 리소스를 사용하게 만들어, 공격자가 비밀번호를 추측하는 것이 매우 어렵습니다.

 

DelegatingPasswordEncoder

 

Spring Security 5.0 이전에는 기본 PasswordEncoder가 NoOpPasswordEncoder였으며, 이는 평문 비밀번호를 요구했습니다. 그러나 더 현대적이고 안전한 비밀번호 인코딩 방식으로 전환하기 위해 Spring Security는 DelegatingPasswordEncoder를 도입했습니다.

 

이 엔코더는 다음과 같은 문제를 해결합니다:

  • 현재의 비밀번호 저장 권장 사항을 따르는 비밀번호 인코딩 보장
  • 기존 포맷과의 호환성 유지
  • 향후의 인코딩 방식 업그레이드 지원

 

비밀번호 저장 형식

일반적인 비밀번호 저장 형식은 {id}encodedPassword의 형태를 따릅니다. 예를 들어:

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password

여기서 {bcrypt}는 BCryptPasswordEncoder를, {noop}는 NoOpPasswordEncoder를 사용함을 나타냅니다.

 

비밀번호 인코딩 및 비교

DelegatingPasswordEncoder는 비밀번호를 인코딩할 때, 지정된 idForEncode 값을 기반으로 적절한 PasswordEncoder를 사용합니다. 이렇게 하면 비밀번호를 최신 인코딩 방식으로 안전하게 저장하고, 레거시 포맷도 동시에 지원할 수 있습니다.

 

BCryptPasswordEncoder

BCryptPasswordEncoder 는 널리 사용되는 bcrypt 알고리즘을 사용하여 비밀번호를 해시화하는 Spring Security의 구현체입니다. bcrypt는 의도적으로 느리게 동작하여 비밀번호 크래킹에 대한 저항력을 높입니다. bcrypt는 적응형 일방향 함수로, 시스템 성능에 맞게 조정되어야 하며, 일반적으로 비밀번호 검증에 약 1초가 걸리도록 조정하는 것이 권장됩니다. 기본 구현에서는 강도(strength) 10이 사용되지만, 시스템 성능에 따라 이 값을 조정할 수 있습니다.

// 강도 16으로 BCryptPasswordEncoder 생성
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

 

Argon2PasswordEncoder

Argon2PasswordEncoder  Argon2 알고리즘을 사용하여 비밀번호를 해시화합니다. Argon2는 비밀번호 해싱 경쟁 대회(Password Hashing Competition) 에서 우승한 알고리즘으로, 커스텀 하드웨어를 사용한 비밀번호 크래킹을 방지하기 위해 의도적으로 느리게 동작하며 많은 메모리를 필요로 합니다.

// 기본 설정으로 Argon2PasswordEncoder 생성
Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

 

Pbkdf2PasswordEncoder

Pbkdf2PasswordEncoder  PBKDF2 알고리즘을 사용하여 비밀번호를 해시화합니다. PBKDF2는 비밀번호 크래킹을 방지하기 위해 의도적으로 느리게 동작합니다. 특히 FIPS 인증이 필요한 경우 적합한 알고리즘입니다.

// 기본 설정으로 Pbkdf2PasswordEncoder 생성
Pbkdf2PasswordEncoder encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

 

SCryptPasswordEncoder

SCryptPasswordEncoder  scrypt 알고리즘을 사용하여 비밀번호를 해시화합니다. scrypt는 커스텀 하드웨어를 사용한 비밀번호 크래킹을 방지하기 위해 의도적으로 느리게 동작하며 많은 메모리를 필요로 합니다.

// 기본 설정으로 SCryptPasswordEncoder 생성
SCryptPasswordEncoder encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

 

기타 PasswordEncoder

Spring Security는 이전 버전의 비밀번호 인코딩 방식을 사용하는 레거시 시스템과의 호환성을 위해 여러 PasswordEncoder 구현체를 제공합니다. 그러나 이러한 구현체는 더 이상 안전하지 않기 때문에 사용이 권장되지 않으며, 대체로 deprecated 처리되었습니다.

 

비밀번호 저장 구성

기본적으로 Spring Security는 DelegatingPasswordEncoder 를 사용합니다. DelegatingPasswordEncoder는 최신 비밀번호 인코딩 방식을 적용하면서도, 기존의 비밀번호 포맷과의 호환성을 유지할 수 있도록 설계되었습니다. 시스템에 맞는 비밀번호 인코딩 설정은 Spring bean을 통해 쉽게 맞춤화할 수 있습니다.

 

비밀번호 변경 엔드포인트 구성

Spring Security는 비밀번호 변경 엔드포인트를 구성할 수 있는 기능을 제공합니다. 예를 들어, 비밀번호 변경 URL이 /change-password라면 다음과 같이 설정할 수 있습니다:

http
    .passwordManagement(Customizer.withDefaults());

 

다른 엔드포인트를 지정하고 싶다면 다음과 같이 할 수 있습니다.

http
    .passwordManagement((management) -> management
        .changePasswordPage("/update-password")
    );

 

비밀번호 유출 여부 확인

Spring Security는 Have I Been Pwned API와의 통합을 통해 비밀번호 유출 여부를 확인하는 기능을 제공합니다. CompromisedPasswordChecker 인터페이스의 구현체인 HaveIBeenPwnedRestApiPasswordChecker를 사용하면 사용자 비밀번호가 데이터 유출에 포함되었는지 확인할 수 있습니다.

@Bean
public CompromisedPasswordChecker compromisedPasswordChecker() {
    return new HaveIBeenPwnedRestApiPasswordChecker();
}

 

비밀번호가 유출된 경우, 사용자가 로그인하지 못하도록 하거나 비밀번호 재설정 페이지로 리다이렉트할 수 있습니다.

 


Spring Data Integration

Spring SecuritySpring Data와의 통합을 제공하여 쿼리 내에서 현재 사용자를 참조할 수 있도록 지원합니다. 이는 결과를 필터링해야 하는 경우, 특히 페이지 결과를 지원하기 위해서는 매우 유용하고 필요합니다. 결과를 나중에 필터링하는 것은 확장성 측면에서 적합하지 않기 때문입니다.

 

Spring Data & Spring Security 구성

이 기능을 사용하려면, org.springframework.security:spring-security-data 의존성을 추가하고, SecurityEvaluationContextExtension 타입의 빈을 제공해야 합니다. Java 구성에서는 다음과 같이 설정할 수 있습니다.

// Java 설정
@Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
    return new SecurityEvaluationContextExtension();
}

# XML 설정
<bean class="org.springframework.security.data.repository.query.SecurityEvaluationContextExtension"/>

 

 

@Query 내에서의 보안 표현식

이제 Spring Security를 쿼리 내에서 사용할 수 있습니다. 예를 들어, 다음과 같이 사용할 수 있습니다.

@Repository
public interface MessageRepository extends PagingAndSortingRepository<Message,Long> {
    @Query("select m from Message m where m.to.id = ?#{ principal?.id }")
    Page<Message> findInbox(Pageable pageable);
}

 

이 쿼리는 Authentication.getPrincipal().getId()가 메시지의 수신자와 동일한지 확인합니다. 이 예제는 principal이 id 속성을 가진 객체로 커스터마이징되어 있다고 가정합니다. SecurityEvaluationContextExtension 빈을 노출함으로써, 모든 일반적인 보안 표현식을 쿼리 내에서 사용할 수 있습니다.

 


 

Spring Security 와 멀티스레드 환경에서의 보안 처리

Spring Security는 대부분의 환경에서 스레드별로 보안 컨텍스트를 저장합니다. 이는 새 스레드에서 작업이 수행될 때, 기존 스레드의 SecurityContext가 손실된다는 의미입니다. 이를 해결하기 위해 Spring Security는 멀티스레드 환경에서 보안을 쉽게 다룰 수 있는 인프라를 제공합니다. 특히, AsyncContext.start(Runnable)와 Spring MVC의 비동기 통합에서 사용되는 기본 추상화 계층을 제공합니다.

 

 

DelegatingSecurityContextRunnable

DelegatingSecurityContextRunnableSpring Security의 동시성 지원에서 가장 기본적인 구성 요소 중 하나입니다. 이 클래스는 Runnable을 감싸서 특정 SecurityContextSecurityContextHolder를 초기화한 후, Runnable을 실행하고 실행이 끝난 후에는 SecurityContextHolder를 정리합니다. 이 클래스는 다음과 같은 형태를 가집니다:

 

public void run() {
    try {
        SecurityContextHolder.setContext(securityContext);
        delegate.run();
    } finally {
        SecurityContextHolder.clearContext();
    }
}

 

이 간단한 구조는 SecurityContext를 하나의 스레드에서 다른 스레드로 원활하게 전달할 수 있게 해줍니다. 대부분의 경우 SecurityContextHolder는 스레드 단위로 동작하므로, 이 기능은 매우 중요합니다. 예를 들어, Spring Security의 <global-method-security> 지원을 사용해 서비스를 보호하는 경우, 현재 스레드의 SecurityContext를 보안된 서비스를 호출하는 스레드로 쉽게 전달할 수 있습니다.

 

 

DelegatingSecurityContextRunnable 사용 예시

Runnable originalRunnable = new Runnable() {
    public void run() {
        // 보안된 서비스 호출
    }
};

SecurityContext context = SecurityContextHolder.getContext();
DelegatingSecurityContextRunnable wrappedRunnable =
    new DelegatingSecurityContextRunnable(originalRunnable, context);

new Thread(wrappedRunnable).start();

 

위의 코드는 다음과 같은 단계로 이루어집니다:

  1. 보안된 서비스를 호출할 Runnable을 생성합니다.
  2. SecurityContextHolder에서 사용할 SecurityContext를 가져와 DelegatingSecurityContextRunnable을 초기화합니다.
  3. DelegatingSecurityContextRunnable을 사용하여 새 스레드를 생성합니다.
  4. 생성된 스레드를 시작합니다.

SecurityContextHolder에서 직접 SecurityContext를 가져와 사용하는 것이 일반적이므로, 이를 위한 간단한 생성자도 제공됩니다:

DelegatingSecurityContextRunnable wrappedRunnable =
    new DelegatingSecurityContextRunnable(originalRunnable);

new Thread(wrappedRunnable).start();

 

 

DelegatingSecurityContextExecutor

DelegatingSecurityContextRunnable을 사용하는 것이 쉬운 방법이지만, Spring Security에 대한 지식을 필요로 한다는 점에서 이상적이지는 않습니다. DelegatingSecurityContextExecutor를 사용하면 Spring Security에 대한 지식 없이도 보안 컨텍스트를 처리할 수 있습니다.

 

DelegatingSecurityContextExecutorRunnable 대신 Executor를 위임 객체로 받는다는 점에서 DelegatingSecurityContextRunnable과 유사하게 설계되었습니다. 다음은 사용 예시입니다:

SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = new UsernamePasswordAuthenticationToken("user", "password", AuthorityUtils.createAuthorityList("ROLE_USER"));
context.setAuthentication(authentication);

SimpleAsyncTaskExecutor delegateExecutor = new SimpleAsyncTaskExecutor();
DelegatingSecurityContextExecutor executor =
    new DelegatingSecurityContextExecutor(delegateExecutor, context);

Runnable originalRunnable = new Runnable() {
    public void run() {
        // 보안된 서비스 호출
    }
};

executor.execute(originalRunnable);

 

위 코드의 단계는 다음과 같습니다:

  1. DelegatingSecurityContextExecutor에서 사용할 SecurityContext를 생성합니다.
  2. Runnable을 실행할 delegateExecutor를 생성합니다.
  3. DelegatingSecurityContextExecutor를 생성하여 전달된 모든 Runnable을 DelegatingSecurityContextRunnable로 감싼 후 delegateExecutor에 전달합니다.

이제, DelegatingSecurityContextExecutor를 사용하여 보안 컨텍스트를 숨길 수 있습니다. 코드에서 SecurityContext와 관련된 처리가 전혀 보이지 않도록 설정할 수 있습니다.

 

스프링 빈을 이용한 예시

@Autowired
private Executor executor; // 이 예에서는 DelegatingSecurityContextExecutor 인스턴스

public void submitRunnable() {
    Runnable originalRunnable = new Runnable() {
        public void run() {
            // 보안된 서비스 호출
        }
    };
    executor.execute(originalRunnable);
}

 

이 코드에서는 SecurityContext가 스레드로 전달되고 originalRunnable이 실행된 후 SecurityContextHolder가 정리됩니다. 또한, 실행 시점의 사용자 정보가 사용되어 Runnable을 실행할 수 있습니다.

 


Jackson 지원

Spring Security는 Spring Security 관련 클래스들을 직렬화하는 데 있어 성능을 향상시키기 위해 Jackson 지원을 제공합니다. 이는 특히 분산 세션(예: 세션 복제, Spring Session 등) 작업 시 유용합니다.

 

Jackson과의 통합

Spring Security 관련 클래스들을 직렬화/역직렬화하기 위해, SecurityJackson2Modules.getModules(ClassLoader)를 사용하여 ObjectMapper에 모듈을 등록할 수 있습니다. 이를 통해 Spring Security 관련 객체를 JSON으로 변환하거나, JSON으로부터 Spring Security 객체를 복원할 수 있습니다.

ObjectMapper mapper = new ObjectMapper();
ClassLoader loader = getClass().getClassLoader();
List<Module> modules = SecurityJackson2Modules.getModules(loader);
mapper.registerModules(modules);

// ObjectMapper를 평소처럼 사용
SecurityContext context = new SecurityContextImpl();
// 예시로 SecurityContext를 JSON으로 직렬화
String json = mapper.writeValueAsString(context);

 

위 코드는 ObjectMapper에 Spring Security의 Jackson 모듈을 등록하고, 이를 사용하여 SecurityContext 객체를 JSON 문자열로 직렬화하는 예시입니다.

 

활용 예시

이 기능은 특히 다음과 같은 경우에 유용합니다:

  • 분산 세션 환경: 세션 복제나 Spring Session과 같은 기능을 사용할 때, Spring Security 객체를 효율적으로 직렬화하여 다른 노드로 전송할 수 있습니다.
  • JSON 기반 통신: REST API나 메시지 큐와 같이 JSON 형식을 사용하는 시스템 간의 통신에서, Spring Security 객체를 포함한 데이터를 JSON으로 변환하여 전송하거나, 수신한 JSON 데이터를 Spring Security 객체로 변환할 수 있습니다.

 


Spring Security의 로컬라이제이션(Localization) 지원

Spring Security는 인증 실패나 접근 거부(인가 실패)와 관련된 예외 메시지를 포함하여 모든 예외 메시지를 로컬라이즈할 수 있습니다. 이는 애플리케이션이 다양한 로케일을 지원해야 할 때 유용합니다. 반면, 개발자나 시스템 관리자에게 초점을 맞춘 예외 및 로그 메시지(예: 잘못된 속성, 인터페이스 계약 위반, 잘못된 생성자 사용, 시작 시점 검증, 디버그 수준의 로깅 등)는 로컬라이즈되지 않으며, 영어로 하드코딩되어 있습니다.

 

메시지 파일 및 설정

spring-security-core-xx.jar에는 org.springframework.security 패키지가 포함되어 있으며, 이 패키지 내에는 기본 메시지 파일인 messages.properties와 일부 일반적인 언어에 대한 로컬라이즈된 버전이 포함되어 있습니다. 이 메시지 파일은 Spring의 MessageSourceAware 인터페이스를 구현하는 Spring Security 클래스에 의해 사용되며, 애플리케이션 컨텍스트가 시작될 때 메시지 리졸버가 의존성 주입될 것으로 기대됩니다. 일반적으로는 애플리케이션 컨텍스트에 메시지 파일을 참조하는 빈을 등록하기만 하면 됩니다. 예시는 다음과 같습니다:

<bean id="messageSource"
	class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
    <property name="basename" value="classpath:org/springframework/security/messages"/>
</bean>

 

messages.properties 파일은 표준 리소스 번들에 따라 이름이 지정되며, Spring Security 메시지에서 기본적으로 지원하는 언어를 나타냅니다. 이 기본 파일은 영어로 작성되어 있습니다.

 

메시지 파일 커스터마이징 및 로컬라이제이션

다른 언어를 지원하거나 메시지를 커스터마이징하려면 messages.properties 파일을 복사하여 적절히 이름을 변경한 후 위에서 설명한 빈 정의에 등록하면 됩니다. 이 파일에 포함된 메시지 키는 많지 않으므로, 로컬라이제이션 작업은 비교적 간단합니다. 만약 이 파일을 로컬라이즈했다면, 커뮤니티와 작업 내용을 공유하기 위해 JIRA 태스크를 통해 해당 파일을 첨부하는 것을 고려해 볼 수 있습니다.

 

로케일 설정

Spring Security는 실제로 적절한 메시지를 조회하기 위해 Spring의 로컬라이제이션 지원을 사용합니다. 이를 위해서는, 들어오는 요청의 로케일이 Spring의 org.springframework.context.i18n.LocaleContextHolder에 저장되어 있어야 합니다. Spring MVC의 DispatcherServlet은 애플리케이션을 위해 이 작업을 자동으로 처리하지만, Spring Security의 필터는 DispatcherServlet보다 먼저 호출되므로, 필터가 호출되기 전에 LocaleContextHolder에 올바른 로케일이 설정되어 있어야 합니다.

 

이 작업은 직접 필터를 작성하여 수행하거나(이 경우 해당 필터는 web.xml에서 Spring Security 필터들보다 먼저 위치해야 함), Spring의 RequestContextFilter를 사용할 수 있습니다. 로컬라이제이션을 사용하는 방법에 대한 자세한 내용은 Spring Framework 문서를 참조하십시오.


여기까지 Spring Security 의 개요와 지원하는 기능들에 대해 알아보았습니다. 다음 시간에는 Spring Servlet Container 와 Spring Security 통합에 대해서 자세히 알아보겠습니다.

 

반응형

'Spring' 카테고리의 다른 글

[Spring] Spring Framework Overview - 2  (0) 2024.08.22
[Spring] Spring Framework Overview - 1  (0) 2024.08.21