인증 프로세스

enxnong·2024년 5월 21일
0

스프링 시큐리티

목록 보기
1/13

정수원님의 강의 스프링 시큐리티 완전 정복 [6.x 개정판] 보면서 공부한 내용입니다.

기본 인증 - httpBasic()

basic 인증

  • http는 액세스 제어와 인증을 위한 프레임워크를 제공하며 가장 일반적인 인증 방식이다
  • 인증 프로토콜은 HTTP 인증 헤더에 기술되어 있다

  1. 클라이언트는 인증정보 없이 서버로 접속을 시도한다
  2. 서버는 인증을 받지 않으면 해당 요청을 거부한다는 가정이 있을 때, 서버는 클라이언트에게 인증 요구를 보낸다
    → 401 응답, WWW-Authenicate헤더를 기술해서 보안영역과 basic인증 방법을 보냄
  3. 클라이언트는 id, password를 Base64로 인코딩하여 Aithorization 헤더에 담아서 요청한다
  4. 인증이 완료되면 200(성공) 응답을 보낸다

💡 주의사항

  • base64로 인코딩한 값은 암호화된 값이 아니어서 디코딩이 가능하기 때문에 인증 정보가 노출된다
  • 그렇기에 반드시 HTTPS(보안 프로토콜)와 같이 TLS 기술과 함께 사용해야한다

💡 TLS 란?

  • 정보를 암호화해서 송수신하는 프로토콜로 웹사이트 주소는 HTTPS로 시작한다.

httpBasic() API

  • HttpBasicConfigurer 설정 클래스를 통해 api를 설정 가능
  • 내부적으로 BasicAuthenticationFilter가 생성되어 기본 인증 방식의 인증 처리를 담당

💡API 내 authenticationEntryPoint의 담당역할

  • 인증을 받지 못한 경우 다시 로그인 페이지로 이동하도록 도와줌
@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) // 어떤 요청에도 인증을 받겠다
                .httpBasic(basic -> basic.authenticationEntryPoint(new CustomAuthenticationEntryPoint()));

        return http.build();
    }

  • Basic 인증을 받지 못한 사용자에게 인증을 받을 수 있도록 처리하는 기본 구현체

기본 인증 필터

BasicAuthenticationFilter

  • 해당 필터는 기본 인증 서비스를 제공하는데 사용된다
  • BasicAuthenticationConverter을 사용하여 요청 헤더에 기술된 인증정보의 유효성을 체크하며 Base64 인코딩된 username과 password를 추출한다
  • 인증 이후 세션을 사용하는 경우와 사용하지 않는 경우에 따라 처리되는 흐름에 차이가 있다
    => 세션을 사용하는 경우 : 매 요청마다 인증과정을 거치지 않음
    => 세션을 사용하지 않는 경우(기본) : 매 요청마다 인증과정을 거침

💡 BasicAuthenticationFilter는 일반적으로 세션을 사용하지 않고
인코딩된 문자열을 통해 계속적으로 요청을 통해서 인증을 처리한다.

기억하기 인증

rememberMe()

  • 사용자가 웹 사이트나 앱에 로그인할 때 자동으로 인증 정보를 기억하는 기능
  • UserNamePasswordAuthenticationFilter와 함께 사용되며, AbstractAuthenticationProcessingFilter 슈퍼클래스에서 훅을 통해 구현된다
    => 인증 성공인 경우 : loginSuccess를 통해 RememberMe 토큰을 생성하고 쿠키로 전달
    => 인증 실패인 경우 : loginFail를 통해 쿠키를 지운다
    => LogoutFilter와 연계하여 로그아웃 시 쿠키를 지운다

토큰 생성

  • 암호화된 토큰으로 생성되며 브라우저에 쿠키를 보내고, 이후 자동 로그인이 이루어지는 방식으로 달성됨

    💡 암호화되는 값
    base64(username, expirationTime, algorithmName, algorithmHex(이하 동일 + password + key))

RememberMeService 구현체

  • TokenBasedRememberMeServices : 쿠키 기반 토큰의 보안을 위해 해싱을 사용한다(메모리)
  • PersistentTokenBasedRememberMeServices : 생성된 토큰을 저장하기 위해 데이터베이스나 다른 영구 저장 매체를 사용한다
  • 두 구현 모두 사용자의 정보를 검색하기 위한 UserDetailsService 객체가 필요하다

RememberMe() API

  • 내부적으로 RememberMeAuthenticationFilter가 생성되어 자동 인증 처리를 담당하게 된다
