ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SpringSecurity 01
    Spring&SpringBoot/SpringSecurity 2025. 9. 15. 17:12

    SecurityFilterChain 타입 빈을 정의한 후 인증 API 및 인가 API 설정

    • 인증 API : 사용자나 시스템이 자신을 증명하는 과정
      • "너 누구야?"
      • ID+비밀번호, 토큰(JWT, OAuth2 Access Token) 등
    • 인가 API : 인증된 사용자가 요청한 리소스나 기능에 접근 권한이 있는지 검사
      • "너 뭐 할 수 있어?"
      • 권한(ROLE), 정책에 따라 접근 허용/거부

     

     

    @RestController
    public class IndexController {
        @GetMapping("/")
        public String index() {
            return "index";
        }
    }
    @EnableWebSecurity
    @Configuration
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                .formLogin(Customizer.withDefaults());
            return http.build();
        }
    }

     

     

    @EnableWebSecurity

    @EnableWebSecurity 어노테이션은 Spring Security를 사용하여 웹 보안을 구성할 때 사용하는 어노테이션

    이 어노테이션을 사용하면 Spring Security와 관련된 설정을 할 수 있다

    • 자동으로 SpringSecurityFilterChain 생성하고 웹 보안 활성화
    • HttpSecurity 객체를 사용하여 Http 요청에 대한 보안 설정
    • AuthenticationManagerBuilder 객체를 사용하여 사용자 인증 정보 설정
    • WebSecurityConfigurerAdapter 클래스를 상속받은 설정 클래스 등
    • Spring Security 7 버전부터는 람다 형식만 지원 예정
    • SecurityFilterChain을 빈으로 정의하게 되면 자동설정에 의한 SecurityFilterChain 빈은 생성되지 않음

     

    설정 후 run 하면 랜덤 비밀번호를 콘솔에서 확인할 수 있고 Spring Security 기본 로그인 페이지에서 로그인을 확인할 수 있다

     

     

    임의의 사용자 추가

    1. application.yml

    # spring.application.name=security-ex
    spring:
      security:
        user:
          name: user
          password: 1234
          roles: USER

     

     

    2. 자바 설정 클래스 정의

    @EnableWebSecurity
    @Configuration
    public class SecurityConfig {
    	...
    
        @Bean
        public UserDetailsService userDetailsService() {
            UserDetails user = User.withUsername("user")
                    .password("{noop}1234")
                    .roles("USER").build();
            return new InMemoryUserDetailsManager(user);
        }
    }

     

     

    SecurityConfig에서 설정했던 user/1111으로 로그인되는 것을 확인할 수 있다

     

     

     

     

     

    Spring Security 로그인 방식 - 폼 인증(Form Login) / HTTP Basic 인증(Basic Authentication)

    1. 폼 인증

    • HTML <form> - </form> -> 아이디/비밀번호 입력 -> POST /login 방식
      • Spring Security는 HttpServletRequest에서 사용자 입력 값을 읽어 옴
    • http.formLogin()으로 활성화
    • 웹 애플리케이션에 적합

     

    폼 인증 흐름

     

     

    @EnableWebSecurity
    @Configuration
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                 .formLogin(form -> form
                         // .loginPage("/loginPage")
                         .loginProcessingUrl("/loginProc")
                         .defaultSuccessUrl("/", true)
                         .failureUrl("/failed")
                         .usernameParameter("userId")
                         .passwordParameter("passwd")
                         .successHandler((request, response, authentication) -> {
                             System.out.println("authentication : " + authentication);
                             response.sendRedirect("/home");
                         })
                         .failureHandler((request, response, exception) -> {
                             System.out.println("exception : " + exception.getMessage());
                             response.sendRedirect("/login");
                         })
                         .permitAll()
                 );
            return http.build();
        }
        ...
    }
    • FormLoginConfigurer 설정 클래스를 통해 API 설정 가능
    • 내부적으로 UsernamePasswordAuthenticationFilter가 생성되어 폼 방식의 인증 처리 담당

     

     

    2. HTTP Basic 인증

    • 브라우저나 클라이언트에서 요청 헤더에 "Authorization: Basic base64(username:password)" 아이디/비밀번호 보내는 방식
    • 별도의 로그인 페이지 없음
    • 브라우저가 자동으로 팝업 띄워줌
    • API 서버, 테스트용 환경에서 사용

     

     

    @EnableWebSecurity
    @Configuration
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                    .httpBasic(Customizer.withDefaults());
            return http.build();
        }
    
        @Bean
        public UserDetailsService userDetailsService() {
            UserDetails user = User.withUsername("user")
                    .password("{noop}1111")
                    .roles("USER").build();
            return new InMemoryUserDetailsManager(user);
        }
    }
    • HttpBasicConfigurer 설정 클래스를 통해 API 설정
    • 내부적으로 BasicAuthenticationFilter가 생성되어 인증 처리 담당

     

     

    • BasicAuthenticationConverter를 사용해서 요청 헤더에 기술된 인증정보의 유효성을 체크하며 Base64 인코딩 된 userName/password 추출
    • Base64 인코딩 된 값은 이코딩 가능하기 대문에 인증정보가 노출된다 > HTTPS와 같이 TLS 기술과 함께 사용

     

     

    @EnableWebSecurity
    @Configuration
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                    .httpBasic(basic -> basic.authenticationEntryPoint(new CustomAuthenticationEntryPoint()));
            return http.build();
        }
    
        @Bean
        public UserDetailsService userDetailsService() {
            UserDetails user = User.withUsername("user")
                    .password("{noop}1111")
                    .roles("USER").build();
            return new InMemoryUserDetailsManager(user);
        }
    }
    public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
            response.setHeader("WWW-Authenticate", "Basic realm=security");
            response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
        }
    }
    • AuthenticationEntryPoint : Spring Security에서 아직 인증되지 않은 사용자가 resource에 접근했을 때 어떻게 처리할지 정의하는 지점
    • ExceptionTranslationFilter가 인증/인가 예외 처리 하는데 로그인이 안 된 경우 AuthenticationEntryPoint 호출
    • 기본적으로 HttpBasicAuthenticationEntryPoint가 동작해서 브라우저에 401 Unauthorized + WWW-Authenticate 헤더 보냄 
      • CustomAuthenticationEntryPoint 등록 > 내가 정의한 commenc() 로직 실행

     

     

     

     

     

    RememberMe 인증

    • 사용자가 웹 사이트나 애플리케이션에 로그인할 대 자동으로 인증 정보를 기억하는 기능
    • UsernamePasswordAuthenticationFilter와 함께 사용되며 AbstractAuthenticationProcessingFilter 슈퍼클래스에서 훅을 통해 구현된다
      • 인증 성공 > RememberMeServices.loginSuccess() > RemeberMe 토큰 생성하고 쿠키로 전달
      • 인증 실패 > RememberMeServices.loginFail() 쿠키 삭제
      • LogoutFilter와 연계해서 로그아웃 시 쿠키 삭제
    • 토큰 생성 : 기본적으로 암호화된 토큰 생성하고 세션에서 이 쿠키를 감지하여 자동 로그인이 이루어지는 방식
      • base64(username + ":" + expirationTime + ":" + algorithmName + ":" + algorithmHex(username + ":" + expirationTime + ":" + password + ":" + key))
    • RemeberMeServices 
      • TokenBasedRememberMeServices - 쿠키 기반 토큰의 보안을 위해 해싱을 사용
      • PersistentTokenBasedRememberMeServices -생성된 토큰을 저장하기 위해 데이터베이스나 다른 영구 저장 매체를 사용

     

     

    @EnableWebSecurity
    @Configuration
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                    .formLogin(Customizer.withDefaults())
                    .rememberMe(rememberMe -> rememberMe
                            // .alwaysRemember(true)
                            .tokenValiditySeconds(3600)
                            .userDetailsService(userDetailsService())
                            .rememberMeParameter("remember")
                            .rememberMeCookieName("rmember")
                            .key("security"));
            return http.build();
        }
    
        @Bean
        public UserDetailsService userDetailsService() {
            UserDetails user = User.withUsername("user")
                    .password("{noop}1111")
                    .roles("USER").build();
            return new InMemoryUserDetailsManager(user);
        }
    }

    • 로그인 후 세션ID 및 쿠키 생성된 것을 확인할 수 있다

     

     

    • 세션ID 삭제 후 다시 URL로 접속하면 기억된 쿠키로 인해 자동로그인 됨을 확인할 수 있다
    • RemeberMeAuthenticationFilter
      • SecurityContextHolder에 Authentication이 포함되지 않은 경우 실행되는 필터
      • 세션이 만료되거나 애플리케이션 종료로 인해 인증 상태가 소멸되는 경우 토큰 기반 인증을 사용해 유효성을 검사하고 자동 로그인 처리를 수행

     

     

     

     

     

    익명 인증 사용자 - anonymous

    • 인증되지 않은 사용자를 null로 두는 대신, ROLE_ANONYMOUS 인증 객체로 취급
    • 코드가 단순해짐
    • 권한 제어를 더 세밀하게 가능
      • 예: 어떤 URL은 익명 사용자만 접근 가능하게 (.anonymous()),
      • 어떤 URL은 로그인한 사용자만 접근 가능하게 (.authenticated()).
    • 세션을 쓰지 않음 > 익명 사용자는 굳이 세션에 저장하지 않음 → 리소스 낭비 방지

     

     

    • Request → SecurityFilterChain → SecurityContextHolder
    • 로그인 x : AnonymousAuthenticationToken (ROLE_ANONYMOUS)
    • 로그인 o : UserDetails (ROLE_USER / ROLE_ADMIN)
    • 인증 시도 실패(비번 틀림 등) → AuthenticationException 발생 → EntryPoint가 처리

     

     

    @RestController
    public class IndexController {
        ...
        
        @GetMapping("/anonymous")
        public String anonymous() {
            return "anonymous";
        }
    
        @GetMapping("/authentication")
        public String authentication(Authentication authentication) {
            if (authentication instanceof AnonymousAuthenticationToken) {
                return "anonymous";
            } else {
                return "not anonymous";
            }
        }
    
        @GetMapping("/anonymousContext")
        public String anonymousContext(@CurrentSecurityContext SecurityContext context) {
            return context.getAuthentication().getName();
        }
    }
    @EnableWebSecurity
    @Configuration
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests(auth -> auth
                            .requestMatchers("/anonymous").hasRole("GUEST")
                            .requestMatchers("/anonymousContext", "/authentication").permitAll()
                            .anyRequest().authenticated())
                    .formLogin(Customizer.withDefaults())
                            .anonymous(annoymous -> annoymous
                                    .principal("guest")
                                    .authorities("ROLE_GUEST")
                            );
            return http.build();
        }
    
        @Bean
        public UserDetailsService userDetailsService() {
            UserDetails user = User.withUsername("user")
                    .password("{noop}1111")
                    .roles("USER").build();
            return new InMemoryUserDetailsManager(user);
        }
    }
    • AnonymousAuthenticationFilter
      • SecurityContextHolder 에 Authentication 객체가 없을 경우 감지하고 필요한 경우 새로운 Authentication 객체로 채운다

     

     

    • authentication의 값이 null이기 때문에 인증 여부 관계없이 "not anonymous"

     

     

    • 익명 요청에서 Authentication 을 얻고 싶다면 @CurrentSecurityContext를 사용
    • CurrentSecurityContextArgumentResolver 에서 요청을 가로채어 처리
    • 인증받지 않았을 때 "guest", 인증받았을 때 "user"

     

     

     

     

     

    로그아웃

    • 기본적으로 DefaultLogoutPageGeneratingFilter 를 통해 로그아웃 페이지를 제공하며 “ GET / logout ” URL 로 접근이 가능
    • CSRF 기능을 비활성화할 경우 혹은 RequestMatcher 를 사용할 경우 GET, PUT, DELETE 모두 가능
    • 로그아웃 필터를 거치지 않고 스프링 MVC 에서 커스텀하게 구현할 수 있으며 로그인 페이지가 커스텀하게 생성될 경우 로그아웃 기능도 커스텀하게 구현해야 한다

     

     

    @RestController
    public class IndexController {
    	...
        
        @GetMapping("/logoutSuccess")
        public String logoutSuccess() {
            return "logoutSuccess";
        }
    }
    @EnableWebSecurity
    @Configuration
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests(auth -> auth
                            .requestMatchers("/logoutSuccess").permitAll()
                            .anyRequest().authenticated())
                    .formLogin(Customizer.withDefaults())
                    .logout(logout -> logout
                            .logoutUrl("/logoutProc")
                            // .logoutRequestMatcher(new AntPathRequestMatcher("/logoutProc"), "POST"))
                            .logoutSuccessUrl("/logoutSuccess")
                            .logoutSuccessHandler(new LogoutSuccessHandler() {
                                @Override
                                public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                                    response.sendRedirect("logoutSuccess");
                                }
                            })
                            .deleteCookies("JSESSIONID", "remember-me")
                            .invalidateHttpSession(true)
                            .clearAuthentication(true)
                            .addLogoutHandler(new LogoutHandler() {
                                @Override
                                public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
                                    HttpSession session = request.getSession();
                                    session.invalidate();
                                    SecurityContextHolder.getContextHolderStrategy().getContext().setAuthentication(null);
                                    SecurityContextHolder.getContextHolderStrategy().clearContext();
                                }
                            })
                            .permitAll()
                    );
            return http.build();
        }
    
        @Bean
        public UserDetailsService userDetailsService() {
            UserDetails user = User.withUsername("user")
                    .password("{noop}1111")
                    .roles("USER").build();
            return new InMemoryUserDetailsManager(user);
        }
    }

     

     

     

    참고) AntPathRequestMatcher

    - Spring Security 6.5 기준 AntPathRequestMatcher 전체 클래스가 deprecated > PathPatternRequestMatcher  사용

    - Spring Security 7에서는 지원 중단

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    	...
        
    	.logoutRequestMatcher(new AntPathRequestMatcher("/logoutProc"), "POST")
        ...
    }

     

     

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        RequestMatcher logoutMatcher =
                PathPatternRequestMatcher.withDefaults()
                        .matcher(HttpMethod.POST, "/logoutProc");
                        
                        ...
                        
                .logout(logout -> logout
                        .logoutUrl("/logoutProc")
                        .logoutRequestMatcher(logoutMatcher)
                        
                        ...
        }

     

     

     

     

     

    요청 캐시 - RequestCache / SavedRequest

     

     

    1. RequestCache ( 요청 저장 관리자 )

    • 리다이렉트 된 후에 이전에 했던 요청 정보를 담고 있는 'SavedRequest’ 객체를 쿠키 혹은 세션에 저장하고 필요시 다시 가져와 실행하는 캐시 메커니즘

     

     

    2. SavedRequest ( 실제 저장된 요청 정보 )

    • 로그인과 같은 인증 절차 후 사용자를 인증 이전의 원래 페이지로 안내하며 이전 요청과 관련된 여러 정보를 저장

     

     

    @RestController
    public class IndexController {
        @GetMapping("/")
        public String index(String customParam) {
            if (customParam != null) {
                return "customPage";
            }
            return "index";
        }
    }
    @EnableWebSecurity
    @Configuration
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
            requestCache.setMatchingRequestParameterName("customParam=y");
    
            http.authorizeHttpRequests(auth -> auth
                            .requestMatchers("/logoutSuccess").permitAll()
                            .anyRequest().authenticated())
                    .formLogin(form -> form
                            .successHandler(new AuthenticationSuccessHandler() {
                                @Override
                                public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                                    SavedRequest savedRequest = requestCache.getRequest(request, response);
                                    String redirectUrl = savedRequest.getRedirectUrl();
                                    response.sendRedirect(redirectUrl);
                                }
                            }))
                    .requestCache(cache -> cache.requestCache(requestCache))
            ;
            return http.build();
        }
    
        @Bean
        public UserDetailsService userDetailsService() {
            UserDetails user = User.withUsername("user")
                    .password("{noop}1111")
                    .roles("USER").build();
            return new InMemoryUserDetailsManager(user);
        }
    }

     

     

     

     

     

    728x90

    'Spring&SpringBoot > SpringSecurity' 카테고리의 다른 글

    SpringSecurity 04  (0) 2025.09.18
    SpringSecurity 03  (0) 2025.09.17
    SpringSecurity 02  (0) 2025.09.16
Designed by Tistory.