이 시리즈는 Spring Security에 대한 학습을 한 후 기록하는 시리즈입니다. 해당 시리즈는 Spring Security 5.7 버전이므로 현재 6.x 버전과 설정방법은 다르지만 개념적 이해는 같으므로 5.7로 진행했습니다.
Filter
들에 대한 학습API
의 개념과 사용법, 처리과정, 동작방식API
설정 시 생성 및 초기화 되어 사용자의 요청을 처리하는 Filter
Form
방식, 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()
을 설정했다면,WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
UsernamePasswordAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
Spring 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()
: 비활성화