본문 바로가기

WEB개발/Spring

[Spring] Spring Security

 

 Spring Security는 Java 기반의 애플리케이션에서 보안을 관리하고 구현하기 위한 프레임워크입니다. 주로 스프링 프레임워크와 함께 사용되며, 웹 애플리케이션과 RESTful API를 포함한 다양한 애플리케이션에서 인증(Authentication)과 권한 부여(Authorization) 기능을 제공합니다.

 

  1. 인증(Authentication): 사용자나 클라이언트가 누구인지 확인하는 과정입니다. 예를 들어, 사용자가 로그인할 때 사용자의 자격 증명(예: 사용자 이름과 비밀번호)을 확인하여 신원을 인증합니다.

  2. 권한 부여(Authorization): 인증된 사용자가 어떤 자원이나 기능에 접근할 수 있는지 결정하는 과정입니다. 예를 들어, 관리자만 특정 페이지에 접근할 수 있도록 설정할 수 있습니다.

  3. 보안 정책 구성: Spring Security는 다양한 보안 정책을 구성할 수 있는 유연성을 제공합니다. 예를 들어, 특정 URL 패턴에 대해 인증을 요구하거나, 특정 역할에 따라 접근 권한을 제한할 수 있습니다.

  4. CSRF 보호: Cross-Site Request Forgery(CSRF) 공격을 방지하기 위한 보호 기능을 제공합니다. Spring Security는 자동으로 CSRF 토큰을 생성하고 검증하여 애플리케이션의 보안을 강화합니다.

  5. 세션 관리: 사용자 세션을 관리하고, 세션 고정 공격(Session Fixation Attack)을 방지하는 기능을 제공합니다.

  6. OAuth( Open Authorization ) 2 및 JWT 지원: OAuth2를 통한 소셜 로그인, JWT(JSON Web Token)를 통한 토큰 기반 인증 등의 현대적인 보안 요구사항도 지원합니다.

 

 

1. 필터 체인 (AuthenticationFilter)

 

Spring Security는 필터 체인 아키텍처를 사용하여 요청을 처리합니다. HTTP 요청이 들어오면, 여러 필터가 요청을 순차적으로 가로채어 필요한 인증 및 권한 부여 절차를 수행합니다. 주요 필터는 다음과 같습니다:

  • UsernamePasswordAuthenticationFilter: 사용자 이름과 비밀번호를 기반으로 인증을 처리합니다.
  • BasicAuthenticationFilter: 기본 인증을 처리합니다.
  • CsrfFilter: CSRF 공격 방지를 위해 CSRF 토큰을 확인합니다.

 

2. 인증 프로세스

  1. 사용자 인증 요청 : 사용자가 로그인 양식을 제출하면, 인증 정보(예: 사용자 이름 및 비밀번호)가 서버로 전송됩니다.
  2. AuthenticationManager : Spring Security는 AuthenticationManager를 통해 인증을 수행합니다. 기본적으로 ProviderManager를 사용하여 등록된 여러 인증 제공자 중에서 적절한 제공자를 찾습니다.
  3. UserDetailsService : 사용자의 세부 정보를 가져오기 위해 UserDetailsService를 사용합니다. 이 서비스는 데이터베이스나 다른 저장소에서 사용자 정보를 조회합니다.
  4. PasswordEncoder : 비밀번호가 안전하게 저장되기 위해 해시된 비밀번호와 비교하는 데 사용됩니다.

 

3. 권한 부여

 

인증이 성공하면, Spring Security는 사용자에게 권한을 부여합니다. 사용자가 요청한 리소스에 대한 접근 권한을 확인하기 위해 다음을 수행합니다:

  • AuthorizationDecisionManager : 이 컴포넌트는 사용자의 권한과 요청된 리소스의 접근 권한을 비교하여 결정합니다.
  • SecurityExpressionHandler : 표현식 언어를 사용하여 특정 작업에 대한 접근 제어를 구현합니다.

 

4. 세션 관리

 

