Spring Security

1. 자동 설정에 대한 기본 보안 작동

DEV-HJ 2024. 5. 26. 15:36
반응형

자동 설정에 의한 기본 보안 작동 

스프링 시큐리티를 사용하면 별도의 설정과 개발을 하지 않아도 

기본적인 웹 보안 기능이 현재 애플리케이션에 연동되어 작동한다. 

서버가 기동되면 스프링 시큐리티의 초기화 작업 및 보안 설정이 자동으로 이뤄진다.

 

여기서 개발자가 별도의 설정이나 코드를 작성하지 않아도 기본적인 웹 보안 기능이 현재 시스템에 연동되어 작동한다!

1. 모든 요청에 대해 인증여부를 검증하고 인증 승인 되어야 자원에 접근 가능하다

2. 스프링 시큐리티의 인증 방식은 formLogin과 httpBaisc 로그인 방식 2개를 제공한다

3. 인증을 시도할 수 있는 로그인 페이지를 스프링 시큐리티에서 제공해주고 자동으로 생성되어 렌더링된다.

4. 인증 승인이 이뤄질수 있도록 하나의 계정을 스프링 시큐리티가 제공한다

    - SecurityProperties 설정 클래스에서 생성한다

    - username : user

    - password : 랜덤 문자열

 

위 4가지 기능이 자동으로 작동된다.

 

스프링 시큐리티가 초기화 되면서 SpringBootWebSecurityConfiguration (자동 설정에 의한 기본 보안 설정 클래스 생성)  클래스 안의

defaultSecurityFilterChain 메서드의 기본 설정들이 실행된다 이것으로 위 1~4 까지의 기능이 이뤄진거다. 

defaultSecurityFilterChain 메서드


기본으로 이 기능들이 작동되는데 문제점이있다.

 

첫번째 문제점 (계정 추가나 권한 추가 시 문제점)

하나의 계정을 제공해서 우리가 인증 받을수 있지만, 보안 어플리케이션이 하나의 계정만으로 운영이되나? 불가능하다.

그래서 계정을 추가해야하는데 현재 코드로는 계정 추가를 할 수 없다.

 

두번째 문제점 (시스템에서 필요로 하는 세부적이고 추가적인 보안기능이 필요할 때 문제점)

계정 여러개라 할지라도 계정에 따라 그 계정의 권한이 다를 수 있다.

권한을 다르게 줄 수 있는 방법이 없다.

현재 이 기본 보안 방식 가지고는 운영할 수 없단 얘기다. 그래서 우리가 직접 시큐리티를 커스텀하는 기능을 학습할것이다.


 

 

스프링 시큐리티 설치 후 ctrl+shift+a로 SecurityProperties를 검색하면 스프링 시큐리티의 SecurityProperties 클래스 가 나온다

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.autoconfigure.security;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.DispatcherType;
import org.springframework.util.StringUtils;

@ConfigurationProperties(
    prefix = "spring.security"
)
public class SecurityProperties {
    public static final int BASIC_AUTH_ORDER = 2147483642;
    public static final int IGNORED_ORDER = Integer.MIN_VALUE;
    public static final int DEFAULT_FILTER_ORDER = -100;
    private final Filter filter = new Filter();
    private final User user = new User();

    public SecurityProperties() {
    }

    public User getUser() {
        return this.user;
    }

    public Filter getFilter() {
        return this.filter;
    }

    public static class Filter {
        private int order = -100;
        private Set<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class);

        public Filter() {
        }

        public int getOrder() {
            return this.order;
        }

        public void setOrder(int order) {
            this.order = order;
        }

        public Set<DispatcherType> getDispatcherTypes() {
            return this.dispatcherTypes;
        }

        public void setDispatcherTypes(Set<DispatcherType> dispatcherTypes) {
            this.dispatcherTypes = dispatcherTypes;
        }
    }

    public static class User {
        private String name = "user";
        private String password = UUID.randomUUID().toString();
        private List<String> roles = new ArrayList();
        private boolean passwordGenerated = true;

        public User() {
        }

        public String getName() {
            return this.name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getPassword() {
            return this.password;
        }

        public void setPassword(String password) {
            if (StringUtils.hasLength(password)) {
                this.passwordGenerated = false;
                this.password = password;
            }
        }

        public List<String> getRoles() {
            return this.roles;
        }

        public void setRoles(List<String> roles) {
            this.roles = new ArrayList(roles);
        }

        public boolean isPasswordGenerated() {
            return this.passwordGenerated;
        }
    }
}

 

위 코드를 보면 User 클래스가 보인다.

