스프링 시큐리티에는 보안 6요소를 어떻게 사용할까?

전 회사에서 노드를 스프링을 바꿔야 하는 과제를 받았었다.
문제가 있었다. 당시 그 프로젝트는 SSO가 적용되어있다. SSO는 간단히 말해 여러 서비스를 통합해서 관리를 할 수 있어야 했다.
그래서 스프링 시큐리티를 적용하기로했다.
하지만 문제가 발생했다. 내가 스프링 시큐리티를 모른다는 사실이었다. 그래서 같이 일하시는 분이랑 시큐리티에 대해 같이 학습을 하였다.시간이 지나고 나니 스프링 시큐리티가 어떤 프로젝트인지 전혀 감이 잡히지않아 이렇게 다시 공부하기로 마음먹었다.
|일단 내가 제일 먼저 알아야 하는건 본질에 대해 알아야 한다고 생각한다. 스프링 시큐리티가 과연 뭘까? 스프링은 이걸로 무엇을 하고 싶어하는걸까? 사실 스프링 시큐리티를 적용하지 않아도 인가며 인증이고 전부 가능하다. 스프링 시큐리티를 써서 얻는 이점이 뭘까?
보안성이라고는 하지만 생각해보면 그 정도까지 쓰지 않을거고 만약, 쓴다고 해도 그게 본질인지는 잘 모르겠다.

스프링 시큐리티의 본질

스프링 시큐리티는 보안을 일관된 방식으로 관리하기 위한 추상화 계층이라고 한다.
만약, 인증/인가를 직접 구현 한다고 하면, 로그인 시 사용자 검증, 세션 저장, 요청 시 권한 확인등 본인만의 방식으로 코딩을 하게 된다.
보안 로직을 아키텍처 수준에서 통합 관리할 수 있게 한 것이다.

생각을 해보면 스프링 시큐리티를 해서 얻은 이점은 생각보다 없을거라 생각한다. 이렇게 생각이 드는 이유는 단순히 귀찮아서라고 생각이 든다. 왜냐하면, 분명 빨리 만들 수 있는데 스프링 시큐리티가 걸림돌이 된다고 생각할 수 있다고 생각한다. 하지만 그 코드를 다시 봤을때, 비슷한 코드의 상당 수가 프로젝트를 뒤엎질도 모른다. 또 공부를 해야 한다는 압박감때문에 스프링 시큐리티는 생각만큼 학습하기 어렵다.
이게 내가 생각하는 스프링 시큐리티의 본질이다. 단순한 프레임워크가 아니다. 스프링 시큐리티를 재대로 사용할 수 있다는 것은 여러 보안 정보를 통제 할 수 있다는 의미가 된다. 자 이제 본격적으로 시작해 보자.

왜 스프링 시큐리티를 학습했는지 이제 알았다면, 본격적으로 어떤 것을 할 수 있는지 생각해보자.

스프링 시큐리티는 어떤것을 할 수 있지?

스프링 시큐리티는 본질적으로 보안에 대한 프레임워크다.
고로 보안은 어떤건지에 부터 생각을 해야 한다.
자고로 보안이란 특정 무언가를 지키는 것을 말한다. 무엇을 지킬까? 누가 지킬까? 누구로부터 지킬까? 어떻게 지킬까? 어디까지 허용할까?
언제 지킬까? 등등 다양한 질문을 만들 수 있다. 결국 어떠한 것을 지키기 위한것으로 이해하면 될거 같다.

보안의 6요소는, 보안 사고의 6가지 질문에 대한 체계적 해답이다.

  • "무엇을 지킬까" → 기밀성
  • "누가 지킬까" → 인증성
  • "누구로부터 지킬까" → 무결성
  • "어떻게 지킬까" → 접근통제성
  • "어디까지 허용할까" → 가용성
  • "언제 지킬까" → 부인방지

결국 보안은, "누가 무엇을 언제 어떻게 지키는가"라는 6개의 사고 질문을
기밀성 무결성 가용성 인증성 부인방지 접근통제성이라는 원칙으로 구현한 신뢰의 구조 체계입니다.

그렇다면, 스프링 시큐리티는 저들을 어떻게 추상화 했을까?

보안 6대 요소 추상화

아쉽게도 스프링 시큐리티는 6요소를 전부 담당하지 않고 4개만 담당한다고 한다. 

