
이 시리즈는 Spring Security에 대한 학습을 한 후 기록하는 시리즈입니다. 해당 시리즈는 Spring Security 5.7 버전이므로 현재 6.x 버전과 설정방법은 다르지만 개념적 이해는 같으므로 5.7로 진행했습니다.
Filter들에 대한 학습API의 개념과 사용법, 처리과정, 동작방식API 설정 시 생성 및 초기화 되어 사용자의 요청을 처리하는 FilterForm 방식, Ajax 인증 처리DB와 연동해서 권한 제어 시스템 구현(URL 방식, Method 방식)httpBasic 로그인 방식을 제공한다user, 랜덤 문자열 비밀번호http.formLogin(): Form 로그인 인증 기능이 작동합니다.loginPage("/login.html"): 사용자 정의 로그인 페이지.defaultSuccessUrl("/home"): 로그인 성공 후 이동 페이지.failureUrl("/login.html?error=true"): 로그인 실패 후 이동 페이지.usernameParameter("username"): 아이디 파라미터명 설정.passwordParameter("password"): 패스워드 파라미터명 설정.loginProcessingUrl("/login"): 로그인 Form Action Url.successHandler(loginSuccessHandler()): 로그인 성공 후 핸들러.failureHandler(loginFailureHandler()): 로그인 실패 후 핸들러usernamePasswordAuthenticationFilter를 시작으로 인증처리를 시작합니다AntPathRequestMatcher(/login)에서 요청 정보가 매칭되는지 확인합니다. /login 부분은 loginProcessingUrl 설정에서 변경이 가능합니다chain.doFilter로 다음 filter가 수행되고, 요청 정보가 매칭되면 Authentication으로 넘어가게 됩니다.Authentication에서는 유저가 입력한 username과 password로 Authentication 객체를 생성하고 이를 AuthenticationManager에 인증을 요청합니다AuthenticationManager는 인증을 AuthenticationProvider에 위임하고 여기서 인증에 실패하면 AuthenticationException이 반환됩니다. 추후 이 예외에 대한 후처리를 진행합니다.AuthenticationProvider는 user 객체 정보, authority 권한 정보 등을 담은 Authentication 객체를 생성해서 AuthenticationManager에게 다시 반환합니다AuthenticationManager는 AuthenticationProvider에게 받은 최종적인 인증 객체를 다시 usernamePasswordAuthenticationFilter에게 반환합니다usernamePasswordAuthenticationFilter는 User 객체 정보와 Authorities 정보를 담은 객체로 최종적인 Authenticaton 객체를 만들고 이 객체를 SecurityContext에 저장합니다SecurityContext는 인증 객체를 저장하는 보관소입니다SecurityContext가 Session에 저장되고, 전역적으로 사용할 수 있게 합니다SuccessHandler로 성공 후처리를 하게 됩니다formLogin()을 설정했다면,WebAsyncManagerIntegrationFilterSecurityContextPersistenceFilterHeaderWriterFilterCsrfFilterLogoutFilterUsernamePasswordAuthenticationFilterDefaultLoginPageGeneratingFilterDefaultLogoutPageGeneratingFilterSecurityContextHolderAwareRequestFilterAnonymousAuthenticationFilterSessionManagementFilterExceptionTranslationFilterFilterSecurityInterceptorSpring Security는 세션 무효화시키고, 사용자가 로그인할 때 생성한 인증 객체 토큰을 삭제하고, 인증 객체가 저장되어 있는 Security Context 객체도 삭제합니다. 쿠키가 설정되어 있다면 쿠키 정보 또한 삭제한 후 로그인 페이지로 리다이렉트합니다.http.logout(): 로그아웃 기능이 작동합니다..logoutUrl("/logout"): 로그아웃 처리 URL.logoutSuccessUrl("/login"): 로그아웃 성공 후 이동할 페이지.deleteCookies("JSESSIONID", "remember-me"): 로그아웃 후 쿠키 삭제.addLogoutHandler(logoutHandler()): 로그아웃 핸들러.logoutSuccessHandler(logoutSuccessHandler()): 로그아웃 성공 후 핸들러Spring Security는 원칙적으로 로그아웃은 POST 방식으로 구현합니다.
POST 방식으로 요청을 LogoutFilter가 받습니다.SecurityContext로부터 현재 인증된 인증 객체를 꺼내옵니다.SecurityContextLogoutHandler에게 전달합니다.SecurityContextLogoutHandler가 세션 무효화, 쿠키 삭제, SecurityContextHolder.clearContext()로 해당 SecurityContext를 삭제하고, 인증 객체도 null로 처리합니다.LogoutFilter는 SimpleUrlLogoutSuccessHandler를 호출해서 로그인 페이지로 이동하게 처리합니다.Remember-Me 쿠키에 대한 http 요청을 확인한 후 토큰 기반 인증을 사용해서 유효성을 검사하고, 토큰이 검증되면 사용자가 로그인됩니다.Remember-Me 쿠기 설정)http.rememberMe(): rememberMe 기능이 작동합니다..rememberMeParameter("remember"): rememberMe를 활성화할 때 사용할 파라미터명.tokenValiditySeconds(3600): 토큰 유효기간 기본은 14일.alwaysRemember(true): rememberMe 기능이 활성화되지 않아도 항상 실행userDetailsService(userDetailsService): rememberMe를 인증할 때 사용자 계정을 조회하는 설정RememberMeAuthenticationFilter가 동작하는 첫번째 조건은 Authentication 객체의 값이 null인 경우입니다. 이미 Authentication이 값을 가지고 있다면 인증되어 있다는 뜻이므로 굳이 RememberMeAuthenticationFilter가 동작할 이유가 없는 것이죠RememberMeAuthenticationFilter는 사용자가 세션이 만료되었거나, 사용중 브라우저 종료로 세션이 끊기는 등으로 인증 객체를 SecurityContext안에서 찾지 못하는 경우에 사용자의 인증을 유지하기 위해서 이 필터가 동작해서 인증을 시도합니다.Remember Me기능을 켠 채로 로그인해 Remember me 토큰이 발급된 상태여야 합니다.RememberMeAuthenticationFilter가 동작하게 되면 RememberMeServices가 동작하게 됩니다.RememberMeService는 인터페이스로 구현체는 두 가지가 있습니다. TokenBasedRememberMeServices와 PersistentTokenBasedRememberMeServices입니다.TokenBasedRememberMeServices는 사용자의 요청 토큰과 메모리상에 저장되어 있는 토큰을 비교하는 구현체입니다.PersistentTokenBasedRememberMeServices는 말 그대로 DB에 토큰 내용을 저장해서 사용자의 값과 비교하는 구현체입니다.RememberMeServices가 토큰을 추출해서 사용자가 가지고 있는 토큰이 Remember me라는 이름을 가진 토큰인지 확인합니다. 해당 토큰이 없다면 다음 필터로 넘어갑니다.Decode Token에서 해당 토큰이 규칙을 지키고 있는 토큰인지 확인합니다.(정상 유무 판단)Authentication을 생성해서 AuthenticationManager에게 전달해서 인증을 처리합니다.AnonymousAuthenticationFilter는 인증을 받지 않은 사용자를 null로 처리하는 것이 아닌 익명사용자로 처리하는 것입니다.AnonymousAuthenticationFilter는 사용자의 Authentication이 존재하는지 확인합니다.(SecurityContext에서 확인)null이 아닌 AnonymousAuthenticationToken을 생성합니다.SecurityContextHolder안의 SecurityContext에 해당 토큰을 저장합니다.http.sessionManagement(): 세션 관리 기능이 작동합니다..maximumSessions(1): 최대 허용 가능 세션 수, -1을 설정하면 무제한 로그인 세션 허용.maxSessionsPreventsLogin(true): true로 설정하면 동시 로그인을 차단합니다.(현재 사용자 인증을 실패시키는 방법), false라면 기존 세션을 만료시킵니다..invalidSessionUrl("/invalid"): 세션이 유효하지 않을 때 이동할 페이지.expiredUrl("/expired"): 세션이 만료된 경우 이동할 페이지
.invalidSessionUrl("/invalid")와.expiredUrl("/expired")를 동시에 설정한다면 invalidSessionUrl 설정이 우선시되어 해당 URL로 이동합니다
세션 고정 공격이라고 합니다.Spring Security는 해당 공격을 방지하기 위해서 보호 기능을 제공합니다. http.sessionManagement().sessionFixation().changeSessionId(): 기본값으로 사용자가 인증을 시도하면 세션은 그대로 유지한채 세션 아이디만 변경합니다.migrateSession(): 세션과 세션 아이디 모두 새로 생성합니다.newSession(): 세션과 세션 아이디 모두 새로 생성하지만 이전 세션의 옵션을 사용할 수 없습니다.none(): 세션과 세션 아이디 모두 그대로 두는 설정이므로 세션 고정 공격에 노출될 위험이 있습니다..sessionCreationPolicy(4가지 정책 설정이 가능합니다.)SessionCreationPolicy.Always: Spring Security가 항상 세션 생성SessionCreationPolicy.If_Requried: Spring Security가 필요시 생성(기본값)SessionCreationPolicy.Never: Spring Security가 생성하지 않지만 이미 존재하면 사용SessionCreationPolicy.Stateless: Spring Security가 생성하지 않고 존재해도 사용하지 않음, 세션 자체를 사용하지 않으므로, JWT 토큰 방식을 사용할 때 사용session.isExpired() == true
SessionManagementFilter에서 확인했을 때, 이미 세션 최대 허용 개수가 가득 차 있다면 세션을 만료시키고 이 후 ConcurrentSessionFilter에서 해당 내용을 확인해서 만료상태라면 로그아웃처리와 오류 페이지 응답을 처리합니다.
user1이 로그인을 시도하면 ConcurrentSessionControlAuthenticationStrategy에서 현재 사용자의 세션 개수를 확인합니다.user2가 인증을 시도하면 똑같이 ConcurrentSessionControlAuthenticationStrategy에서 세션 개수를 확인하고 이 때는 이미 최대 허용 개수인 1개가 생성중이므로 두 전략 중 선택한 전략대로 움직입니다.user2의 인증을 똑같이 세션 고정 보호처리를 한 후에 세션 정보에 등록하고, user1의 세션을 만료시킵니다.user1이 서버에 자원을 요청하면 ConcurrentSessionFilter는 만료된 것을 확인하고, 바로 로그아웃 처리하고 오류 페이지를 응답합니다.Spring Security에서 하는 권한 설정은 두가지 방식으로 할 수 있습니다
http.antMatchers("/users/**").hasRole("USER")@PreAuthorize("hasRole('USER')")
public void user() {
System.out.println("user")
}
주의 사항
설정 시에 구체적인 경로가 먼저 오고 그것보다 큰 범위의 경로가 뒤에 오도록 설정해야 합니다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/shop/**") // 인가를 하려는 특정 경로, 설정하지 않으면 모든 경로가 해당됩니다.
.authorizeRequests()
.antMatchers("/shop/login", "/shop/users/**").permiAll() // 해당하는 경로의 요청은 허용하겠다는 설정입니다.
.antMatchers("/shop/mypage").hasRole("USER") // 이 경로는 USER 권한을 가져야 합니다.
.antMatchers("/shop/admin/pay").access("hasRole('ADMIN')")
.antMatchers("/shop/admin/**").access("hasRole('ADMIN') or hasRole('SYS')")
.anyRequest().authenticated()
}

AuthenticationEntryPoint 호출RequestCache: 사용자의 이전 요청 정보를 세션에 저장하고 이를 꺼내 오는 캐시 메카니즘SavedRequest: 사용자가 요청했던 request 파라미터 값들, 그 당시의 헤더값들 등이 저장AccessDeniedHandler에서 예외 처리http.exceptionHandling(): 예외 처리 기능이 동작합니다..authenticationEntryPoint(authenticationEntryPoint()): 인증 실패 시 처리.accessDeniedHandler(accessDeniedHandler()): 인가 실패 시 처리
<input type="hidden" name="${csrf.parameterName}" value="${_csrf.token}">HTTP 메소드: PATCH, POST, PUT, DELETEhttp.csrf(): 기본 활성화되어 있습니다.http.csrf().disabled(): 비활성화