User 계정이 하나 생성 되는것이다. user가 가지는 이름이 user고 password가 랜덤 문자열인것을 확인할 수 있다.

이 2개의 정보를 시큐리티가 개발자에게 제공한것이다.

 

로그인시 DB에 있는 데이터와 일치하는지 확인해야하는데, 그러기 위해서 getName()을 사용한다.

(현재는 시큐리티에서 제공해주는 기본 계정과 일치하는지 보고 있다)

스프링 시큐리티에서 getName()을 어디서 호출하는지 찾아보자

 

userDetailsServiceAutoConfiguration 클래스에서 getName() 메서드를 호출하고 있다.

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.autoconfigure.security.servlet;

import java.util.List;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.util.StringUtils;

@AutoConfiguration
@ConditionalOnClass({AuthenticationManager.class})
@Conditional({MissingAlternativeOrUserPropertiesConfigured.class})
@ConditionalOnBean({ObjectPostProcessor.class})
@ConditionalOnMissingBean(
    value = {AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class},
    type = {"org.springframework.security.oauth2.jwt.JwtDecoder"}
)
public class UserDetailsServiceAutoConfiguration {
    private static final String NOOP_PASSWORD_PREFIX = "{noop}";
    private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");
    private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);

    public UserDetailsServiceAutoConfiguration() {
    }

    @Bean
    public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties, ObjectProvider<PasswordEncoder> passwordEncoder) {
        SecurityProperties.User user = properties.getUser();
        List<String> roles = user.getRoles();
        return new InMemoryUserDetailsManager(new UserDetails[]{User.withUsername(user.getName()).password(this.getOrDeducePassword(user, (PasswordEncoder)passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build()});
    }

    private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
        String password = user.getPassword();
        if (user.isPasswordGenerated()) {
            logger.warn(String.format("%n%nUsing generated security password: %s%n%nThis generated password is for development use only. Your security configuration must be updated before running your application in production.%n", user.getPassword()));
        }

        return encoder == null && !PASSWORD_ALGORITHM_PATTERN.matcher(password).matches() ? "{noop}" + password : password;
    }

    static final class MissingAlternativeOrUserPropertiesConfigured extends AnyNestedCondition {
        MissingAlternativeOrUserPropertiesConfigured() {
            super(ConfigurationPhase.PARSE_CONFIGURATION);
        }

        @ConditionalOnProperty(
            prefix = "spring.security.user",
            name = {"password"}
        )
        static final class PasswordConfigured {
            PasswordConfigured() {
            }
        }

        @ConditionalOnProperty(
            prefix = "spring.security.user",
            name = {"name"}
        )
        static final class NameConfigured {
            NameConfigured() {
            }
        }

        @ConditionalOnMissingClass({"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository", "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector", "org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository"})
        static final class MissingAlternative {
            MissingAlternative() {
            }
        }
    }
}

 

이 클래스도 하나의 설정 클래스인데,

inMemoryUserDetailsManager 메서드로 user 정보를 관리할 수 있는 매니저 클래스를 만드는것이다.

 

user정보를 가져와서 최종적으로 User 객체를 생성하고, 메모리에 저장하는 클래스를 been으로 생성하고 있는것이다.

이렇게 해서 우리에게 계정이 제공됐다.


 

 

초기화 가정을 살펴보자. 어떤 클래스에서 초기화 작업을할까?

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.autoconfigure.security.servlet;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.security.ConditionalOnDefaultWebSecurity;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
class SpringBootWebSecurityConfiguration {
    SpringBootWebSecurityConfiguration() {
    }

    @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnMissingBean(
        name = {"springSecurityFilterChain"}
    )
    @ConditionalOnClass({EnableWebSecurity.class})
    @EnableWebSecurity
    static class WebSecurityEnablerConfiguration {
        WebSecurityEnablerConfiguration() {
        }
    }

    @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnDefaultWebSecurity
    static class SecurityFilterChainConfiguration {
        SecurityFilterChainConfiguration() {
        }

        @Bean
        @Order(2147483642)
        SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests((requests) -> {
                ((AuthorizeHttpRequestsConfigurer.AuthorizedUrl)requests.anyRequest()).authenticated();
            });
            http.formLogin(Customizer.withDefaults());
            http.httpBasic(Customizer.withDefaults());
            return (SecurityFilterChain)http.build();
        }
    }
}

 