요소 담당 계층 설명
기밀성(Confidentiality) 🟢 Application 인증되지 않은 사용자의 접근 차단 — Spring Security가 직접 처리
무결성(Integrity) 🟢 Application ↔ Infra 경계 요청 변조 방지는 처리하지만, 저장 데이터 일관성은 DB/Infra가 담당
가용성(Availability) 🔴 Infra / Network 부하 분산, 장애 복구, 서버 클러스터링 — Spring Security가 개입 불가
인증성(Authenticity) 🟢 Application 신원 확인(Authentication) — Spring Security 핵심 기능
부인방지(Non-Repudiation) 🔴 Application ↔ External System 행위 추적 로그는 가능하지만, 위조 불가능한 증거는 외부 감사 시스템이 담당
접근통제성(Access Control) 🟢 Application 권한 기반 인가(Authorization) — Spring Security 핵심 기능

스프링 시큐리티는 Application이다. 그렇기 때문에 Application에서 제한할 수 있는 요소들만 처리가 가능하다고 한다.
하나씩 보면서 왜 Application계층인지 생각해보자.

1. 기밀성

"허가되지 않은 사용자가 보호 대상에 접근하지 못하게 하는 것"이다. 즉, "보호해야 할 가치 있는 무언가(자산, 정보, 상태)"를 정의하고, 그 자산이 외부로 노출되지 않도록 경계를 세우는 행위다. 

생각보다 애매한게 어디에서 자산들을 지키는지도 애매하다. 분명 표에선 어플리케이션 계층에서 막는다고 적어놨지만 인프라 계층에서도 막을 수 도 있지 않을까? 각 계층은 지켜야 할 리소스들이 전부 다르다.
어플리케이션이 지켜야 할 범위는 요청/응답, 세션, 리소스라고 한다. 그렇다라는 의미는 다른 자원들은 어플리케이션에서는 크게 신경을 쓰지 않아도 된다는 뜻이 된다.

그렇다면 스프링 시큐리티에서는 어떻게 사용하는지 살펴보자.

스프링 시큐리티(Spring Security)는 기밀성을 “요청(Request)의 진입점에서 인증되지 않은 접근을 차단함으로써 보장하는 구조” 로 추상화한다.

즉, 외부에서 들어오는 모든 HTTP 요청은 컨트롤러로 도달하기 전에 Security Filter Chain이라는 보안 게이트를 통과해야 한다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 1. HTTPS 강제 → 전송 중 데이터 기밀성 (통신 구간)
            .requiresChannel(channel -> channel.anyRequest().requiresSecure())

            // 2. 요청별 접근 정책 → 보호 리소스 정의 (요청,응답)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/**").authenticated()
                .anyRequest().permitAll()
            )

            // 3. 세션 정책 → 세션 고정 공격 방지 (세션)
            .sessionManagement(session ->
                session.sessionFixation().migrateSession()
            )

            // 4. 인증 실패 응답 정책 (응답)
            .exceptionHandling(ex -> ex
                .accessDeniedPage("/error/403")
            )

            // 5. 로그인/로그아웃 처리 (응답)
            .formLogin(Customizer.withDefaults())
            .logout(Customizer.withDefaults());

        return http.build();
    }
}

대략적으로 요렇게 사용이 되어진다. 

2. 무결성

무결성은 특별하다. 왜냐하면 어플리케이션 계층에서 모든 무결성을 담당하지는 않는다. DB나 infra에서도 무결성은 처리할 수 있다고 한다. 일단 무결성이란 뭘까? 그거 부터 집고 넘어가야 할듯 싶다. 무결성이라는건 무결하다는 걸까? 깨끗? 

무결성(Integrity) 이란
“정보가 인가되지 않은 방식으로 변경되거나 손상되지 않고,
생성 시점부터 소비 시점까지 일관되고 신뢰할 수 있는 상태를 유지하는 것”이다.

즉, 데이터의 신뢰성(data trust)을 의미한다. 그렇다면, 스프링 시큐리티는 데이터를 지키기 위해서 어떤 방법을 사용할 수 있을까?
가장 간단하게 생각할 수 있는건 암호화라고 생각이 든다. 스프링 시큐리티는 데이터를 직접 '보관'하지 않는다고 한다.
대신, 애플리케이션 내부에서 데이터의 신뢰성(Data Trust) 을 유지하기 위해 세 가지 암호학적 메커니즘을 사용한다.

구분  역할  관련 기능 설명
암호화(Encryption) 데이터 노출 차단 TLS, Cookie 암호화 데이터를 숨긴다
서명(Signature) 데이터 변조 검증 JWT, OAuth2 토큰 데이터가 변조되지 않았음을 증명한다
해시(Hash) 데이터 상태 비교 PasswordEncoder, CSRF, Session 데이터의 일관성을 검증한다.

암호화

@Service
@RequiredArgsConstructor
public class UserService {

    private final PasswordEncoder encoder;
    private final UserRepository repo;

    public void register(String username, String password) {
        String encoded = encoder.encode(password); // 암호화 수행
        repo.save(new User(username, encoded));
    }

