인프런 정수원 지식공유자님의 스프링 시큐리티 강의를 수강하고 공부한 내용을 정리한 글입니다.
📌 인증 API
Spring Security는 프로젝트에 의존성(dependency)를 추가하여 사용하는 프레임워크로 다양한 인증 API를 제공한다.
<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
의존성을 추가하면 아래와 같은 일이 이루어진다.
서버 기동
👉🏻 Spring Security 초기화
👉🏻 웹 보안 기능 활성화
👉🏻 보안 기능이 시스템에 반영되어 작동
📌 사용자 정의 보안 기능
WebSecurityConfigurerAdapter를 상속받은 사용자 정의 보안 설정 클래스(SecurityConfig)를 작성하여 HttpSecurity가 제공하는 API를 사용하여 세부적인 보안 기능을 설정할 수 있다.

WebSecurityConfigureAdapter
🫧 기본적인 보안 기능 활성화
🫧 시스템이 보안 기능 작동하도록 설정
🫧 초기화 작업
🫧 HttpSecurity 생성
📌 Form Login 인증
사용자가 인증이 필요한 자원에 접근하려고 할 때, 로그인 페이지로 유도하여 사용자가 인증을 요청(POST form data : username + password)하여
🙂 인증에 성공하면 SESSION 및 인증 토큰을 생성 / 저장하고, 세션에 저장된 인증 토큰으로 접근이 가능하도록 한다.
😞 인증에 실패하면 로그인 페이지로 redirect 한다.
http.formLogin() : Form 로그인 인증 기능이 작동
http.formLogin()
.loginPage("/login.html") // 사용자 정의 로그인 페이지 경로
.defaultSuccessUrl("/home") // 로그인 성공 후 이동 페이지 경로
.failureUrl("/login.html?error=true") // 로그인 실패 후 이동 페이지 경로
.usernameParameter("username") // 아이디 파라미터명 설정 -> form 태그 명
.passwordParameter("password") // 패스워드 파라미터명 설정 -> ui 화면 값과
.loginProcessingUrl("/login") // 로그인 Form Action Url -> 동일하게 해야 함
.successHandler(loginSuccessHandler()) // 로그인 성공 후 핸들러 -> 로그인 성공 이후 작업 처리
.failureHandler(loginFailureHandler()) // 로그인 실패 후 핸들러 -> 로그인 실패 이후 작업 처리
📌 Form Login 인증 필터 : UsernamePasswordAuthenticationFilter
Form Login 요청 시 Filter를 통해 인증 과정을 거친다.

