이전 글까지 Client 에 CAS를 이용한 Single Sign On 설정을 하고 로그인 테스트까지 해봤다.

이번 글에서는 다음 내용에 대해 알아보자.

  1. Client에 로그아웃을 설정한다. (클라이언트 로그아웃)
  2. Single Sign Out(단일 로그 아웃)을 설정한다.

먼저 Client에 로그아웃을 설정하는 것은 매우 간단하다.

Spring Security 설정 부분에 logout을 추가하면 된다.

 

 

빨간색 박스 안에 있는 내용중에 '.logout()'을 추가하면 LogoutFilter 가 등록되고 URL Path에 '/logout/' 으로 입력하면 로그아웃 Filter가 실행 된다. 그리고  '.logoutSuccessUrl("/secured") 를 추가하면 로그아웃이 성공하면 '/secured' 로 이동 하라는 설정이다. ( .logoutSuccessUrl을 설정하지 않으면 Default로 '/login?logout' 으로 이동한다.)

 

노란색 박스 안에 있는 내용은 csrf 를 비활성화 시키는 것인데, 스프링 5 이후에는 csrf가 필수로 적용되어야 하는데 지금은 테스트를 하기 위한 작업이라서 비활성화 시켰다. (비활성화 해야 GET(HttpMethod)를 처리 한다.)

 

이전 글까지 했던 설정에 문제가 없었다면 다음 이미지와 같이 'localhost:9000/secured' 페이지를 확인 할 수 있을 것이다.

이제 path를 '/logout' 으로 변경하고 요청을 보내보자.

어떻게 될까??

Logout을 했는데 '/secured' 페이지에 접근이 가능하다!

이유는??

 

Client에 CAS SSO 를 설정하지 않고 Security 설정에 formLogin() 을 사용 했다면 로그인 화면으로 이동 했을 것이다.

SSO를 적용하지 않은 경우에는 이렇게 동작하는 것이 정상적인 동작이다.

 

하지만 현재 작업중인 Client 프로젝트에는 SSO 설정을 했기 때문에 로그아웃을 한 뒤에 '/secured' 에 접근해도 로그인 화면으로 이동하지 않는 것이다.

 

간단하게 설명하면(자세한 설명은 별도로 작성할 예정이다.)

Client에서는 로그아웃을 했지만 CAS 인증서버 에서는 로그아웃 하지 않았기 때문이다.

  1. Client에서 로그아웃을 실행한다.
  2. Logout Filter에서 로그아웃을 진행하고 세션을 만료시킨다.
  3. Logout이 성공하고 logoutSuccessUrl 로 설정한 '/secured' 페이지로 이동한다.
  4. '/secured' 페이지는 인증이 필요하므로 CAS 인증 서버로 redirect 한다.
  5. CAS 인증 서버에는 아직 Client의 로그인 정보가 유효하므로 로그인 요청을 하지 않고 인증성공 응답을 한다.
  6. Client에서는 CAS 인증 서버에서 인증성공 응답이 왔으므로 인증성공 처리를 한다.

위와 같은 이유로 Client 에서 로그아웃 처리를 했어도 인증이 필요한 페이지에 접근이 가능한 것이다.

 

그렇다면 어떻게 해야 할까?? 간단하다.

CAS 인증 서버에서 로그아웃을 하면 된다.

 

CAS 인증 서버에 로그아웃 요청을 보내기 위해서는 Client 프로젝트에 다음과 같이 설정이 필요하다.

 

먼저 CAS 인증서버 Logout을 하기 위해 필요한 Bean을 추가하는 것이다.

 

CAS Configuration 클래스에 다음과 같이 Bean을 추가하자.

@Configuration
public class CasClientConfiguration {
	//기타 Bean...
    //@Bean
    //CasAuthenticationProvider 등등...
    @Bean
    public LogoutFilter casLogoutFilter() {
        LogoutFilter logoutFilter = new LogoutFilter( "https://localhost:8443/cas/logout",securityContextLogoutHandler());
        logoutFilter.setFilterProcessesUrl("/logout/cas");
        return logoutFilter;
    }
    
    @Bean
    public SecurityContextLogoutHandler securityContextLogoutHandler() {
        return new SecurityContextLogoutHandler();
    }

    @Bean
    public SingleSignOutFilter singleSignOutFilter() {
        SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
        singleSignOutFilter.setCasServerUrlPrefix("https://localhost:8443/cas");
        singleSignOutFilter.setIgnoreInitConfiguration(true);
        return singleSignOutFilter;
    }    
}

 

각각 설명을 하면 다음과 같다.

 

casLogoutFilter

cas Server 로그아웃을 하기 위한 filter 이다.
기본 LogoutFilter 와는 별개로 존재 하고 FilterChain 순서 상 기본 LogoutFilter 앞에 위치한다.
/logout/cas URL 을 intercept 해서 Cas Server 로 redirection 한 뒤 전체 Logout 을 실행한다.
서브는 관련된 모든 서비스에 로그아웃 요청을 전송하고 서비스에서는 SingleSignOutFilter 에서 HTTP 세션을 무효하 시키면서 로그아웃이 된다.

 

securityContextLogoutHandler