    public boolean authenticate(String username, String rawPassword) {
        User user = repo.findByUsername(username)
                        .orElseThrow(() -> new UsernameNotFoundException(username));
        return encoder.matches(rawPassword, user.getPassword()); // 해시 비교
    }
}


@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // HTTP 요청은 차단, HTTPS만 허용
            .requiresChannel(channel -> channel
                .anyRequest().requiresSecure()
            )
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            );

        return http.build();
    }
}

 

서명

@Component
public class JwtProvider {

    private static final String SECRET = "secretKeyForJwtSignature"; // HMAC 키
    private final Key key = Keys.hmacShaKeyFor(SECRET.getBytes());

    // 토큰 생성
    public String createToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(Date.from(Instant.now().plus(Duration.ofHours(1))))
                .signWith(key, SignatureAlgorithm.HS256) // 🔑 서명(Signature)
                .compact();
    }

    // 토큰 검증 (서명 위조 탐지)
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token); // Signature 검증
            return true;
        } catch (JwtException e) {
            return false; // 서명 불일치 → 변조된 토큰
        }
    }
}

해시

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            )
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            );

        return http.build();
    }
}

CSRF란? 사용자가 의도하지 않은 요청을 공격자가 대신 수행하게 만드는 공격

다양한 방식으로 무결성을 제공하는것으로 봐서 생각보다 공을 드린 성질 같다고 생각이 든다.

3. 가용성

이 부분은 아쉽게도 스프링 시큐리티에서 제공하지 않는다고 한다. 그렇다면 왜 제공하지 않는지 생각해보자.
가용성이란 뭘까? 인가된 사용자가 필요한 시점에 시스템과 데이터를 정상적으로 이용할 수 있도록 보장하는 것을 의미한다.

대게 가용성이란 시스템 전체의 안전성과 관련된 개념이다.
하지만 다행스럽게도 직접적으로는 제공하지는 않지만 간접적으로 도와?준다고 한다.
스프링 시큐리티는 과도한 접근으로 인한 서비스 불능을 막는 데는 부분적으로 기여한다고 한다.

기능 설명  가용성 기여도
Rate Limiting (요청 제한) 초당 요청 수 제한 → 폭주 방지 ✅ 과부하 완화
Session Timeout 불필요한 세션 정리 → 메모리 확보 ✅ 자원 효율성
Stateless Authentication (JWT) 세션 의존성 제거 → Scale-Out 용이 ✅ 서버 확장성
CORS 정책 불필요한 외부 요청 차단 ✅ 외부 리스크 감소
Remember-Me / Persistent Token 재로그인 부하 감소 ⚙️ 사용자 편의성 향상

4. 인증성

보안에서 특히나 인증이 중요하다. 왜냐하면 기껏 데이터를 암호화하고 보호하면 뭐하나 그 데이터를 인증할 수 있는 수단이 필요하다.
근데 위에서 무결성을 학습할때 인증과 관련된 이야기가 있었던 같은데... 무결성과 인증성은 뭐가 다른거지? 무결성에는 서명과 해시가 존재한다. 두개다 인증이랑도 연관이 있어 보이는데 뭐가 다른걸까?
확인 결과 무결성 같은 경우는 데이터 자체를 검증을 하게 되고, 인증성은 데이터의 출처를 검증하게 된다.
이게 무슨 말이냐면, 무결성은 데이터가 신뢰할 수 있는데이터 인지 검증한다고 했다.

그리고 만약, 출처가 분명하지 않은 데이터를 쓰는건 보안적으로 좋지 않은 행동이기 때문.
물론, 이름때문에 헷갈릴수도 있는데 고거는 감안해야 한다고 생각한다.

다양한 방법으로 데이터의 무결성을 스프링 시큐리티에서 제공하는 방법에 대해 알아봤다.
그렇다면, 인증성을 스프링 시큐리티에서는 제공하는지 알아보자.

스프링 시큐리티는 어떻게 제공하지?

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    Object getCredentials();     // 자격 증명 (비밀번호, 토큰 등)
    Object getPrincipal();       // 사용자 정보 (UserDetails 등)
    boolean isAuthenticated();   // 인증 여부
}

요렇게 Authentication 클래스를 이용해서 제공한다고 한다.

5. 부인방지