UsernamePasswordAuthenticationFilter
🫧 인증 처리 관련 요청을 담당
AntPathRequestMatcher(/login)
🫧 요청 정보의 url이 login으로 시작되는지 검사 (default는 login이지만 사용자가 설정할 수 있다)
🫧 인증 정보가 일치하지 않으면 다음 필터로 이동(chain.doFilter)
🫧 인증 정보가 일치하면 Authentication 객체 생성
Authentication(Username + Password)
🫧 사용자가 로그인 할 때 입력한 Username + Password를 저장하여 실질적 인증처리를 맡기는 역할
AuthenticationManager
🫧 Filter로부터 처음 인증 객체를 전달받고 인증 처리
🫧 AuthenticationProvider 객체 중 하나를 선택하여 인증 처리 위임
🫧 Provider로 받은 최종적인 인증 객체를 다시 Filter로 return
AuthenticationProvider
🫧 실질적 인증처리 담당
🫧 인증 성공 / 실패 결과를 반환
🫧 인증에 성공하면 Authentication 객체를 생성, 인증에 성공한 정보를 객체에 저장한 후, AuthenticationManager로 return
🫧 인증에 실패하면 AuthenticationException 예외 발생
AuthenticationException
🫧 인증 예외 발생 시 최종적으로 인증 실패 후 후속 작업 처리
Authentication(User + Authorities)
🫧 인증에 성공하여 받은 객체에는 인증 결과 User 객체와 권한 정보(Authorities)를 return 받는다
🫧 SecurityContext(= 인증 객체를 저장, 보관하는 저장소) 객체에 저장
SecurityContext
🫧 인증 객체를 저장, 보관하는 저장소, 보관소
🫧 session에도 저장하여 전형적으로 사용자가 객체를 참조할 수 있도록 처리하는 작업이 가능
SuccessHandler
🫧 인증 성공 이후의 작업을 처리
📌 Logout 처리
사용자가 Logout을 요청하면 세션 무효화, 인증 토큰 삭제(SecurityContext 객체도 함께 삭제), 쿠키 정보 삭제 과정을 거치고 로그인 페이지로 redirect한다.
http.logout() : 로그아웃 기능이 작동
protected void configure(HttpSecurity http) throws Exception {
http.logout() // 로그아웃 처리
.logoutUrl("/logout") // 로그아웃 처리 URL
.logoutSuccessUrl("/login") // 로그아웃 성공 후 이동페이지
.deleteCookies("JSESSIONID", "remember-me") // 로그아웃 후 쿠키 삭제
.addLogoutHandler(logoutHandler()) // 로그아웃 핸들러
.logoutSuccessHandler(logoutSuccessHandler()) // 로그아웃 성공 후 핸들러
}
📌 LogoutFilter