logout Filter 가 실행(doFilter) 될 때 해당 Handler 가 사용 된다.
자세한 동작은 클래스 내부의 logout Method 를 확인 해보면 알겠지만 session 을 만료 시키고, SecurityContextHolder 의 SecurityContext 를 가져온 뒤 Authentication 객체를 null 로 만든다.

 

singleSignOutFilter

  1. cas Server 에서 로그아웃이 완료되면 같은 ID로 로그인 되어 있는 모든 Client 서비스에 로그아웃 요청을 보낸다.
    요청 URL 은 Cas Server 에 등록되어 있는 ServiceId 를 사용한다.
    Cas Server 에서 Client 의 로그아웃 요청을 보낼 때, logoutRequest 라는 parameter 를 함께 보내는데,
    SingSignOutFilter 에서는 logoutRequest parameter 가 존재할 경우에 서비스 로그아웃 처리를 하게 된다.
  2. cas Server 에서 로그인이 완료되면(또는 이미 로그인 되어 있으면) 로그인 요청을 했던 Client 서비스에 redirect 한다.
    이때 ticket 이라는 parameter 를 함께 전달하는데, SingleSignOutHandler 에 ticket 을 저장한다.
    저장 방식은 현재 ID_TO_SESSION_KEY_MAPPING(Map)에 Key : SessionId, Value : ticket 으로 저장하고
    MANAGED_SESSIONS(Map)에 key : ticket, Value : Session 으로 저장 한다.
    이렇게 저장하는 이유는 Cas Server 에서 각각의 Client 서비스에 로그아웃 요청을 보낼 때 사용하기 위해서다.
    Cas Server 로 부터 로그아웃 요청을 받은 Client 는 함께 받은 xml 형식의 parameter 값에서 ticket 값을 얻는다.
    ticket 값으로 MANAGED_SESSIONS 에서 Session 을 가져온 뒤 해당 session 을 만료 시킨다.
    이렇게 되면 Cas Server 를 통해 인증 완료한 뒤 저장되어 있던 session 이 만료 되기 때문에 인증이 만료된다.

그리고 Security 설정에 다음과 같이 추가를 한다.

 

Bean으로 추가 했던 SingleSignOutFilter와 casLogoutFilter를 Filter에 추가 한다.

그리고 logoutSuccessUrl 을 '/logout/cas' 로 수정한다.

'/logout/cas'는 casLogoutFilter 의 filterProcessesUrl 이다. 즉 '/logout/cas' 로 요청이 왔을 때 Filter가 동작하는 것이다.

client에서 로그아웃 처리를 하고나서 바로 casLogoutFilter 가 동작하는 것이고 casLogoutFilter는 CAS 인증서버에 로그아웃 요청을 보내는 것이므로 진짜 로그아웃 처리가 될 것이다.

 

client를 다시 실행시켜서 '/secured' 로 이동한 뒤 logout 요청을 보내보자.

그럼 다음과 같은 화면이 나올 것이다.

 

;

CAS 인증서버로 redirect 된 것을 확인할 수 있다.

그리고 다시 Client의 '/secured' 로 이동해보면 다음과 같은 화면이 나올 것이다.

CAS 인증서버의 로그인 화면으로 이동한 것을 확인할 수 있다.

그렇다. Client 로그아웃이 잘 되었다!

 

이번에는 단일로그아웃에 대해서 알아보자.

단일로그아웃 이란

여러개의 서비스가 하나의 인증서버를 통해 인증처리를 했을 경우, 하나의 서비스에서 로그아웃 했을 경우에 다른 서비스들 역시 로그아웃 처리가 되도록 하는 것을 말한다.

 

CAS 인증서버 프로젝트에 json 파일로 Client를 등록했던 것을 기억할 것이다.

해당 파일에 logoutUrl 을 추가하면 된다.

사실 logoutUrl 을 지정하지 않으면 serviceId 에서 host 부분을 가져와서 사용하는데, localhost로 되어 있는 경우에는 logoutUrl을 추출하지 못한다.

따라서 현재 테스트에서 처럼 logoutUrl 을 별도로 추가하는 방법

정상적인 domain으로 되어 있는 경우 logoutUrl이 자동으로 등록되는 방법이 있다.

 

Client 프로젝트 설정은 이미 되어 있고 테스트만 해보면 된다.

시나리오는 다음과 같다.

  1. 두 개의 Client 서비스를 실행한다.
  2. 하나의 서비스에서 로그인을 시도한다.
  3. 다른 서비스에서 인증이 필요한 페이지에 요청을 보내고 로그인이 잘 되었는지 확인한다.
  4. 하나의 Client 서비스에서 로그아웃을 시도한다.
  5. 다른 서비스에서 로그아웃이 되었는지 확인한다.

위 순서대로 테스트 하면 하나의 Client 서비스에서 로그아웃을 했을 때, 다른 서비스에서도 로그아웃이 되는 것을 확인 할 수 있다.

 

샘플 프로젝트에서 설정한 것처럼 logoutSuccessUrl에 CAS 인증서버의 로그아웃 URL을 보내는 방법 보다는 logoutSuccessUrl에 로그아웃 성공 시 보여지는 화면을 하나 만들고 그 화면에서 CAS 인증서버 로그아웃 요청('/logout/cas')을 처리하는 것이 좋다.

이유는 단일로그아웃에 대한 자세한 설명글에서 다루겠다.

+ Recent posts