http.rememberMe(... -> ...
	.alwaysRemember(true) // 쿠키가 항상 생성되어야하는지의 여부 (기본 : false)
    .tokenValiditySeconds(3600) // 토큰의 유효한 시간
    .userDetailsService(..) // 사용자 정보를 가져옴
    .rememberMeParameter("remember") // 기본적으로 설정되어있음
    .RememberMeCookieName("remember") // 인증을 위한 토큰을 저장하는 쿠키 이름
    .key("security") // 기억하기 인증을 위해 생성된 토큰을 식별하는 키
  • remember 체크 생성

  • 체크 후 cookie 확인 => remember 쿠키 생성

  • 세션을 지워도 쿠키에서 저장되어있기 때문에 다시 접근해도 인증을 받지 않아도됨

기억하기 인증 필터

RememberMeAuthenticationFilter

  • SecurityContextHolder에 Authentication이 포함되지 않은 경우 실행되는 필터
    => 즉, 인증이 되지 않은 경우에만 필터 작용
  • 토큰 기반 인증을 통해 검증되면 자동 로그인 처리 수행

=> this.securityContextHolderStrategy.getContext().getAuthentication()가 true인 경우: 자동인증 수행 x (사용자가 인증 상태에 있는 것)

-> 세션을 삭제했음에도 불구하고 remember라는 캐시때문에 자동인증이 실행됨

-> 해당 과정을 통해 자동 로그인 실행됨

익명 사용자

anonymous()

  • 익명으로 인증된사용자와 인증되지 않은 사용자의 개념적 차이는 없으며, 단지 액세스 제어 속성을 구성하는데 더 편리한 방법을 제공한다
    => 인증되지 않은 사용자지만 익명으로 인증되었다라는 개념으로도 사용됨
  • SecurityContextHolder 가 항상 Authentication 객체를 포함하고 null 을 포함하지 않는 것을 규칙으로 세우게되면 클래스를 더 견고하게 작성할 수 있다
  • 인증 사용자와 익명 인증 사용자를 구분해서 어떤 기능을 수행하고자 할 때 유용할 수 있으며 익명 인증 객체를 세션에 저장하지 않는다
  • 익명 인증 사용자의 권한을 별도로 운용할 수 있다
    => 즉, 인증 된 사용자가 접근할 수 없도록 구성이 가능하다

    💡 인증된 사용자와 익명 사용자가 있을 때 익명 사용자는 실체가 없는 것으로 해석하기 쉽다.
    단, 스프링 시큐리티는 인증된 사용자와 익명 사용자를 동일한 기준(객체로)에서 구분하여 SecurityContext안에 저장을 한다. 차이점은 익명 사용자의 인증 객체는 세션에 저장하지 않는 것이다.

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
				.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
				.formLogin(Customizer.withDefaults())
				.anonymous(anonymous -> anonymous
						.principal("guest") // 익명 사용자 이름 설정
						.authorities("ROLE_GUEST") // 익명 사용자 권한 설정
				);
		return http.build();
}

스프링 MVC 에서 익명 인증 사용하기

  • 스프링 MVC가 HttpServletRequest#getPrincipal 을 사용하여 파라미터를 해결하는데 요청이 익명(인증을 받지 못한 사용자가 요청했을 때)일 때 파라미터 값은 null이다
public String method(Authentication authentication) { // 인증 객체가 들어옴
		if (authentication instanceof AnonymousAuthenticationToken) { // 익명일 때는 AnonymousAuthenticationToken객체가 들어오는 것이 아닌 null로 들어옴
				return "anonymous";
		} else {
				return "not anonymous"; // null인 경우 반환
		}
}
  • 익명 요청에서 Authentication 을 얻고 싶다면 @CurrentSecurityContext를 사용하면 된다.
  • CurrentSecurityContextArgumentResolver 에서 요청을 가로채어 처리한다
public String method(@CurrentSecurityContext SecurityContext contest) { // 익명 사용자의 값을 얻을 때
		return context.getAuthentication().getName();
}

AnonymousAuthenticationFilter

  • SecurityContextHolder 에 Authentication 객체가 없을 경우 감지하고 필요한 경우 새로운 Authentication 객체로 채운다

  • AnonymousAuthenticationFilter의 역할

@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()) // 인증을 못받았을 때 formLogin을 통해 인증을 받음
                // 익명 사용자의 권한
                .anonymous(anonymous -> anonymous
                        .principal("guest")
                        .authorities("ROLE_GUEST")// 익명 사용자만 접근할 수 있는 자원 설정
                        // 익명 사용자라 할지라고 권한이 따로 있으므로 해당 권한을 가진 자만 접근 가능하도록 설정
                        // 만약, 인증을 받은 사용자라 할지라도 해당 권한이 없으면 익명 사용자가 접근할 수 있는 자원에는 접근 안되
                );
}

    @GetMapping("/anonymous")                                                        
    public String anonymous(Authentication authentication) {                        
        return "anonymous";                                                          
    }                                                                          
    @GetMapping("/authentication")                                                   
    public String authentication(Authentication authentication){                     
        if(authentication instanceof AnonymousAuthenticationToken){                  
            return "anonymous";                                                      
        } else {                                                                     
            return "null";                                                           
        }                                                                            
    }                                                                          
    @GetMapping("/anonymousContext")                                                 
    public String anonymousContext(@CurrentSecurityContext SecurityContext context){ 
        // null이 아닌 실제 익명 객체를 참고하고 싶을 때 context를 통해 익명 객체를 받을 수 있다                   
        return context.getAuthentication().getName();                                
}                  