LogoutFilter
🫧 사용자로부터 로그아웃 요청을 받아서 로그아웃 처리
AntPathRequestMatcher(/logout)
🫧 맞는 정보로 로그아웃을 요청하고 있는지 검사
🫧 일치하지 않으면 다음 필터로 이동하고 로그아웃 처리하지 않음
Authentication
🫧 SecurityContext로부터 인증 객체를 꺼내고 SecurityContextLogoutHandler에게 전달
SecurityContextLogoutHandler
🫧 css는 무효화, 쿠키, SecurityContext 객체 삭제
🫧 인증 객체 null로 초기화
SimpleUrlLogoutHandler
🫧 SecurityContextLogoutHandler가 성공적으로 종료되면 LogoutFilter가 SimpleUrlLogoutHandler를 호출
🫧 로그인 페이지로 이동하도록 처리
📌 Remember Me 인증
설정 클래스에서 Remember Me를 활성화하면 Spring Security가 Rememeber Me 인증 기능을 제공한다.
Remember Me 인증 기능
1. 세션이 만료되고 웹 브라우저가 종료된 후에도 어플리케이션이 사용자를 기억하는 기능
2. Remember-Me 쿠키에 대한 Http 요청을 확인한 후 토큰 기반 인증을 사용해 유효성을 검사하고 토큰이 검증되는 사용자는 로그인
3. 사용자 라이프 사이클
⠀⠀🫧 인증 성공 (Rememeber-Me 쿠키 설정)
⠀⠀🫧 인증 실패 (쿠키가 존재하면 쿠키 무효화)
⠀⠀🫧 로그아웃 (쿠키가 존재하면 쿠키 무효화
http.rememberMe() : rememberMe 기능이 작동
protected void configure(HttpSecurity http) throws Exception {
http.rememberMe()
.rememberMeParameter("remember") // default는 remember-me -> 사용자 화면 파라미터 명과 일치해야 함
.tokenValiditySeconds(3600). // 쿠키 만료 시간, default는 14일
.alwaysRemember(true) // 리멤버 미 기능이 활성화되지 않아도 항상 실행, default는 false
.userDetailsService(userDetailService) // 사용자 계정을 조회하는 처리 과정에 필요한 클래스, 리멤버 미 인증할 때 필요한 설정, 반드시 해줘야 함
}
📌 Remember Me 인증 필터 : RememberMeAuthenticationFilter

RememberMeAuthenticationFilter
🫧 사용자 요청을 받아서 그 요청을 처리하는 조건이 있다.
1. Authentication 인증 객체가 null일 경우 (인증 받지 않은 사용자)
2. 사용자가 최초의 Form을 받을 당시 RememberMe를 활성화하여 RememberMe 쿠키를 발급받은 경우
에 RememberMeAuthenticationFilter가 동작하여, 사용자 인증을 다시 받게 만들어 인증을 유지해 사용자가 계속 서버에 접속할 수 있도록 한다.
RememberMeService
🫧 실제로 RememberMe 인증 처리하는 TokenBasedRememberMeServices, PersistentTokenBasedRememberMeService를 구현체로 가진다
🫧 Token Cookie를 추출하고
🫧 토큰이 RememberMe 이름을 가진 토큰인지 검사
🫧 토큰이 존재하면 다음으로 진행, 존재하지 않으면 다음 필터로 이동
🫧 토큰이 존재하면 Decode Token (토큰의 포맷이 정상적으로 규칙을 지킨 토큰인지 판단)
🫧 정상 토큰이면 사용자가 요청한 토큰의 값과 서버에 저장된 토큰의 값이 일치하는지 확인
🫧 토큰이 일치하면 토큰의 유저 정보를 DB에서 조회하고, 해당 쿠키에 포함된 유저 정보가 DB에 존재하는지 확인
🫧 유저 정보가 DB에 존재하면, 새로운 Authentication 인증 객체(RememberMeAuthenticationToken)를 생성하고
🫧 AuthenticationManager 인증 관리자에게 전달하여 실질적 인증 처리를 위임한다
TokenBasedRememberMeServices
🫧 메모리에 저장한 토큰과 사용자가 요청한 쿠키 토큰과 비교하여 인증 처리
🫧 토큰 유효기간은 일반적으로 14일
PersistentTokenBasedRememberMeServices
🫧 DB에 서비스 발급한 토큰을 저장, 사용자가 요청하는 클라이언트 값과 DB값을 비교하여 인증 처리
🫧 영구적
📌 익명사용자 인증 필터 : AnonymousAuthenticationFilter
사용자가 자원에 접근을 시도하면 session에 저장된 User 객체가 존재하는지 판단하고, null이라면 인증 받지 않은 사용자라고 판단하고 자원으로 접근하지 못 하게 한다.

AnonymousAuthenticationFilter
🫧 사용자가 request를 요청하면, 요청한 사용자의 Authentication 인증 객체 존재 여부를 판단
🫧 인증 객체가 존재하면(인증 받은 사용자) 다음 필터로 이동
🫧 인증 객체가 존재하지 않으면(익명 사용자) 익명 사용자용 인증 객체(AnonymousAuthenticationToken) 생성(null이 아니다)하여 SecurityContext 객체 안에 익명 사용자용 토큰 객체 저장 (=인증받지 않은 사용자를 null로 처리하는 것이 아니라 별도의 익명 사용자용 인증 객체를 생성)
🫧 익명 사용자 인증 처리 필터
🫧 익명 사용자와 인증 사용자를 구분해서 처리하기 위한 용도로 사용
🫧 화면에서 인증 여부를 구현할 때 isAnonymous()와 isAuthenticated()로 구분해서 사용
🫧 인증 객체를 세션에 저장하지 않는다
📌 동시 세션 제어
동일한 계정으로 인증을 받을 때 생성되는 세션의 허용 개수를 초과한 경우, 어떻게 계속적으로 초과하지 않고 세션을 유지하는지에 대한 제어이다.

Spring Security는 2가지 방법으로 제어한다.
1. 이전 사용자 세션 만료
2. 현재 사용자 인증 실패
http.sessionManagement() : 세션 관리 기능이 작동
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.maximunSessions(1) // 최대 허용 가능 세션 수, -1 : 무제한 로그인 세션 허용
.maxSessionPreventsLogin(true) // 동시 로그인 차단 -> 세션을 생성하지 못 하도록 함 (현재 사용자 인증 실패 방법), false : 기존 세션 만료 (default) (이전 사용자 세션 만료 방법)
.invalidSessionUrl("/invalid") // 세션이 유효하지 않을 때 이동할 페이지
.expiredUrl("/expired") // 세션이 만료될 경우 이동 할 페이지 (invalid와 expired 모두 설정하였다면 invalid가 우선시 됨)
}
📌 세션 고정 보호