SpringBootWebSecurityConfiguration 클래스에서 진행된다.

 

 

   @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnDefaultWebSecurity
    static class SecurityFilterChainConfiguration {
        SecurityFilterChainConfiguration() {
        }

        @Bean
        @Order(2147483642)
        SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests((requests) -> {
                ((AuthorizeHttpRequestsConfigurer.AuthorizedUrl)requests.anyRequest()).authenticated();
            });
            http.formLogin(Customizer.withDefaults());
            http.httpBasic(Customizer.withDefaults());
            return (SecurityFilterChain)http.build();
        }
    }

위 설정 클래스에서, 이 만큼의 코드가 실행이 되는것이다.

이 코드도 결국 하나의 been을 생성하는것이다. SecurityFilterChain타입의 객체 빈을 생성하는것이다.

 

이게 무조건 생성하는건 아니다. 조건이 있다. 

그 조건이 @ConditionalOnDefaultWebSecurity 이다. 들어가서 봐보자

 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.autoconfigure.security;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Conditional;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({DefaultWebSecurityCondition.class})
public @interface ConditionalOnDefaultWebSecurity {
}

안에 보면 @Conditional({DefaultWebSecurityCondition.class}) 이 있다. DefaultWebSecurityCondition을 봐보자

 

 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.autoconfigure.security;

import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

class DefaultWebSecurityCondition extends AllNestedConditions {
    DefaultWebSecurityCondition() {
        super(ConfigurationPhase.REGISTER_BEAN);
    }

    @ConditionalOnMissingBean({SecurityFilterChain.class})
    static class Beans {
        Beans() {
        }
    }

    @ConditionalOnClass({SecurityFilterChain.class, HttpSecurity.class})
    static class Classes {
        Classes() {
        }
    }
}

DefaultWebSecurityCondition 이 클래스 안에,  이 조건들을 보는것이다.

조건이 2개가 있는데 2개가 모두 참이여만 최종적으로 기본 보안 작동이 이뤄지는것이다.

 

@ConditionalOnClass({SecurityFilterChain.class, HttpSecurity.class})

SecurityFilterChain, htttpSecurity 클래스가 지금 클래스패스에 존재하느냐?

우리가 시큐리티 의존성을 추가했기 때문에 클래스패스에 존재한다. 참이다.

 

클래스패스란?

 

 

@ConditionalOnMissingBean({SecurityFilterChain.class})

SecurityFilterChain이 현재 생성이 되어있지 않으면 참이다.

SecurityFilterChain이란 타입의 객체를 아직 생성한적이 없다. 그럼 참이 된다. 아직 어떤 클래스도 만든적이 없다.

 

 

   @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnDefaultWebSecurity
    static class SecurityFilterChainConfiguration {
        SecurityFilterChainConfiguration() {
        }

        @Bean
        @Order(2147483642)
        SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests((requests) -> {
                ((AuthorizeHttpRequestsConfigurer.AuthorizedUrl)requests.anyRequest()).authenticated();
            });
            http.formLogin(Customizer.withDefaults());
            http.httpBasic(Customizer.withDefaults());
            return (SecurityFilterChain)http.build();
        }
    }

그럼으로 이 구문이 실행됨으로 스프링 시큐리티가 초기화될때, 웹 보안 기능이 이뤄진다.


위 코드가 실행되면서 httpSecurity 객체가 주입되고, 

http.authorizeHttpRequests((requests) -> {
    ((AuthorizeHttpRequestsConfigurer.AuthorizedUrl)requests.anyRequest()).authenticated();
});

anyRequest() 메서드로 모든 요청이, authenticated() 메서드로 인증 되어야만 서버 접근이 가능하도록 설정했다.

 

authorizeHttpRequests()

이 메서드는 Http 통신으로 인해서 인가 여부를 보겠다는 메서드다. 

이 설정을 해서 indexController에서 기본 페이지를 index로 설정하고 8080 port를 열어도 index 페이지로 못갔다. 

 

 

 

 

http.formLogin(Customizer.withDefaults());
http.httpBasic(Customizer.withDefaults());

그 인증을 받기 위해서, 시큐리티에서 인증 페이지를 제공하는데

그 설정을 이 코드에서, 여기서 한다.

우리는 web을 사용하기 때문에 formLogin 방식으로 인증하도록 로그인 페이지가 생성된것이다.


 

요약

1. 스프링 시큐리티 의존성을 추가했다

2. 그 다음, indexController를 만들고, 서버 기동 후 8080 port로 접속 했는데도 불구하고 index 화면으로 가지 못했다.

3. 원하는 페이지로 가지 못하고 로그인 페이지로 강제 이동 되었다.

4. 거기서 주어진 계정으로 로그인 했을때 그제서야 indexController 로 갈 수 있었다.

5. 그 이유가 스프링 시큐리티의 이 설정 때문이였다 ↓

 

반응형