HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🔒
Spring Boot — Spring Security Essentials
/
2️⃣
02. Spring Security Architecture
02. Spring Security Architecture
2️⃣

02. Spring Security Architecture

github.com
https://github.com/prgrms-be-devcourse/w16-SpringBoot_Part_C/tree/class_2

지난 미션 리뷰

configure(AuthenticationManagerBuilder auth) 메소드 override
  • passwordEncoder는 NoOpPasswordEncoder로 사용함
  • 기본 로그인 계정을 AuthenticationManagerBuilder 클래스를 통해 추가
    로그아웃, Cookie 기반 자동 로그인 (Remember-Me) 기능 설정하기
    • HttpSecurity 클래스의 logout() API를 통해 로그아웃 기능을 설정
      • 로그아웃 처리 path “/logout”
      • 로그아웃 성공 후 리다이렉션 path “/”
    • HttpSecurity 클래스의 rememberMe() API를 통해 Cookie 기반 자동 로그인 기능을 설정
      • 파라미터명 “remember-me”
      • 자동 로그인 토큰 유효기간 5분
    솔루션
    • 기본 로그인 계정을 추가할때 password 설정시 주의점
      • Spring Security 5에서는 DelegatingPasswordEncoder 클래스가 기본 PasswordEncoder로 사용됨
      • DelegatingPasswordEncoder 클래스는 패스워드 해시 알고리즘별로 PasswordEncoder를 제공하는데, 해시 알고리즘별 PasswordEncoder 선택을 위해 패스워드 앞에 prefix를 추가함
      • {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG {noop}password {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
        public static PasswordEncoder createDelegatingPasswordEncoder() { String encodingId = "bcrypt"; Map<String, PasswordEncoder> encoders = new HashMap<>(); encoders.put(encodingId, new BCryptPasswordEncoder()); encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder()); encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder()); encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5")); encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance()); encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); encoders.put("scrypt", new SCryptPasswordEncoder()); encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1")); encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256")); encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder()); encoders.put("argon2", new Argon2PasswordEncoder()); return new DelegatingPasswordEncoder(encodingId, encoders); }
        PasswordEncoderFactories 클래스 구현 발췌
      • prfiex 부분이 생략되는 경우 기본 PasswordEncoder로 bcrypt가 사용됨
      • password 해시 알고리즘을 변경하거나, 강력한 해시 알고리즘을 사용하여 password를 업그레이드 할 수 있도록 함
      • ⚠️
        InMemoryUserDetailsManager 객체를 사용한다면(보다 정확하게는 UserDetailsPasswordService 인터페이스 구현체) 최초 로그인 1회 성공시, {noop} 타입에서 → {bcrypt} 타입으로 PasswordEncoder가 변경된다.
      • DelegatingPasswordEncoder 사용이 필요 없다면 BCryptPasswordEncoder 클래스를 명시적으로 Bean 선언하면됨
    • 전체 코드
    @Configuration @EnableWebSecurity public class WebSecurityConfigure extends WebSecurityConfigurerAdapter { @Override public void configure(WebSecurity web) { web.ignoring().antMatchers("/assets/**"); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user").password("{noop}user123").roles("USER") .and() .withUser("admin").password("{noop}admin123").roles("ADMIN") ; } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/me").hasAnyRole("USER", "ADMIN") .anyRequest().permitAll() .and() .formLogin() .defaultSuccessUrl("/") .permitAll() .and() /** * remember me 설정 */ .rememberMe() .rememberMeParameter("remember-me") .tokenValiditySeconds(300) .and() /** * 로그아웃 설정 */ .logout() .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .logoutSuccessUrl("/") .invalidateHttpSession(true) .clearAuthentication(true) ; } }
    remeber-me 가 적용된 로그인 화면
    remeber-me 가 적용된 로그인 화면

    Spring Security Architecture

    Conceptual Architecture
    • 거시적인 관점에서 Spring Security는 웹 요청을 가로챈 후 사용자를 인증하고, 인증된 사용자가 적절한 권한을 지니고 있는 확인함
      • AuthenticationManager 사용자 인증 관련 처리
      • AccessDecisionManager 사용자가 보호받는 리소스에 접근할 수 있는 적절한 권한이 있는지 확인
      • https://www.slideshare.net/analizator/spring-security-framework
    FilterChainProxy (Spring Security 필터 체인) 소개
    • Spring Security의 실제적인 구현은 서블릿 필터 (javax.servlet.Filter 인터페이스 구현체) 를 통해 이루어짐
      • 서블릿 필터는 웹 요청을 가로챈 후 전처리 또는 후처리를 수행하거나, 요청 자체를 리다이렉트 하기도 함
    • FilterChainProxy 세부 내용은 WebSecurityConfigurerAdapter 추상 클래스를 상속하는 구현체에서 설정함 (보통 @EnableWebSecurity 어노테이션도 함께 사용)
      • 웹 요청은 이러한 필터 체인을 차례로 통과하게 됨
        • 웹 요청은 모든 필터를 통과하게 되지만, 모든 필터가 동작하는 것은 아님
        • 각 필터는 웹 요청에 따라 동작 여부를 결정할 수 있고, 동작할 필요가 없다면 다음 필터로 웹 요청을 즉시 넘김
      • 요청을 처리하고 응답을 반환하면 필터 체인 호출 스택은 모든 필터에 대해 역순으로 진행
      • 보통 springSecurityFilterChain 이라는 이름으로 Bean 등록됨
    • 웹 요청은 어떻게 FilterChainProxy로 전달될까?
      • 웹 요청을 수신한 서블릿 컨테이너는 해당 요청을 DelegatingFilterProxy (javax.servlet.Filter 인터페이스 구현체) 로 전달함
        • DelegatingFilterProxy Bean은 SecurityFilterAutoConfiguration 클래스에서 자동으로 등록됨
          • @Bean @ConditionalOnBean(name = DEFAULT_FILTER_NAME) public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(SecurityProperties securityProperties) { DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(DEFAULT_FILTER_NAME); registration.setOrder(securityProperties.getFilter().getOrder()); registration.setDispatcherTypes(getDispatcherTypes(securityProperties)); return registration; }
            SecurityFilterAutoConfiguration 구현 발췌 (DelegatingFilterProxyRegistrationBean 을 통해 DelegatingFilterProxy 인스턴스를 생성함)
        • DelegatingFilterProxy는 실제적으로 웹 요청을 처리할 Target Filter Bean을 지정해야함
          • Target Filter Bean은 바로 앞에서 알아본 FilterChainProxy
          • @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { // Lazily initialize the delegate if necessary. Filter delegateToUse = this.delegate; if (delegateToUse == null) { synchronized (this.delegateMonitor) { delegateToUse = this.delegate; if (delegateToUse == null) { WebApplicationContext wac = findWebApplicationContext(); if (wac == null) { throw new IllegalStateException("No WebApplicationContext found: " + "no ContextLoaderListener or DispatcherServlet registered?"); } delegateToUse = initDelegate(wac); } this.delegate = delegateToUse; } } // Let the delegate perform the actual doFilter operation. invokeDelegate(delegateToUse, request, response, filterChain); } protected Filter initDelegate(WebApplicationContext wac) throws ServletException { String targetBeanName = getTargetBeanName(); Assert.state(targetBeanName != null, "No target bean name set"); Filter delegate = wac.getBean(targetBeanName, Filter.class); if (isTargetFilterLifecycle()) { delegate.init(getFilterConfig()); } return delegate; } protected void invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { delegate.doFilter(request, response, filterChain); }
            DelegatingFilterProxy 구현 발췌 (실제적으로 요청을 처리할 delegate 로 요청을 전달함)
            https://docs.spring.io/spring-security/site/docs/current/reference/html5/#servlet-architecture
    FilterChainProxy를 구성하는 Filter 목록
    • 정말 다양한 필터 구현을 제공함
    • 결국 Spring Security를 잘 이해하고 활용한다는 것은 이들 Filter 를 이해하고, 적절하게 사용한다는 것을 의미함
    Spring Security Reference
    Imagine you're designing an application for a pet clinic. There will be two main groups of users of your Spring-based application: staff of the pet clinic, as well as the pet clinic's customers. The staff will have access to all of the data, whilst your customers will only be able to see their own customer records.
    Spring Security Reference
    https://docs.spring.io/spring-security/site/docs/current/reference/html5/#servlet-security-filters
    간단한 몇 개의 Filter에 대해 자세히 알아보자.

    인증 요청에 의해 가로채어진 원래 요청으로 이동하기 (RequestCacheAwareFilter)

    • 익명 사용자가 보호 받는 리소스 (예: /me)에 접근할 경우
      • 접근 권한이 없기 때문에 AccessDecisionManager 에서 접근 거부 예외가 발생함
      • ExceptionTranslationFilter 접근 거부 예외를 처리함
        • 현재 사용자가 익명 사용자라면, 보호 받는 리소스로의 접근을 캐시처리하고, 로그인 페이지로 이동 시킴
        private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AccessDeniedException exception) throws ServletException, IOException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication); if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) { if (logger.isTraceEnabled()) { logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied", authentication), exception); } sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException( this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource"))); } else { if (logger.isTraceEnabled()) { logger.trace( LogMessage.format("Sending %s to access denied handler since access is denied", authentication), exception); } this.accessDeniedHandler.handle(request, response, exception); } } protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException { // SEC-112: Clear the SecurityContextHolder's Authentication, as the // existing Authentication is no longer considered valid SecurityContextHolder.getContext().setAuthentication(null); this.requestCache.saveRequest(request, response); this.authenticationEntryPoint.commence(request, response, reason); }
        ExceptionTranslationFilter 구현 일부 발췌
    • RequestCacheAwareFilter를 통해 위에서 살펴본 캐시된 요청을 처리할 수 있음
      • 캐시된 요청이 있다면 캐시된 요청을 처리하고, 캐시된 요청이 없다면 현재 요청을 처리함
      • @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest wrappedSavedRequest = this.requestCache.getMatchingRequest((HttpServletRequest) request, (HttpServletResponse) response); chain.doFilter((wrappedSavedRequest != null) ? wrappedSavedRequest : request, response); }
        RequestCacheAwareFilter 구현 일부 발췌

    전송 레이어 보안 적용 (ChannelProcessingFilter)

    전송 레이어 보안을 위해 SSL 인증서를 생성하고, 이를 Spring Boot 웹 어플리케이션에 적용한다. 이제 웹 어플리케이션은 HTTPS 프로토콜을 통해 서비스 된다.
    HTTP와 HTTPS
    • HTTP(Hyper Text Transfer Protocol)는 인터넷상에서 데이터를 주고 받기 위한 프로토콜
      • 클라이언트와 서버가 주고 받는 데이터는 암호화되어 있지 않음
      • 따라서, 악의적인 데이터 감청, 데이터 변조의 가능성이 있음
    • HTTPS(HyperT ext Transfer Protocol Secure)는 HTTP 프로토콜의 암호화 버전
      • 클라이언트와 서버가 주고 받는 모든 데이터는 암호화되어 있음
      • 데이터 암호화를 위해 SSL(Secure Sockets Layer)을 사용
      • 💡
        SSL은 Netscape가 개발했으며 SSL 3.0부터 TLS라는 이름으로 변경되었다. 일반적으로 SSL, TLS은 같은 의미를 지닌다. 그러나 SSL이란 용어가 더 많이 사용된다.
    • SSL 암호화를 위해 SSL 인증서가 필요함
      • 서버는 SSL인증서를 클라이언트에 전달함
      • 클라이언트는 서버가 전달한 SSL 인증서를 검증하고, 신뢰할 수 있는 서버인지 확인함
      • 신뢰할 수 있는 서버라면 SSL 인증서의 공개키를 이용해 실제 데이터 암호화에 사용될 암호화키를 암호화하여 서버에 전달함
        • 실제 데이터 암복호화는 대칭키 방식
        • 서버와 클라이언트 사이의 대칭키 공유를 위해 RSA 암호화를 사용함
    SSL 인증서 생성
    • keytool 도구를 이용해 임의로 SSL 인증서를 생성할 수 있음 (keytool은 Java 설치 경로 bin 디렉토리 아래에 위치함)
      • 물론 실제 서비스에는 사용할 수 없으며, 어디까지나 로컬 테스트 용도로만 활용해야 함
      • notion image
    • keystore 만들기
      • keytool -genkey -alias [keystore 별칭] -keyalg RSA -storetype PKCS12 -keystore [keystore 파일]
      • iyboklee@DESKTOP-6I1BVIA:/mnt/c/Users/iybok/stores$ keytool -genkey -alias prgrms_keystore -keyalg RSA -storetype PKCS12 -keystore prgrms_keystore.p12 Enter keystore password: Re-enter new password: What is your first and last name? [Unknown]: localhost What is the name of your organizational unit? [Unknown]: Prgrms What is the name of your organization? [Unknown]: Prgrms What is the name of your City or Locality? [Unknown]: Seoul What is the name of your State or Province? [Unknown]: Seoul What is the two-letter country code for this unit? [Unknown]: KR Is CN=localhost, OU=Prgrms, O=Prgrms, L=Seoul, ST=Seoul, C=KR correct? [no]: y iyboklee@DESKTOP-6I1BVIA:/mnt/c/Users/iybok/stores$ ls prgrms_keystore.p12
    • keystore 에서 인증서 추출하기
      • keytool -export -alias [keystore 별칭] -keystore [keystore 파일] -rfc -file [인증서 파일]
      • iyboklee@DESKTOP-6I1BVIA:/mnt/c/Users/iybok/stores$ keytool -export -alias prgrms_keystore -keystore prgrms_keystore.p12 -rfc -file prgrms.cer Enter keystore password: Certificate stored in file <prgrms.cer> iyboklee@DESKTOP-6I1BVIA:/mnt/c/Users/iybok/stores$ ls prgrms.cer prgrms_keystore.p12
    • trust-store 만들기
      • keytool -import -alias [trust keystore 별칭] -file [인증서 파일] -keystore [trust keystore 파일]
      • iyboklee@DESKTOP-6I1BVIA:/mnt/c/Users/iybok/stores$ keytool -import -alias prgrms_truststore -file prgrms.cer -keystore prgrms_truststore.p12 Enter keystore password: Re-enter new password: Owner: CN=localhost, OU=Prgrms, O=Prgrms, L=Seoul, ST=Seoul, C=KR Issuer: CN=localhost, OU=Prgrms, O=Prgrms, L=Seoul, ST=Seoul, C=KR Serial number: 16cd5188 Valid from: Thu Aug 19 19:37:07 KST 2021 until: Wed Nov 17 19:37:07 KST 2021 Certificate fingerprints: MD5: 26:91:CD:3D:BC:B9:2E:C7:6B:23:2C:B0:3C:DF:E2:BB SHA1: 3E:85:57:2A:7B:51:2B:20:5A:F8:FB:92:41:87:6C:41:A4:1E:01:A5 SHA256: 63:AD:A4:85:49:08:B7:01:75:36:34:A6:02:B6:2A:9B:1F:16:C0:5D:63:CE:F2:66:68:71:65:6E:31:1E:4B:D6 Signature algorithm name: SHA256withRSA Subject Public Key Algorithm: 2048-bit RSA key Version: 3 Extensions: #1: ObjectId: 2.5.29.14 Criticality=false SubjectKeyIdentifier [ KeyIdentifier [ 0000: DB 81 03 CF 01 A9 25 34 70 46 F4 FF EF 8D BA 3D ......%4pF.....= 0010: 24 C7 3B 6C $.;l ] ] Trust this certificate? [no]: y Certificate was added to keystore iyboklee@DESKTOP-6I1BVIA:/mnt/c/Users/iybok/stores$ ls prgrms.cer prgrms_keystore.p12 prgrms_truststore.p12
    SSL 인증서 적용
    • prgrms_keystore.p12, prgrms_truststore.p12 2개 파일을 resources 디렉토리로 복사 후 application.xml 파일에 설정 추가
      • 포트를 443으로 변경 (HTTPS 기본 포트)
      • server.ssl 설정 추가
      • spring: application: name: spring security 01 autoconfigure: exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration thymeleaf: cache: true security: user: name: user password: user123 roles: USER messages: basename: i18n/messages encoding: UTF-8 cache-duration: PT1H server: port: 443 ssl: enabled: true key-alias: prgrms_keystore key-store: classpath:prgrms_keystore.p12 key-store-password: prgrms123 key-password: prgrms123 trust-store: classpath:prgrms_truststore.p12 trust-store-password: prgrms123
    • 웹 어플리케이션을 시작하면 443 (https) 포트를 통해 서비스가 기동하는 것을 로그로 확인할 수 있음
      • notion image
    • 웹 브라우저 주소에 https://localhost 를 입력
      • 정상적으로 페이지 접근을 확인할 수 있음
        • 로그인/로그아웃도 정상적으로 수행 가능
      • SSL 인증서가 유효하지 않기 때문에 경고가 뜸 (유효한 인증서라면 경고가 뜨지 않음)
        • 브라우저 마다 경고가 다를 수 있음 (크롬의 경우 HTTPS 연결이 사용되지 않았다는 경고 메시지 발생)
        notion image
    Spring Security 설정하기
    • ChannelProcessingFilter 설정을 통해 HTTPS 채널을 통해 처리해야 하는 웹 요청을 정의할 수 있음
      • FilterInvocationSecurityMetadataSource 클래스에 HTTPS 프로토콜로 처리해야 URL 정보가 담김
      • 실제적인 처리를 ChannelDecisionManager 클래스로 위임함
      • public class ChannelProcessingFilter extends GenericFilterBean { private ChannelDecisionManager channelDecisionManager; private FilterInvocationSecurityMetadataSource securityMetadataSource; // ...생략... @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; FilterInvocation filterInvocation = new FilterInvocation(request, response, chain); Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(filterInvocation); if (attributes != null) { this.logger.debug(LogMessage.format("Request: %s; ConfigAttributes: %s", filterInvocation, attributes)); this.channelDecisionManager.decide(filterInvocation, attributes); if (filterInvocation.getResponse().isCommitted()) { return; } } chain.doFilter(request, response); } // ...생략... }
        ChannelProcessingFilter 구현 일부 발췌
    • HttpSecurity 클래스를 통해 ChannelProcessingFilter 세부 설정을 할 수 있음
      • @Override protected void configure(HttpSecurity http) throws Exception { http /** * HTTP 요청을 HTTPS 요청으로 리다이렉트 */ .requiresChannel() .anyRequest().requiresSecure() ; }

    미션

    • AnonymousAuthenticationFilter, ExceptionTranslationFilter 에 대해 정리해보기
    • 대칭 키 암호화, RSA 암호화에 대해 정리해보기
    • SSL 인증서를 직접 생성해보고, Spring Boot 프로젝트에 적용해보기
      •