🗡 세션 고정 공격 : 사용자와 공격자가 세션을 공유하여, 공격자는 인증을 하지 않고 세션만으로 자원에 접근이 가능
🛡 세션 고정 보호 : 인증이 성공할 때마다 새로운 Session을 생성하고 Sessionid를 발급하여 세션 고정 공격에 대비한다.
http.sessionManageMent() : 세션 관리 기능이 작동
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionFixation().changeSessionID() // 기본값 ↔ none, migrateSession, newSession
// changeSessionID 사용자가 인증에 성공하면 세션은 그대로, 세션 아이디만 변경 (서블릿 3.1 이상에서 기본값으로 설정)
// migrateSession 새로운 세션도 생성, 세션 아이디로 새로 발금 (서블릿 3.1 이하에서 기본값으로 설정)
// → 세션에 설정한 여러 속성값을 그대로 사용할 수 있도록 처리
// newSession 세션과 세션 아이디가 새롭게 발급되지만, 이전 세션에서 설정한 속성값들을 사용할 수 없음
// none 세션이 새롭게 생성되지 않음 → 세션 고정 공격에 취약
}
📌 세션 정책
http.sessionManageMent().sessionCreationPolicy()
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.If_Required)
}
SessionCreationPolicy.Always // 스프링 시큐리티가 항상 세션 생성
SessionCreationPolicy.If_Required // 스프링 시큐리티가 필요 시 세션 생성 (기본값)
SessionCreationPolicy.Never. // 스프링 시큐리티가 세션을 생성하지 않지만 이미 존재하면 사용
SessionCreationPolicy.Stateless. // 스프링 시큐리티가 세션을 생성하지 않고 존재해도 사용하지 않음 → 세션을 사용하지 않는 인증 방식을 투입할 경우 사용 (ex. JWT = JSON WEB TOKEN)
📌 세션 제어 필터 : SessionManagementFilter, ConcurrentSessionFilter
☕️ SessionManagementFilter
⠀⠀🥄 세션 관리 : 인증 시 사용자의 세션 정보를 등록, 조회, 삭제 등의 세션 이력 관리
⠀⠀🥄 동시적 세션 제어 : 동일 계정으로 접속이 허용되는 최대 세션 수를 제한
⠀⠀🥄 세션 고정 보호 : 인증 할 때마다 세션쿠키를 새로 발급하여 공격자의 쿠키 조작을 방지
⠀⠀🥄 세션 생성 정책 : Always, If_Requried, Never, Stateless
☕️ ConcurrentSessionFilter
⠀⠀🥄 매 요청 마다 현재 사용자의 세션 만료 여부 체크
⠀⠀🥄 세션이 만료되었을 경우 즉시 만료 처리
⠀⠀🥄 Session.isExpired() == true : 로그아웃 처리, 즉시 오류 페이지 응답

사용자 로그인 시도
👉🏻 이전에 동일한 계정으로 인증을 시도하여 세션이 생성된 상태라면, 세션 새로 생성한다
👉🏻 최대 세션 허용 개수가 초과되었을 경우 이전 사용자의 세션을 만료(session.expiredNow())
👉🏻 만약 이때 이전 사용자가 서버에 접속한다고 가정하면, ConcurrentSessionFilter는 매 요청마다 session 만료 여부를 체크하기 때문에 이전 사용자의 만료 여부를 확인한다
👉🏻 SessionManagementFilter 안에서 이전 사용자의 세션 만료여부를 확인
👉🏻 세션이 만료되도록 설정되어 즉시 이전 사용자의 Session을 만료, 로그아웃 하고 오류 페이지를 응답한다
위처럼 Session 제어 처리는 SessionManagementFilter와 ConcurrentSessionFilter가 연계해서 동시적으로 처리한다.

