BasicAuthenticationFilter
• 이 필터는 기본 인증 서비스를 제공하는 데 사용된다
• BasicAuthenticationConverter 를 사용해서 요청 헤더에 기술된 인증정보의 유효성을 체크하며
Base64 인코딩된 username 과 password 를 추출한다
[유효성 체크 ==> Authentication이라는 헤더가 있는지, Basic이라는 인증기술이 적혀있는지 확인하는 절차 ]
• 인증 이후 세션을 사용하는 경우와 사용하지 않는 경우에 따라 처리되는 흐름에 차이가 있다.
세션을 사용하는 경우 매 요청 마다 인증과정을 거치지 않으나
세션을 사용하지 않는 경우 매 요청마다 인증과정을 거쳐야 한다[ex) jwt같은]
BasicAuthenticationFilter를 통해 유효성 체크를 진행하고, 성공하면 Base64 인코딩된 username 과 password 를 추출해서 인증을 위해 UsernamePasswordAuthenticationToken에 Username,Password를 담아준다.
그리고 AuthenticationManager에 UsernamePasswordAuthenticationToken를 전달한다.
그리고 AuthenticationManager가 인증을 시도한다.
요청 컨텍스트에 SecurityContext가 저장된다 => 하나의 HTTP 요청이 처리되는 동안 유지되는 실행 환경을 의미한다.
Spring Security는 SecurityContextHolder를 사용하여 SecurityContext를 저장하고 관리하고
SecurityContextHolder는 기본적으로 ThreadLocal 전략을 사용한다.
ThreadLocal은 각 스레드마다 독립적인 변수를 가질 수 있게 해주는 Java의 기능이다.
웹 애플리케이션에서 각 요청은 보통 별도의 스레드에서 처리된다.
SecurityContextHolder는 전역적으로 하나만 존재하고
SecurityContextHolder는 ThreadLocal을 사용하여 각 스레드(즉, 각 HTTP 요청)에 대해
독립적인 SecurityContext를 관리한다.
각 HTTP 요청은 자신만의 SecurityContext를 가지며,
SecurityContext는 전역적인 SecurityContextHolder를 통해 ThreadLocal에 저장되고 접근된다.
따라서 해당 HTTP 요청 처리 중 어느 시점에서든 SecurityContextHolder를 통해
현재 요청에 대한 인증 정보에 접근할 수 있다.
[SecurityContextHolder는 정적 메서드를 통해 현재 스레드의 SecurityContext에 접근할 수 있게 해주는
유틸리티 클래스이다.
실제로 저장되는 것은 SecurityContext이며, 이는 Authentication 객체를 포함한다.
이러한 구조 덕분에 멀티스레드 환경에서도 각 요청의 보안 컨텍스트가 안전하게 격리되어 관리된다.]
form인증은 http request동안 인증이 살아있는것이 아닌 세션범위동안 살아있다.
즉 세션이 유지되고있다면 인증이 살아있다.
form 인증같은 세션기반 인증
1. 세션 기반 인증:
- Form 인증은 일반적으로 세션 기반 인증 방식을 사용합니다.
- 사용자가 성공적으로 로그인하면, 서버는 세션을 생성하고 이 세션에 인증 정보(SecurityContext)를 저장합니다.
2. 세션 유지 기간:
- 인증 상태는 세션이 유효한 동안 유지됩니다.
- 세션은 여러 HTTP 요청에 걸쳐 유지됩니다.
3. SecurityContext 저장 및 복원:
- SecurityContextPersistenceFilter가 각 요청의 시작에 세션에서 SecurityContext를 로드하고, 요청 종료 시 다시 세션에 저장합니다.
- 이로 인해 사용자는 한 번 로그인하면 세션이 유효한 동안 계속 인증된 상태를 유지할 수 있습니다.
4. 세션 만료:
- 세션이 만료되면 인증 정보도 함께 사라집니다.
- 세션 만료는 설정된 시간이 지나거나, 서버가 재시작되거나, 사용자가 로그아웃할 때 발생할 수 있습니다.
5. 요청 간 인증 상태 유지:
- 각 HTTP 요청은 독립적이지만, 세션을 통해 요청 간에 인증 상태가 유지됩니다.
- 클라이언트는 보통 쿠키를 통해 세션 ID를 서버에 전달하여 자신의 세션을 식별합니다.
6. SecurityContextHolder의 역할:
- 각 요청 처리 중에는 ThreadLocal을 통해 SecurityContext에 접근합니다.
- 요청 간에는 세션을 통해 SecurityContext가 유지됩니다.
따라서 귀하의 말씀대로, Form 인증에서는 인증 상태가 단일 HTTP 요청의 범위를 넘어 세션의 수명 동안 유지됩니다. 이는 사용자 경험을 개선하고, 매 요청마다 재인증할 필요가 없게 해줍니다.
다만, 이러한 방식은 보안과 사용자 편의성 사이의 균형을 고려해야 합니다. 긴 세션 유지 시간은 편리하지만, 보안 위험을 증가시킬 수 있으므로 적절한 세션 관리 정책(예: 적절한 세션 타임아웃 설정, 중요한 작업 시 재인증 요구 등)이 필요합니다.
AuthenticationManager가 인증에 실패했을 때
AuthenticationManager가 인증에 실패했을 때, 정확히 말하면 SecurityContextHolder 안의 SecurityContext를 지우는 것입니다. 이 과정을 단계별로 설명하겠습니다
- 인증 실패 발생:
- AuthenticationManager가 인증을 시도하고 실패하면 AuthenticationException을 던집니다.
- 예외 처리:
- 이 예외는 일반적으로 인증을 처리하는 필터(예: UsernamePasswordAuthenticationFilter 또는 BasicAuthenticationFilter)에서 잡힙니다.
- SecurityContext 클리어:
- 필터는 SecurityContextHolder.clearContext() 메소드를 호출합니다.
- 이 메소드는 현재 스레드의 SecurityContext를 제거합니다.
- 구체적인 동작
SecurityContextHolder.clearContext();
- 이 메소드는 SecurityContextHolder 자체를 삭제하는 것이 아니라, 현재 ThreadLocal에 저장된 SecurityContext를 null로 설정합니다.
- SecurityContextHolder는 싱글톤 유틸리티 클래스로, 삭제되는 것이 아닙니다.
- 결과:
- 이 작업 후에 SecurityContextHolder.getContext()를 호출하면 새로운 빈 SecurityContext 객체가 반환됩니다.
- 즉, 인증 정보가 제거된 상태가 됩니다.
- 추가 처리:
- 인증 실패 후, 일반적으로 AuthenticationEntryPoint가 호출되어 클라이언트에게 적절한 응답(예: 401 Unauthorized)을 보냅니다.
- Remember Me 처리:
- Remember Me 기능이 활성화되어 있다면, 기존의 Remember Me 토큰도 제거됩니다.
if (rememberMeServices != null) { rememberMeServices.loginFail(request, response); } - 세션 처리:
- 설정에 따라 현재 세션을 무효화할 수 있습니다.
HttpSession session = request.getSession(false); if (session != null) { session.invalidate(); } - AuthenticationEntryPoint 호출:
- 클라이언트에게 인증 실패를 알리고 적절한 응답을 보내기 위해 AuthenticationEntryPoint가 호출됩니다.
- WWW-Authenticate 헤더 설정:
- AuthenticationEntryPoint는 WWW-Authenticate 헤더를 설정합니다.
response.addHeader("WWW-Authenticate", "Basic realm=\"" + getRealmName() + "\""); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage()); - 인증 실패 이벤트 발생:
- AuthenticationFailureBadCredentialsEvent와 같은 이벤트가 발생할 수 있습니다.
if (eventPublisher != null) { eventPublisher.publishEvent(new AuthenticationFailureBadCredentialsEvent( authenticationToken, new BadCredentialsException("Bad credentials"))); } - 로깅:
- 인증 실패에 대한 로그가 기록됩니다.
logger.warn("Authentication attempt failed: " + exception.getMessage());
주의할 점:
- SecurityContextHolder 자체는 삭제되지 않습니다. 이는 애플리케이션 전반에 걸쳐 사용되는 정적 유틸리티 클래스입니다.
- 실제로 삭제되는 것은 현재 스레드에 연결된 SecurityContext 인스턴스입니다.
- 이 작업은 현재 요청의 컨텍스트에만 영향을 미칩니다. 다른 동시 요청의 SecurityContext에는 영향을 주지 않습니다.
- WWW-Authenticate 헤더는 클라이언트에게 서버가 기대하는 인증 방식을 알려줍니다.
- Remember Me 토큰 제거는 지속적인 인증 실패로 인한 보안 위험을 방지합니다.
요약: AuthenticationManager가 인증에 실패했을 때, SecurityContextHolder 안의 SecurityContext가 제거(클리어)되고, Remember Me 토큰이 제거되며, 적절한 응답 헤더가 설정됩니다. 또한, 인증 실패 이벤트가 발생하고 로깅이 이루어집니다. 이 과정을 통해 보안을 강화하고, 클라이언트에게 적절한 피드백을 제공하며, 시스템 관리자가 인증 실패를 모니터링할 수 있게 됩니다.
기억하기 인증 - rememberMe()
RememberMe 인증
• 사용자가 웹 사이트나 애플리케이션에 로그인할 때 자동으로 인증 정보를 기억하는 기능이다
• UsernamePasswordAuthenticationFilter 와 함께 사용되며, AbstractAuthenticationProcessingFilter 슈퍼클래스에서 훅을 통해 구현된다
- 인증 성공 시 RememberMeServices.loginSuccess() 를 통해 RememberMe 토큰을 생성하고 쿠키로 전달한다
- 인증 실패 시 RememberMeServices.loginFail() 를 통해 쿠키를 지운다
- LogoutFilter 와 연계해서 로그아웃 시 쿠키를 지운다
토큰 생성
기본적으로 암호화된 토큰으로 생성 되어지며 브라우저에 쿠키를 보내고,
향후 세션에서 이 쿠키를 감지하여 자동 로그인이 이루어지는 방식으로 달성된다
• username: UserDetailsService 로 식별 가능한 사용자 이름
• password: 검색된 UserDetails 에 일치하는 비밀번호
• expirationTime: remember-me 토큰이 만료되는 날짜와 시간, 밀리초로 표현
• key: remember-me 토큰의 수정을 방지하기 위한 개인 키
• algorithmName: remember-me 토큰 서명을 생성하고 검증하는 데 사용되는 알고리즘(기본적으로 SHA-256 알고리즘을 사용)
-algorithmHex는 해당 내용[username..등]으로 암호화된다.
RememberMeServices 구현체
• TokenBasedRememberMeServices - 쿠키 기반 토큰의 보안을 위해 해싱을 사용한다
• PersistentTokenBasedRememberMeServices -생성된 토큰을 저장하기 위해 데이터베이스나 다른 영구 저장 매체를 사용한다
• 두 구현 모두 사용자의 정보를 검색하기 위한 UserDetailsService 가 필요하다
rememberMe() API
• RememberMeConfigurer 설정 클래스를 통해 여러 API 들을 설정할 수 있다
• 내부적으로 RememberMeAuthenticationFilter 가 생성되어 자동 인증 처리를 담당하게 된다
rememberMe() 실습
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, HttpSecurity httpSecurity) throws Exception {
http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.rememberMe(rememberMe -> rememberMe
.alwaysRemember(true) // true시 기억하기 체크박스를 체크하지않더라도 기억하기 기능을 사용하겠다라는 뜻
.tokenValiditySeconds(3600) // 토큰 만료시간 설정
.userDetailsService(userDetailsManager()) // / UserDetails 를 조회하기 위해 사용되는 UserDetailsService를 지정
.rememberMeParameter("remember") // 로그인 시 사용자를 기억하기 위해 사용되는 HTTP 매개변수이며 기본값은 'remember-me' 이다
.rememberMeCookieName("remember") // 기억하기(remember-me) 인증을 위한 토큰을 저장하는 쿠키 이름이며 기본값은 'remember-me' 이다
.key("security") // 기억하기(remember-me) 인증을 위해 생성된 토큰을 식별하는 키를 설정한다
);
return http.build();
}
@Bean
public UserDetailsManager userDetailsManager() {
UserDetails user = User.withUsername("user")
.password("{noop}1234")
.authorities("ROLE_USER")
.build();
UserDetails user2 = User.withUsername("user2")
.password("{noop}1234")
.authorities("ROLE_USER")
.build();
UserDetails user3 = User.withUsername("user3")
.password("{noop}1234")
.authorities("ROLE_USER")
.build();
return new InMemoryUserDetailsManager(user, user2, user3);
}
}
remember를 체크하고 로그인성공하면
쿠키에 remember라는 쿠키가 생성된것을 확인할 수 있다.
쿠키명이 remember인 이유는
rememberMeCookieName("remember")
위의 설정떄문이다.
로그인하기도전에 jsessionID가 있는 이유는 다음과 같다. [ claude]
Spring Security의 기본 동작과 관련이 있습니다. Spring Security를 사용할 때 JSESSIONID가 로그인 전에 생성되는 것은 일반적인 동작입니다. 이에 대해 좀 더 자세히 설명드리겠습니다:
- 세션 생성: Spring Security는 기본적으로 사용자가 애플리케이션에 첫 요청을 보낼 때 세션을 생성합니다. 이는 로그인 여부와 관계없이 발생합니다.
- 인증 전 세션: 이 세션은 사용자가 아직 인증되지 않은 상태에서도 생성됩니다. 이를 '인증 전 세션'이라고 부르기도 합니다.
- SecurityContextHolder: Spring Security는 이 세션을 사용하여 SecurityContextHolder를 관리합니다. SecurityContextHolder는 현재 사용자의 보안 컨텍스트 정보를 저장합니다.
- 세션 고정 보호: 이 방식은 세션 고정 공격(Session Fixation Attack)을 방지하는 데 도움이 됩니다. 로그인 성공 시 Spring Security는 새로운 세션 ID를 생성하여 이전 세션 ID를 대체합니다.
- 설정 변경 가능: 필요에 따라 이 동작을 변경할 수 있습니다. 예를 들어, sessionCreationPolicy를 STATELESS로 설정하면 세션을 생성하지 않게 할 수 있습니다.
예시 코드:
성능과 확장성: 대부분의 경우 이 기본 동작은 문제가 되지 않지만, 매우 높은 트래픽을 처리해야 하는 경우에는 불필요한 세션 생성이 서버 부하를 증가시킬 수 있습니다.
쿠키에서 jsessionId를 삭제해도 remember가 있기 때문에 자동으로 재인증이 진행되기때문에 로그인페이지로 이동해서 다시 로그인할 필요가 없다. [ 재인증시 새로운 세션 생성 ]
.alwaysRemember(true) // true시 기억하기 체크박스를 체크하지않더라도 기억하기 기능을 사용하겠다라는 뜻
해당 설정을 이용하면 체크박스를 체크하지않더라도 rememberMe 쿠키가 생성되므로 위험할 수 있다.
기억하기 인증 필터 - RememberMeAuthenticationFilter
RememberMeAuthenticationFilter
• SecurityContextHolder에 Authentication이 포함되지 않은 경우 실행되는 필터이다
• 세션이 만료되었거나 어플리케이션 종료로 인해 인증 상태가 소멸된 경우
토큰 기반 인증을 사용해 유효성을 검사하고 토큰이 검증되면 자동 로그인 처리를 수행한다
- RememberMeAuthenticationFilter의 동작:
- 이 필터는 SecurityContextHolder에 Authentication이 없을 때 동작합니다.
- 주로 세션 만료나 애플리케이션 재시작 후 첫 요청 시 활성화됩니다.
- SecurityContextHolder와 SecurityContext:
- SecurityContextHolder는 SecurityContext를 포함합니다.
- SecurityContext 안에 Authentication 객체가 저장됩니다.
- 정확한 구조는 다음과 같습니다: SecurityContextHolder > SecurityContext > Authentication
- 필터의 처리 과정:
- SecurityContextHolder에서 SecurityContext를 가져옵니다.
- SecurityContext 내 Authentication 객체의 존재 여부를 확인합니다.
- Authentication이 없으면, Remember-Me 토큰을 확인합니다.
- 토큰 기반 인증:
- Remember-Me 쿠키에서 토큰을 추출합니다.
- 토큰의 유효성을 검사합니다 (만료 여부, 서명 확인 등).
- 토큰이 유효하면 해당 사용자 정보로 새 Authentication 객체를 생성합니다.
- 자동 로그인 처리:
- 새로 생성된 Authentication 객체를 SecurityContext에 설정합니다.
- 이 SecurityContext를 SecurityContextHolder에 저장합니다.
- 결과적으로 사용자는 자동으로 로그인된 상태가 됩니다.
- 새 세션 생성:
- 이 과정에서 새로운 세션이 생성되고, 새 JSESSIONID가 발급됩니다.
이 과정을 통해 Remember-Me 기능은 사용자가 명시적으로 로그아웃하지 않았다면, 브라우저를 닫았다가 다시 열거나 세션이 만료된 후에도 자동으로 로그인 상태를 유지할 수 있게 합니다.
익명 사용자 - anonymous()
익명 사용자
• 스프링 시큐리티에서 "익명으로 인증된" 사용자와 인증되지 않은 사용자 간에 실제 개념적 차이는 없으며
단지 액세스 제어 속성을 구성하는 더 편리한 방법을 제공한다고 볼 수 있다
[ 익명 사용자 => 인증되지않은 사용자 ]
• SecurityContextHolder 가 항상 Authentication 객체를 포함하고 null 을 포함하지 않는다는 것을
규칙을 세우게 되면 클래스를 더 견고하게 작성할 수 있다
[ 이 접근 방식으로 인해 null 체크가 필요 없어져 코드의 견고성이 향상됩니다.]
• 인증 사용자와 익명 인증 사용자를 구분해서 어떤 기능을 수행하고자 할 때 유용할 수 있으며
익명 인증 객체를 세션에 저장하지 않는다
• 익명 인증 사용자의 권한을 별도로 운용할 수 있다. 즉 인증 된 사용자가 접근할 수 없도록 구성이 가능하다
스프링 MVC 에서 익명 인증 사용하기
public String method(Authentication authentication) {
if (authentication instanceof AnonymousAuthenticationToken) {
return "anonymous";
} else {
return "not anonymous";
}
}
스프링 MVC가 파라미터로 authentication 객체를 넣어줄때 httpServlet의 getPrincipal()을 이용하여 넣어준다.
익명사용자일경우에는 authentication에 null을 넣어서 반환한다.
[ null이 반환되므로 익명요청이지만 결과는 not anonymous가 나오게 된다. ]
그래서
익명 요청에서 Authentication 을 얻고 싶다면 @CurrentSecurityContext를 사용하면 된다.
해당 어노테이션이 붙어있으면 CurrentSecurityContextArgumentResolver 에서 요청을 가로채어 넣어준다.
public String method(@CurrentSecurityContext SecurityContext context){
return context.getAuthentication().getName();
}
AnonymousAuthenticationFilter
• SecurityContextHolder 에 Authentication 객체가 없을 경우 감지하고 필요한 경우 새로운 Authentication 객체로 채운다
Authentication이 null이라면 AnonymousAuthenticationToken을 생성한다.
이 토큰에는 보통 "anonymousUser"라는 principal과 "ROLE_ANONYMOUS" 권한이 포함된다.
생성된 AnonymousAuthenticationToken을 SecurityContextHolder의 SecurityContext에 설정
테스트
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, HttpSecurity httpSecurity) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/anonymous").hasRole("GUEST") // 해당 경로에는 GUEST 권한을 가진 사용자만 접근가능
.requestMatchers("/anonymousContext","/authentication").permitAll() // 해당 경로는 아무 사용자나 접근가능
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.anonymous(httpSecurityAnonymousConfigurer -> httpSecurityAnonymousConfigurer
.principal("guest") // principal의 기본값은 anonymousUser 이다.
.authorities("ROLE_GUEST") // 기본값은 ROLE_ANONYMOUS
);
return http.build();
}
@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();
}
로그인하지않은 상태에서 /anonymous 경로로 접근하면
다음과 같이 경로에 접근가능하다.
.anonymous(httpSecurityAnonymousConfigurer -> httpSecurityAnonymousConfigurer
.principal("guest") // principal의 기본값은 anonymousUser 이다.
.authorities("ROLE_GUEST") // 기본값은 ROLE_ANONYMOUS
);
이 설정으로 인해 인증되지않은 사용자 [익명사용자]는 GUEST라는 권한을 가지게 되고
/anonymous 경로는 GUEST 권한을 가진 사용자만 접근이 가능하기 때문이다.
로그인하고 다시 /anonymous에 접근해보면
403이라는 권한에러가 발생한다.
logout은 controller에 따로 경로설정없어도 spring security가 구현해놓았다.
로그아웃후 http://localhost:8080/anonymousContext에 접근해보면
익명사용자의 principal이 잘 보이는것을 확인가능하다.
@GetMapping("/anonymousContext")
public String anonymousContext(@CurrentSecurityContext SecurityContext context) {
return context.getAuthentication().getName();
}
'인프런 > 스프링 시큐리티 완전 정복 [6.x 개정판]' 카테고리의 다른 글
7) 인증 아키텍처 - Authentication , SecurityContext / SecurityContextHolder (1) | 2024.10.14 |
---|---|
6) 인증 프로세스 - logout() ,RequestCache / SavedRequest (0) | 2024.10.11 |
4) 인증프로세스 - 폼 인증,폼 인증 필터, HTTP Basic 인증 (0) | 2024.10.08 |
3) 초기화 과정이해 - DelegatingFilterProxy / FilterChainProxy , 사용자 정의 보안 설정하기 (0) | 2024.10.05 |
2) 초기화 과정이해 - WebSecurity / HttpSecurity (3) | 2024.10.02 |
댓글