로그아웃

  • DefaultLogoutPageGeneratingFilter 를 통해 로그아웃 페이지를 제공하며 “GET / logout” URL 로 접근이 가능
  • 기본적으로 로그아웃 실행은 POST/logout 으로만 가능하나 CSRF 기능을 비활성화거나 RequestMatcher을 사용할 경우 GET, PUT, DELETE 모두 가능

    💡 CSRF 기능이란?
    사용자 요청에 대해서 세션을 악의적인 목적으로 사용하지 못하도록 스프링 시큐리티가 활성화하는 기능 (공격에 대비)

  • 로그아웃 필터를 거치지 않고, 스프링 MVC에서 커스텀하게 구현할 수 있다.
    => 로그인 페이지가 커스텀하게 생성될 경우 로그아웃 기능도 커스텀하게 구현
    => 로그인 페이지 기본으로 생성될 경우 로그아웃 기능도 기본으로 생성

logout() API

http.logout(httpSecurityLogoutConfigurer -> httpSecurityLogoutConfigurer
 .logoutUrl("/logoutProc") // 로그아웃이 발생하는 URL 을 지정
 .logoutRequestMatcher(new AntPathRequestMatcher("/logoutProc","POST")) // 로그아웃이 발생하는 RequestMatcher 을 지정
// Method 를 지정하지 않으면logout URL이 어떤 HTTP 메서드로든 요청될 때 로그아웃 할 수 있다 (GET, PUT, DELETE 모두 가능)
 .logoutSuccessUrl("/logoutSuccess") // 로그아웃이 발생한 후 이동하는 URL (getMapping 해줘야함)
 .logoutSuccessHandler((request, response, authentication) -> { // 사용할 LogoutSuccessHandler 를 설정
response.sendRedirect("/logoutSuccess"); // LogoutSuccessHandler가 더 우선시됨 
 })
 .deleteCookies("JSESSIONID“, “CUSTOM_COOKIE”) // 로그아웃 성공 시 제거될 쿠키의 이름 지정
 .invalidateHttpSession(true) // HttpSession을 무효화해야 하는 경우 true (기본값), 그렇지 않으면 false
 .clearAuthentication(true) // 로그아웃 시 SecurityContextLogoutHandler가 인증(Authentication)을 삭제 해야 하는지 여부 : true (기본값)
 // fasle 인 경우 : Authentication 삭제 안함
 .addLogoutHandler((request, response, authentication) -> {}) // 기존의 로그아웃 핸들러 뒤에 새로운 LogoutHandler를 추가
// 기존의 것은 그대로 수행
 .permitAll() // logoutUrl(), RequestMatcher() 의 URL 에 대한 모든 사용자의 접근을 허용

요청 캐시

RequestCache

  • 인증 절차 문제로 리다이렉트 된 후에 이전에 했던 요청 정보를 담고 있는 'SavedRequest’ 객체를 쿠키 혹은 세션에 저장하고 필요시 다시 가져와 실행하는 캐시 메카니즘이다
    => 요청 객체를 캐시화 한다

SavedRequest

  • SavedRequest 은 로그인과 같은 인증 절차 후 사용자를 인증 이전의 원래 페이지로 안내하며 이전 요청과 관련된 여러 정보를 저장한다

requestCache() API

  • 요청 url 뒤에 '?/customParam=y'인 경우에만! HttpSession에 저장된 SavedRequest를 가져오도록 설정할 수 있다 (기본값 : continue)
    => 나머지는 가져오지 않음
// 특정한 경우에만 필요한 것
HttpSessionRequestCacherequestCache=newHttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("customParam=y");
		http
          .requestCache((cache)->cache
          .requestCache(requestCache)
);
  • 요청을 저장하지 않으려면 NullRequestCache를 설정하면 된다
RequestCachenullRequestCache=newNullRequestCache();
      http
        .requestCache((cache)->cache
        .requestCache(nullRequestCache)
);

RequestCacheAwareFilter

  • 이전에 저장했던 웹 요청을 다시 불러오는 역할이다
  • SavedRequest 가 현재 Request 와 일치하면 이 요청을 필터 체인의 doFilter 메소드에 전달하고 SavedRequest 가 없으면 필터는 원래 Request 을 그대로 진행한다
profile
높은 곳을 향해서

0개의 댓글