1️⃣ user1 로그인 시도
👉🏻 UsernamePasswordAuthenticationFilter가 인증 처리
👉🏻 ConcurrentSessionControlAuthenticationStrategy 클래스 호출
⠀⠀🫧 동시적 세션 제어 처리
⠀⠀🫧인증을 시도하는 사용자 계정으로 생성된 세션 개수 카운팅
👉🏻 ChangeSessionIdAuthenticationStrategy 클래스 호출
⠀⠀🫧 세션 고정 보호 처리
⠀⠀🫧 새롭게 세션을 발급
👉🏻 RegisterSessionAuthenticationStrategy
⠀⠀🫧 사용자 세션 등록, 저장 = 세션 카운트 증가
2️⃣ user2 로그인 시도
👉🏻 UsernamePasswordAuthenticationFilter가 인증 처리
👉🏻 ConcurrentSessionControlAuthenticationStrategy 클래스 호출
⠀⠀🫧 인증을 시도하는 사용자 계정(=user1)으로 생성된 세션 카운트가 최대 허용 세션 개수와 동일하면
⠀⠀🫧 인증 실패 전략
⠀⠀⠀⠀🥚 SessionAuthenticationException(인증 예외) 발생
⠀⠀⠀⠀🥚 세션이 생성되지 못 하고 더 이상의 인증 처리를 막음
⠀⠀⠀⠀🥚 인증 실패
⠀⠀🫧 세션 만료 전략
⠀⠀⠀⠀🥚 session.expireNow()를 통해 user1의 세션을 만료, user2의 인증 성공
👉🏻 ChangeSessionIdAuthenticationStrategy 호출하여 세션 고정 보호 실행
👉🏻 RegisterSessionAuthenticationStrategy 호출하여 세션 정보 등록 (세션 카운드 = 2)
3️⃣ user1 자원 접근 시도
👉🏻 ConcurrentSessionFilter가 user1의 세션 만료 여부를 session.expiredNow()에서 확인
⠀⠀🫧 session.isExpired() == true
⠀⠀🫧 즉시 user1의 세션 만료하도록 logout 처리
⠀⠀🫧 오류 페이지 응답
📌 권한설정과 표현식
🗣 권한설정
💬 선언적 방식
// URL
http.andMatchers("/users/**").hasRole("USER")
// Method
@PreAuthorize("hasRole('USER')")
public void user() {
System.out.println("user")
}
💬 동적 방식 - DB 연동 프로그래밍
// URL
// Method
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.andMatcher("/shop/**") // 설정된 경로로 요청할 때만 설정 클래스의 보안 기능 작동
// = 특정한 Url 정보로 요청을 받아 인증, 인가 처리
// 경로 설정을 생략하면 모든 요청에 대해 보안 검사
.authorizeRequests()
.antMatchers("/shop/login", "/shop/users/**").permitAll() // 리퀘스트와 설정한 경로 정보(login, users)와 매치해서 동일한 정보가 포함되면 권한 정보를 통해 인가 허용
.antMatchers("/shop/mypage").hasRole("USER") // mapage요청과 매치된 요청은 인가 심사를 하는데 요청하는 사용자는 User 권한을 가져야 접근 가능
.antMatchers("/shop/admin/pay").access("hasRole('ADMIN')"); // 상세한 권한 설정이 가능함
.antMatchers("/shop/admin/**").access("hasRole('ADMIN') or hasRole('SYS')");
.antRequest().authenticated() // 모든 요청에는 인증을 받은 사용자만이 접근이 가능하도록 설정
}
// 여기에 해당하는 모든 인가 심사를 통과해야 자원에 접근 가능. 하나라도 권한이 없으면 최종적으로 해당 자원에 접근이 불가능
// !! 설정 시 구체적인 경로가 먼저 오고 그것보다 큰 범위의 경로가 뒤에 오도록 해야 한다 !!
🗣 표현식
| 메소드 | 동작 |
|---|---|
| authenticated() | 인증된 사용자의 접근을 허용 |
| fullyAuthenticated() | 인증된 사용자의 접근을 허용, rememberMe 인증 제외 |
| permitAll() | 무조건 접근을 허용 (role과 anonymous 모두 접근 가능) |
| denyAll() | 무조건 접근을 허용하지 않음 |
| anonymous() | 익명 사용자의 접근을 허용 (role 사용자는 접근 불가) |
| rememberMe() | rememberMe를 통해 인증된 사용자의 접근을 허용 |
| access(String) | 주어진 sql 표현식의 평가 결과가 true면 접근을 허용 |
| hasRole(String) | 사용자가 주어진 역할이 있다면 접근을 허용 |
| hasAuthority(String) | 사용자가 주어진 권한이 있다면 접근을 허용 |
| hasAnyRole(String...) | 사용자가 주어진 역할 중 어떤 것이라도 있다면 접근을 허용 |
| hasAnyAuthority(String...) | 사용자가 주어진 권한 중 어떤 것이라도 있다면 접근을 허용 |
| hasIpAddress(String) | 주어진 IP로부터 요청이 왔다면 접근을 허용 |
위 표현식을 사용하여 SecurityConfig 내에서 사용자를 생성할 수도 있지만 운영 서비스에 추천하는 방식은 아니다.
// SecurityConfig.java
// 사용자 생성
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// AuthenticationManagerBuilder 클래스 → 사용자를 생성하고 권한 설정 기능 제공
// 해당 메소드를 override하여 사용자 생성
auth.inMemoryAuthentication().withUser("user").password("{noop}1111").roles("USER");
// {noop} → password의 prefix → password 암호화 할 때의 특정한 password 유형을 prefix로 작성해주어야 함
// {noop}은 평문으로 암호화가 이루어지도록 하는 prefix
auth.inMemoryAuthentication().withUser("sys").password("{noop}1111").roles("SYS");
auth.inMemoryAuthentication().withUser("admin").password("{noop}1111").roles("ADMIN");
}
// 권한 설정
@Override
protected void configure(HttpSecurity http) throws Exception {
// 운영 서비스에서 추천하는 방식이 아님
// 운영 서비스에서는 자원에 해당하는 권한 설정들을 그때마다 실시간적으로 즉각 설정하고 서비스되어야 함
http
.authorizeRequests()
.antMatchers("/user").hasRole("USER") // 사용자가 /user 요청을 하면 사용자의 권한과 설정된 권한이 일치하는지 확인 = 사용자가 설정된 url로 자원을 요청하면 권한 심
.antMatchers("admin/pay").hasRole("ADMIN") // 구체적인 범위가 상위에 작성되어야
.antMatchers("admin/**").access("hasRole('ADMIN') or hasRole('SYS')") // ADMIN 권한이 USER나 SYS 권한에 접근할 수 x, 이후 권한 계층을 통해 설정해야 함
.anyRequest().authenticated();
http
.formLogin();
}
📌 예외 처리 및 요청 캐시 필터 : ExceptionTranslationFilter, RequestCacheAwareFilter
☕️ExceptionTranslationFilter
⠀⠀🥄 크게 두 가지 종류의 예외(AuthenticationException = 인증 예외, AccessDeniedException = 인가 예외)를 처리
⠀⠀🥄 해당 예외를 발생시키는 필터 = FilterSecurityIntercept
⠀⠀⠀⠀🫧 Spring Security가 관리하는 보안 필터 중 맨 마지막에 위치
⠀⠀⠀⠀🫧 해당 필터 앞에 위치하는 필터가 ExceptionTranslationFilter
⠀⠀⠀⠀🫧 ExceptionTranslationFilter가 try-catch로 감싸서 요청을 전달FilterSecurityIntercept로 전달
⠀⠀⠀⠀🫧 FilterSecurityIntercept는 인증 예외와 인과 예외를 ExceptionTranslationFilter로 through
⠀⠀⠀⠀🫧 ExceptionTranslationFilter는 인증 예외와 인과 예외를 받아서 처리
⠀⠀🥄 AuthenticationException : 인증 예외 처리
⠀⠀⠀⠀🫧 AuthenticationEntryPoint 호출
⠀⠀⠀⠀⠀⠀로그인 페이지 이동, 401 오류 코드 전달 등
⠀⠀⠀⠀⠀⠀사용자가 다시 인증을 시도할 수 있도록 함
⠀⠀⠀⠀🫧 인증 예외가 발생하기 전의 요청 정보를 저장
⠀⠀⠀⠀⠀⠀사용자의 경로를 저장 (로그인한 사용자만 접근할 수 있는 자원에 사용자가 접근을 시도했을 때, 로그인 페이지로 이동해 인증을 유도하고, 로그인에 성공하면 로그인 페이지로 이동하기 전 페이지로 이동) = 캐싱
⠀⠀⠀⠀⠀⠀RequestCache : 사용자의 이전 요청 정보를 세션에 저장하고 이를 꺼내오는 메커니즘
⠀⠀⠀⠀⠀⠀SavedRequest : 사용자가 요청했던 Request 파라미터 값들, 그 당시의 헤더값들 등이 저장
⠀⠀🥄 AccessDeniedException : 인가 예외 처리
⠀⠀⠀⠀🫧 AccessDeniedHandler에서 예외 처리하도록 제공
⠀⠀⠀⠀사용자가 권한이 없어 자원에 접근이 불가능하다는 메시지를 전달
⠀⠀⠀⠀Handler 안에서 후속 처리

