-
SpringSecurity 01Spring&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 - 인증 API : 사용자나 시스템이 자신을 증명하는 과정