Spring Security는 사용자의 인증 정보를 세션에 저장하여 지속적인 인증을 유지합니다. 세션 관리 기능은 다음을 포함합니다:

  • 세션 고정 공격 방지: 사용자가 로그인할 때 새로운 세션을 생성하여 공격을 방지합니다.
  • 동시 세션 제어: 사용자가 동시에 여러 세션을 가질 수 있는지 여부를 제어합니다.
//sessionManagement().sessionFixation().none() : 로그인 시 세션 정보 변경 안함 
//sessionManagement().sessionFixation().newSession() : 로그인 시 세션 새로 생성 
//sessionManagement().sessionFixation().changeSessionId() : 로그인 시 동일한 세션에 대한 id 변경
http
.sessionManagement((session) -> session
.sessionFixation((sessionFixation) -> sessionFixation
	.newSession()
));

 

5. CSRF 보호

 

Spring Security는 기본적으로 CSRF 보호를 제공하여 악의적인 요청으로부터 애플리케이션을 보호합니다. 이는 각 요청에 대해 CSRF 토큰을 요구하여 클라이언트가 요청을 보낼 때 토큰을 확인합니다.

CSRF는 Cross-Site Request Forgery의 약자로, 사용자가 의도하지 않은 요청을 보내게 하는 웹 공격입니다. 이 공격은 웹 애플리케이션이 사용자의 인증 정보를 기반으로 요청을 수행할 수 있는 것을 이용합니다.

 

 

 


 

SecurityConfig.java 예시

package com.example.springsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import com.example.springsecurity.service.CustomUserDetailsService;
import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final CustomUserDetailsService customUserDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        
        http
            .authorizeHttpRequests((auth) -> auth
                    .requestMatchers("/", "/login", "/loginProc", "/join", "/joinProc").permitAll()
                    .requestMatchers("/admin").hasRole("ADMIN")
                    .requestMatchers("/test/**").hasAnyRole("ADMIN", "USER")
                    .anyRequest().authenticated())
            .userDetailsService(customUserDetailsService);

        http.formLogin((auth)-> auth
            .loginPage("/login")
            .loginProcessingUrl("/loginProc")
            .permitAll()
            
            );

        //Http Basic 인증 방식은 아이디와 비밀번호를 Base64 방식으로 인코딩한 뒤 HTTP 인증 헤더에 부착하여 서버측으로 요청을 보내는 방식이다.
        //username:password > dXNlcm5hbWU6cGFzc3dvcmQ=
        /**
        GET /protected/resource HTTP/1.1
        Host: example.com
        Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
         */
        http.httpBasic(Customizer.withDefaults());

        http
            .sessionManagement((session) -> session
                .sessionFixation((sessionFixation) -> sessionFixation
                    .newSession()
                )
            );
    

        //http.csrf((auth)-> auth.disable());

        return http.build();
    }


    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }


    //inMemory방식
    /*@Bean
    public UserDetailsService userDetailsService() {

        UserDetails user1 = User.builder()
                .username("user1")
                .password(bCryptPasswordEncoder().encode("1234"))
                .roles("ADMIN")
                .build();

        UserDetails user2 = User.builder()
                .username("user2")
                .password(bCryptPasswordEncoder().encode("1234"))
                .roles("USER")
                .build();

        return new InMemoryUserDetailsManager(user1, user2);
    }*/


    //계층권한
    //A < B < C
            
    @Bean
    public RoleHierarchy roleHierarchy() {
        //filter에 추가
        /* http
            .authorizeHttpRequests((auth) -> auth
                    .requestMatchers("/login").permitAll()
                    .requestMatchers("/").hasAnyRole("A")
                    .requestMatchers("/manager").hasAnyRole("B")
                    .requestMatchers("/admin").hasAnyRole("C")
                    .anyRequest().authenticated()
            ); */
                
    return RoleHierarchyImpl.withRolePrefix("접두사_")
            .role("C").implies("B")
            .role("B").implies("A")
            .build();
    }   
}

 

 

CustomUserDetailService.java 예시

package com.example.springsecurity.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.example.springsecurity.dto.CustomUserDetails;
import com.example.springsecurity.entity.UserEntity;
import com.example.springsecurity.repository.UserRepository;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        UserEntity userData = userRepository.findByUsername(username);

        if (userData != null) {
            return new CustomUserDetails(userData);
        }

        return null;
    }
}