사용자가 인증하지 않고 user 자원에 접근한다고 가정했을 때
FilterSecurityInterceptor
🫧 맨 마지막에 위치한 인과처리를 수행하는 필터
🫧 권한 설정에 따라 인과 예외 발생 (익명 사용자가 접근하는 것이므로 인증 예외는 발생하지 않음)
🫧 AccessDeniedException 호출
AccessDeniedException
🫧 익명 사용자 / rememberMe 사용자일 경우에는 AccessDeniedHandler를 호출하지 않고 AuthenticationException로 이동
AuthenticationException
🫧 SecurityContext를 null로 처리하고 AuthenticationEntryPoint 구현체 호출
AuthenticationEntryPoint
🫧 구현체 안에서 로그인 페이지로 이동 (인증 예외가 발생해 로그인하도록 유도)
HttpSessionRequestCache
🫧 예외가 발생하지 이전에 DefaultSaveRequest 객체에 사용자의 url 정보 저장
🫧 객체는 session에 저장
🫧 세션에 저장하는 역할은 HttpSessionRequestCache가 담당
인증을 받아서 자원에 접근하려는데 자원 접근 권한이 없다고 가정했을 때
FilterSecurityInterceptor
🫧 인가 예외 발생
ExceptionTranslationFilter
🫧 AccessDeniedException 호출
AccessDeniedException
🫧 AccessDeniedHandler 호출해서 후속 작업 처리
🫧 denied 페이지 호출 (자원에 접근 권한이 없음을 알리는 페이지)
http.exceptionHandling() : 예외 처리 기능이 작동함
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint()) // 인증실패 시 처리 (개발자가 직접 클래스 구현, 해당 클래스를 호출)
.accessDeniedHandler(accessDeniedHandler()) // 인증실패 시 처리 (인터페이스를 구현, 해당 인터페이스를 호출)
}