그 다음은 부인 방지다. 어떤것을 부인하고 방지를 시켜야 하는데 요것 또한 다른 성질과 크게 다르지 않다고 생각이 든다. 
부인방지는 진짜 특정 사건을 했는지 물어보는 행위라 생각하면 된다.
예를 들어, 백화점에서 물건을 훔쳤다고 가정해보자. 그러면 경찰은 당신을 잡아 심문을 할텐데.. 근데 물건을 계속 샀다고 한다면, 검증이 필요하다. 이것이 바로 부인방지다. 가만보면 인증과 비슷해보이지만, 우리가 주목해야 할 부분은 바로 심문이라는 단어다. 심문은 모든 요청이 종료 된 이후에 확인하는 작업이라 생각한다. 왜냐하면, 왜냐하면 심문은 사건이 '발생한 이후'에 진실을 검증하는 과정이기 때문이다.
즉, 부인방지는 요청이 진행되기 전에 신원을 확인하는 인증(Authentication)과 달리, 요청이 모두 끝난 후에 그 행위의 진위를 증명하는 절차(Post-validation) 라고 볼 수 있다.

그렇기 때문에 스프링 시큐리티에서는 부인방지 기능을 직접적으로 제공하지 않는다.
부인방지는 요청이 모두 종료된 이후(Post-Action),
행위의 결과에 대한 검증과 책임을 추적하는 단계에서 발생한다.
이 시점은 이미 어플리케이션 로직을 벗어나 로그, 감사(Audit), 서명(Signature) 시스템이 관여하는 계층이다.

6. 접근 통제성

마지막 성질인 접근 통제성입니다. 접근을 통제한다라... 기밀성에서 요청을 차단한다고 했었다. 그런데 접근 통제성도 이와 비슷한 느낌이라 생각이 든다. 접근 통제성은 기밀성과 달리 요청을 차단하지는 않는다. 그렇다면 무엇을 차단할까?
수정,생성등 허락되지 않는 행동을 막는다고 할 수 있습니다. 그러니까 특정 방법의 행동의 요청만 허용한다고 이해가 된다.

접근통제성은 단순한 요청 차단이 아니라,
요청의 '행위 수준'을 통제하여 시스템의 안정성과 정책 일관성을 보장하는 보안 속성이다.
다시 말해,

  • 기밀성이 "보여줄지 말지"를 결정한다면,
  • 접근통제성은 "무엇을 할 수 있는가"를 결정한다.

결국 이 성질이 제대로 작동할 때, 시스템은 단순한 인증 시스템을 넘어 "행동 단위의 보안 정책"을 갖추게 된다.

접근 통제성이 인가라고 할 수 있다.

그렇다면 스프링에서는 어떻게 동작할까?

http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/admin/**").hasRole("ADMIN")
    .anyRequest().authenticated()
);

- ADMIN이라는 권한을 사용

@Service
public class DocumentService {

    @PreAuthorize("hasRole('EDITOR')")
    public void editDocument(Long id) {
        // 편집 가능
    }

    @PreAuthorize("hasRole('VIEWER') or hasRole('EDITOR')")
    public Document viewDocument(Long id) {
        return repository.findById(id);
    }
}

- 메소드에서 막기

public void decide(Authentication auth, Object object, Collection<ConfigAttribute> attrs) 
        throws AccessDeniedException {

    int denyCount = 0;

    for (AccessDecisionVoter voter : getDecisionVoters()) {
        int result = voter.vote(auth, object, attrs);
        switch (result) {
            case AccessDecisionVoter.ACCESS_GRANTED:
                return; // 허용 즉시 통과
            case AccessDecisionVoter.ACCESS_DENIED:
                denyCount++;
                break;
        }
    }

    if (denyCount > 0) {
        throw new AccessDeniedException("접근 거부");
    }
}

- 내부 로직에서 사용하는 경우

결론

저때 당시에 아무리 공부를 해도 스프링 시큐리티가 왜 필요한지에 대해 의문을 가졌습니다. 그 이유는 스프링 시큐리티를 단순한 도구로만 생각했지 본질에 대해 깊게 생각하지 않았던거 같습니다. 이 짧은 글을 통해 스프링 시큐리티의 모든 내용을 담을 수는 없다고 생각했고, 저의 역량이 거기까지 하지 못한다고 생각했습니다. 그래서 고민끝에 스프링 시큐리티가 어떤 프레임워크인지 찾아보기 시작했습니다. 이게 핵심인거 같습니다. 

보안 6요소는 인증, 기밀,가용, 무결, 부인방지, 접근통제까지 6요소를 학습하면서 스프링 시큐리티가 이런것을 구조화를 시키고 싶다는 것을 느꼈습니다. 이제 다음은 본격적으로 OAUTH에 대해 학습을 해보려고 합니다. OAUTH가 6요소에서 어디에 속하면서 왜 필요한지 스프링 시큐리티에서는 어떻게 사용이 되어지는지... 더 나아가 OAUTH의 버전까지 학습한다면, 나중에 기억이 오래 남을거라 생각합니다.

 

 

 

댓글

Designed by JB FACTORY