이전 글까지 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')을 처리하는 것이 좋다.

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

이전 글에서는 Client에서 Cas 인증 서버로 Redirect 한 뒤 인증처리가 되는 것을 확인했다.

 

하나의 Client가 잘 동작하는 것은 확인 했다.

이제 하나의 Client에서 인증이 완료 됐을 때, 다른 Client에서도 인증이 되는지 확인해 볼 필요가 있다.

 

프로젝트를 하나 더 만들어도 되지만 어차피 거의 똑같은 구조이기에 PORT 만 변경해서 Client 가 실행될 수 있도록
Cas 인증 서버 프로젝트와 Client 프로젝트를 수정해보자.

 

Cas 인증서버 프로젝트

Cas 인증서버에서는 JSON 파일 하나만 추가하면 된다.

그리고 실행 시켜보자.

새로 추가한 Client도 등록이 된 것을 확인할 수 있다.

Client 프로젝트

application.properties 파일에 server.port를 다음과 같이 수정하자.

server.port=${PORT:9000}

환경 변수로 PORT를 사용하고 입력하지 않았을 경우에 '9000' 을 사용하는 설정이다.

 

CasClientConfiguration 파일에 다음과 같이 추가하자.

    @Value("${server.port}")
    String port;

그리고 ServicePropertiesCasAuthenticationProvider Bean을 등록할 때 하드코딩 해놨던 포트 번호를 @Value로 추가한 port 변수로 변경한다.

@Bean
    public ServiceProperties serviceProperties() {
        ServiceProperties serviceProperties = new ServiceProperties();
        serviceProperties.setService("http://localhost:"+port+"/login/cas");
        serviceProperties.setSendRenew(false);
        return serviceProperties;
    }
 @Bean
    public CasAuthenticationProvider casAuthenticationProvider() {
        CasAuthenticationProvider provider = new CasAuthenticationProvider();
        provider.setServiceProperties(serviceProperties());
        provider.setTicketValidator(ticketValidator());
        provider.setUserDetailsService(
                s -> new User("kennen", "Mellon", true, true, true, true,
                        AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
        provider.setKey("CAS_PROVIDER_LOCALHOST_"+port);
        return provider;
    }

설명에서는 하드코딩으로 설정해놨지만 실제 소스는 Properties를 사용한다.

 

이제 Terminal 에서 실행시켜보자.

 

포트 9000

$ mvn spring-boot:run -DPORT=9000

포트 9100

$ mvn spring-boot:run -DPORT=9100

 

포트가 다른 Client 두개가 잘 실행됐다.

9100 포트로 실행된 클라이언트에서 로그인을 시도해보자.

로그인이 잘 성공되면, 9000번 포트로 실행한 Client의 URL Path를 /secured 로 변경해보자.

 

위 이미지 처럼 9000번 포트의 Client에서는 로그인을 하지 않았지만 /secured 에 대한 응답을 확인할 수 있다.

 

다음 글에서는 Client의 로그아웃을 설정하고 Single Sign On의 로그아웃 버전인 Single Sign Out을 설정해보자.

이전 글에서 CAS 인증 서버에 클라이언트를 등록했다.

 

이제 클라이언트의 인증처리를 CAS 인증 서버에서 동작하도록 클라이언트 프로젝트에 작업해보자.

 

먼저, Spring Security의 CAS 모듈과 관련된 Bean을 설정한다. Spring Security와 Cas 인증 서버와 함께 동작하기 위함이다.

 

다음과 같이 설정 클래스를 생성하고 Bean을 추가해보자.

@Configuration
public class CasClientConfiguration {

    @Bean
    public ServiceProperties serviceProperties() {
        ServiceProperties serviceProperties = new ServiceProperties();
        serviceProperties.setService("http://localhost:9000/login/cas");
        serviceProperties.setSendRenew(false);
        return serviceProperties;
    }

    @Bean
    @Primary
    public AuthenticationEntryPoint authenticationEntryPoint(ServiceProperties sP) {
        CasAuthenticationEntryPoint entryPoint
                = new CasAuthenticationEntryPoint();
        entryPoint.setLoginUrl("https://localhost:8443/cas/login");
        entryPoint.setServiceProperties(sP);
        return entryPoint;
    }

    @Bean
    public TicketValidator ticketValidator() {
        return new Cas30ServiceTicketValidator(
                "https://localhost:8443/cas");
    }

    @Bean
    public CasAuthenticationProvider casAuthenticationProvider() {
        CasAuthenticationProvider provider = new CasAuthenticationProvider();
        provider.setServiceProperties(serviceProperties());
        provider.setTicketValidator(ticketValidator());
        provider.setUserDetailsService(
                s -> new User("kennen", "Mellon", true, true, true, true,
                        AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
        provider.setKey("CAS_PROVIDER_LOCALHOST_9000");
        return provider;
    }
}

위에 추가한 Bean을 하나씩 설명하면

ServiceProperties

[현재 서비스에 대한 정보]
ExceptionTranslationFilter 의 doFilter 동작 중에 AccessDeniedException 이 발생하면 ExceptionTranslationFilter 에 있는 AuthenticationEntryPoint(CasAuthenticationEntryPoint)의 commence 메소드가 실행 된다.
이때 Cas 인증 서버로 이동해 로그인을 시도하게 되는데 Service 정보를 함께 전달 한다.
인증 서버에서 인증 시도 후에 다시 서비스로 Redirect 할 때 service 의 URL 로 사용하기 위해서 이다.

authenticationEntryPoint

Spring Security 에서 httpBasic 설정을 추가하게 되면 Default 로 BasicAuthenticationFilter 가 추가된다.
httpBasic().authenticationEntryPoint 를 이용해 AuthenticationEntryPoint 를 지정할 수 있는데,
CasAuthenticationEntryPoint 를 등록하기 위해 해당 Bean 을 추가한다.
인증이 필요한 페이지에 접근 시 AccessDeniedException 이 발생하면 CasAuthenticationEntryPoint 의 commence 가 실행된다.
Cas Server 의 로그인 페이지로 redirect 된다. (ServiceProperties Bean 참고)

casAuthenticationProvider

CasAuthenticationFilter 가 동작할 때 실제로 인증을 처리하는 주체이다.
Filter 가 동작하는 중에 인증을 진행하게 된다.
Bean 으로 등록한 ServiceProperties, ticketValidator, UserDetailsService 가 이 때 사용되며
Key 는 CasAuthenticationToken 을 만들 때 인자로 쓰인다.

ticketValidator

[티켓 유효성 체크]
실행 되기 전 순서
1. ExceptionTranslationFilter 진행 중에 AccessDeniedException 발생
2. CasAuthenticationEntryPoint.commence 실행
3. Cas 서버에서 인증 성공 후에 - CasAuthenticationFilter.doFilter 실행
4. CasAuthenticationFilter.attemptAuthentication 실행
5. AuthenticationManager.authenticate 실행
6. ProviderManager.authenticate 실행
7. AuthenticationProvider.authenticate 실행
8. CasAuthenticationProvider.authenticate 실행
9. 8번 실행 중에 authenticateNow 실행
10. authenticateNow 메소드 안에서 ticketValidator 사용
설명
Cas 서버에서 인증 성공을 하게 되면 CasAuthenticationFilter 로 들어오게 된다.
이 때 redirect 된 서비스가 인증요청을 보낸 서비스가 맞는지 확인하기 위해서 한번 더 체크를 한다.
redirect 될 때 받은 ticket 과 현재 서비스의 이름을 다시 Cas 서버로 보내고
일치 하면 XML 형식으로 응답을 받게 된다.
이후에는 해당 xml 응답을 parsing 해서 UserDetails 로 변환한 뒤 CasAuthenticationToken 을 만들게 된다.

 

 

이제 특정 경로를 인증이 필요하도록 설정하고, 이 때 CasAuthenticationEntryPoint가 사용 되도록 Spring Security를 설정해보자.

 

WebSecurityConfigurerAdapter를 상속받는 Configuration을 만든다.

 

또한 CasAuthenticationFilter를 Bean으로 추가하고,

CasAuthenticationFilter에서 사용 할 AuthenticationManager 역시 Bean으로 추가한다.

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    ServiceProperties serviceProperties;
    @Autowired
    private AuthenticationProvider authenticationProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().regexMatchers("/secured.*", "/login").authenticated()
                .and().authorizeRequests().antMatchers("/favicon.ico", "/static/**").permitAll()
                .and().authorizeRequests().antMatchers("/admin/**").hasAuthority("ADMIN")
                .and().authorizeRequests().antMatchers("/user/**").hasAuthority("USER")
                .and().authorizeRequests().regexMatchers("/").permitAll()
                .and().httpBasic().authenticationEntryPoint(authenticationEntryPoint);
    }
   
    @Bean
    public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
        CasAuthenticationFilter filter = new CasAuthenticationFilter();
        filter.setServiceProperties(serviceProperties); // Bean 위치 : CasConfig.java
        filter.setAuthenticationManager(authenticationManager());
        return filter;
    }

    @Bean
    public AuthenticationManager authenticationManager() {
        return new ProviderManager(Arrays.asList(authenticationProvider));
    }
}

/secured, /login 에 대한 요청은 인증이 필요하도록 설정하고 '/' 에 대한 요청은 public 페이지로 설정했다.

Controller를 생성하고 테스트 해보자.

 

HomeController

@RestController
public class HomeController {

    @GetMapping("/")
    public String home(){
        return "home";
    }
}

SecuredController

@RestController
public class SecuredController {

    @GetMapping("/secured")
    public Authentication secured() {
        Authentication auth = SecurityContextHolder.getContext()
                .getAuthentication();
        return auth;
    }
}

 

먼저 HomeController에 요청을 보내보자.

그리고 /secured 또는 /login 요청을 보내보자.

URL을 확인해보자. 

이전 요청과 포트 번호가 다른 것을 확인할 수 있다. 인증서버로 Redirect 된 것이다.

인증서버에서 인증 가능한 사용자 ID, Password를 입력하자.

그리고 이 ID,Password는 위에서 설정한 CasAuthenticationProvider 에서 UserDetailsService 로 등록했다.

 

로그인 하면 다음과 같은 결과를 확인할 수 있다.

인증이 필요한 요청인 /secured URL을 확인 할 수 있다. 

또한 Controller에서 Return 했던 Authentication 내용을 확인할 수 있다.

 

로그인은 성공했다.

하지만 Single Sign On 은 다수의 Client Application이 한번의 로그인으로 인증처리 하는 것이 목적이기 때문에 Client Application을 하나 더 만들어서 테스트 해본다.

 

다음 글에서는 두 개의 Client가 한번의 로그인으로 인증처리가 되는지 확인해 보자.

 

 

어떻게 인증이 진행 되는지에 대한 자세한 설명은 잠시 미루고 Single Sign On 그리고 Single Sign Out 이 잘 동작하는지를 먼저 확인해보자.

  1. rohyun 2020.11.26 02:52

    재미있는 내용 매우 잘 보고 갑니다

인증서버가 클라이언트의 인증을 해준다는 것은 당연한 일이다.

하지만 모든 클라이언트가 인증을 위해 액세스 하는 것을 허용하지는 않는다.

따라서 인증 서버클라이언트/서비스를 등록하고 해당 서비스들만 액세스 할 수 있도록 해야 한다.

 

이 내용은 사실 CAS 인증 서버 쪽에서 다뤄야 하지만 흐름상 지금 하는 것이 이해하기 쉽다고 생각했다.

 

따라서 이 글에서 진행되는 모든 작업은 CAS 인증서버 프로젝트에서 하면 된다.

 

인증서버에 서비스를 등록하는 방법은 여러가지가 있다.

YAML, JSON, Mongo, LDAP 등이 그것이다.

 

이 글에서는 JSON 서비스 레지스트리 방법을 사용할 것이다.

 

의존성은 이미 https://authentication.tistory.com/27?category=795724 글에서 추가를 했다.

 

Client Application의 정보를 담고 있는 JSON 파일을 작성해보자.

 

  • ${project}/src/main/resources/ 디렉토리 하위에 services 디렉토리를 생성한다.
  • casSecuredApp-19991.json 이라는 파일을 services 디렉토리에 생성한다.

그리고 json 파일에 다음의 내용을 추가한다.

{
    "@class" : "org.apereo.cas.services.RegexRegisteredService",
    "serviceId" : "^http://localhost:9000/login/cas",
    "name" : "CAS Spring Secured App",
    "description": "This is a Spring App that usses the CAS Server for it's authentication",
    "id" : 19991,
    "evaluationOrder" : 1
}

다음과 같은 구조가 된다.

 

json 파일 내용을 설명하면 다음과 같다.

 

serviceId : 인증 서버를 사용하고자 하는 클라이언트에 대한 정규식 URL 패턴을 정의한다. 이 글에서는 localhost:9000 로 된 클라이언트를 추가한다.

id : 실수로 중복해서 서비스가 등록되는 경우 충돌을 방지하기 위해 사용된다. json 파일의 이름은 serviceName-id.json 컨벤션을 따른다.

theme, proxyPolicy, logo, privacyUrl 등등 : https://apereo.github.io/cas/5.1.x/installation/Service-Management.html

 

다음 설정을 통해 JSON Service Registry를 설정하자.

하나는 서비스 구성 파일이 있는 디렉토리의 위치이다.

다른 하나는 JSON 구성 파일에서 서비스 Registry 초기화를 사용 가능하도록 하는 것이다.

 

cas.properties 라는 파일을 ${project}/src/main/resources 디렉토리에 생성한다.

그리고 나서 인증서버를 재구동 해보자.

 

Cas 인증서버에 잘 추가가 되었다.

 

다음 글에서는 클라이언트 서버에서 인증 처리를 Cas 인증서버를 통해 할 수 있도록 작업해 보겠다.

SSO(Single Sign On) 에서 가장 중요한 것은 CAS 인증서버를 구성하는 것이다.

하지만 인증서버를 사용하는 클라이언트가 없다면 인증서버는 없는 것이나 다름없다.

이번글 부터는 클라이언트 서비스 프로젝트를 설명하도록 하겠다.

 

일반적으로 SSO 는 다수의 서비스가 하나의 인증경로를 통해 인증처리를 하기위해 사용하므로, 

반드시 두 개 이상의 서비스가 필요하다고 생각 할 수 있지만 하나의 서비스만 있어도 문제가 없다.

왜냐하면 인증처리만 다른 곳에서 처리하는 것이기 때문이다.

 

따라서 먼저 하나의 클라이언트 서비스만 생성하고 나서 테스트 한 뒤, 정상적으로 작동하면 해당 서비스를 복사해서 SSO 가 잘 동작하는지 확인 해보는 방식으로 진행 하겠다.

 

먼저 Spring initializr를 사용해서 프로젝트를 생성하자.

 

 

빨간색 박스를 확인해자. 의존성으로 Web, Security, Freemarker 를 추가했다.

그리고 Cas의 SSO 인증을 위한 의존성을 추가하자.

<dependency>
     <groupId>org.springframework.security</groupId>
     <artifactId>spring-security-cas</artifactId>
</dependency>

그리고 나서 application.properties 파일에 port를 9000 으로 설정하고 서버를 실행시켜보자.

서버를 실행하고 브라우저에 'localhost:9000' 주소를 입력하면 'localhost:9000/login' 으로 redirect 될 것이다.

로그인 화면을 추가한 적이 없지만 로그인 화면이 보인다.

 

이것은 Spring Security를 의존성 추가하면 별도 작업을 하지 않아도 된다.

 

아직 CAS 인증 서버와 연결하지는 않았지만 기본적으로 Spring Security가 잘 동작하는지를 확인하기 위해 다음과 같은 작업을 추가한다.

 

그리고 재시작 한 뒤 ID와 Password를 입력하면 로그인이 될 것이다.

 

Spring Security 가 잘 동작하는 것을 확인 했으니 이제 진짜 인증서버와 연동하는 작업을 하면 된다.

 

그전에 더 중요한 것이 있는데, CAS 인증 서버에 클라이언트를 등록하는 것이다.

 

다음 글에서는 클라이언트 등록하는 방법에 대해 알아보겠다.

이전 글에서는 JDBC 인증을 어떻게 사용해서 로그인 하는지에 대해 알아보았다.

 

이번 글에서는 JDBC 인증이 어떻게 설정이 되었는지에 대해 알아보도록 하자.

 

가장 먼저, JDBC를 이용해서 사용자 인증을 하기 위해서는 첫 번째 글에서 언급했던 cas-server-support-jdbc 의존성 추가를 해야 한다.

        <dependency>
            <groupId>org.apereo.cas</groupId>
            <artifactId>cas-server-support-jdbc</artifactId>
            <version>${cas.version}</version>
        </dependency>

 

 

의존성이 추가되면 cas-server-support-jdbc 프로젝트에 있는 spring.factories 파일의 EnableAutoConfiguration에 추가 된 @Configuration 클래스들이 Configuration 객체로 추가 된다.

 

 

아래 이미지는 추가 된 CasJdbcAuthenticationConfiguration 클래스이다.

 

CasJdbcAuthenticationConfiguration 를 살펴보자.

 

AuthenticationEventExecutionPlanConfigurer 를 구현한 JdbcAuthenticationEventExecutionPlanConfiguration 클래스가 @Configuration 으로 등록되어 있다. 

따라서 JdbcAuthenticationEventExecutionPlanConfiguration 역시 @Configuration 객체로 등록이 된다.

 

그리고 노란색 상자를 자세히 확인해보자.

jdbcAuthenticationHandlers 메소드 내부를 보면 AuthenticationHandler Collection 객체를 생성하고

application.properties 에 설정한 cas 설정의 값을 가져와서 위에서 생성한 AuthenticationHandler Collection에 추가하고 나서 반환하는 메소드 이다.

 

casProperties.getAuthn().getJdbc() 이 부분은 아래 이미지의 빨간색 박스에 있는 값이다.

따라서 지역변수 jdbc(JdbcAuthenticationProperties)는 getBind, getEncode, getQuery, getSearch 메소드들을 통해 application.properties에 설정한 값을 가져올 수 있게 된다.

예제에서는 cas.authn.jdbc 만 설정했기 때문에 아래 코드만 확인 하면 된다.

jdbc.getQuery().forEach(b -> handlers.add(queryDatabaseAuthenticationHandler(b)));

jdbc.getQuery().foreach 이 부분을 보면 query 배열을 반복하면서 queryDatabaseAuthenticationHandler 메소드에 query 객체를 하나씩 전달해서 handler에 추가(add) 하고 있는것을 확인할 수 있다.

 

queryAndEncodeDatabaseAuthenticationHandler 를 확인해보자.

빨간색 박스로 표시한 부분이 핵심이다. 

간단하게 말해서 queryAndEncodeDatabaseAuthenticationHandler 메소드는 Query를 이용해서 Database에 있는 계정 정보를 조회해서 인증을 처리하는 Handler를 만들어서 리턴하는 메소드 이다.

 

이때 전달받은 파라미터 b(JdbcAuthenticationProperties.Query) 에 있는 정보들을 handler를 생성하고 설정할 때 사용하는데 우리가 application.properties 파일에 설정했던 값들임을 확인할 수 있다.

 

QueryDatabaseAuthenticationHandler 내부에 대해서는 이후에 인증과정에서 자세하게 확인해보자.

지금은 어떻게 등록이 되었는지를 확인하는데 집중하도록 하자.

 

여기까지가 어떻게 JDBC 인증이 설정되는지에 대한 설명이기 때문에 이정도만 이해해도 진행하는데 큰 무리가 없을 것이다.

 

하지만 조금 더 알고 싶은 사람들을 위해 조금 더 설명하도록 하겠다.

 

JdbcAuthenticationEventExecutionPlanConfiguration 이 클래스는 어디서 사용하는 것일까??

위 이미지를 보면 JdbcAuthenticationEventExecutionPlanConfiguration 클래스는 

AuthenticationEventExecutionPlanConfigurer 인터페이스를 구현한 것을 확인할 수 있다.

그리고 AuthenticationEventExecutionPlanConfigurer 인터페이스를 구현한 구현체들을 확인해면 다음과 같다.

그리고 이 구현체들을 사용하는 곳은 바로 CasCoreAuthenticationConfiguration 클래스이다.

빨간색 박스를 보면 List<AuthenticationEventExecutionPlanConfigurer> 를 파라미터로 받고 있는 것을 확인할 수 있고, @Autowired 애노테이션이 추가되었기 때문에 의존성 주입이 되어 있다는 것을 알 수 있다.

정말로 위에 설명했던 구현체들이 맞는지 Debug 모드로 실행하여 확인해보자.

Debug 모드로 실행 중에 breakpoint를 걸어두고 확인해보았다. 

아래 이미지와 같은색 박스끼리 일치한다는 것을 알 수 있다. 

List<AuthenticationEventExecutionPlanConfigurer자체를 Bean 으로 등록한 것은 아니고, 각각의 구현체를 Bean 으로 등록한 뒤에 List 형태로 주입한 것이다.

프로젝트에서 AuthenticationEventExecutionPlanConfigurer 인터페이스를 구현한 @Configuration 클래스를 생성하고 나서 spring.factories 에 추가하게 되면 해당 클래스 역시 List<AuthenticationEventExecutionPlanConfigurer에 포함되는 것을 확인할 수 있다.

 

이제 authenticationEventExecutionPlan 메소드를 자세히 살펴보자.

핵심은 DefaultAuthenticationEventExecutionPlan 클래스를 Bean으로 추가하는 메소드이다.

그리고 위에서 설명한 AuthenticationEventExecutionPlanConfigurer 인터페이스의 구현체들을 주입 받아서  각각의 구현체들을 DefaultAuthenticationEventExecutionPlan Bean 객체에 설정해주는 작업을 한다.

 

각각의 AuthenticationEventExecutionPlanConfigurer 구현체들은 DefaultAuthenticationEventExecutionPlan Bean 객체에 어떤 설정을 하는지 configureAuthenticationExecutionPlan 인터페이스의

configureAuthenticationExecutionPlan 메소드를 확인해보자.

쉬운 설명을 위해서 위에서 설명했던 JdbcAuthenticationEventExecutionPlanConfiguration 클래스를 확인해보자.

위 이미지를 보면 어디서 많이 봤을 것이다. 바로 현재 글 위에서 설명했던 내용과 같다.

위에서는 jdbcAuthenticationHandlers() 에 대해서 설명을 했다. 

지금은 노란색 박스를 설명하려고 한다.

jdbcAuthenticationHandlers().forEach(h -> plan.registerAuthenticationHandlerWithPrincipalResolver(h, personDirectoryPrincipalResolver));

위에서 설명했던 것처럼 jdbcAuthenticationHandlers() 를 통해 설정된 jdbc 인증처리 handler를 가져온다.

그 다음 파라미터로 받은 DefaultAuthenticationEventExecutionPlan 클래스 인스턴스에 handler와 PrincipalResolver 등록해준다.

 

그리고 DefaultAuthenticationEventExecutionPlan 클래스를 확인해보자.

노란색 박스 부분을 보면 JdbcAuthenticationEventExecutionPlanConfiguration 에서 호출한 메소드 임을 확인할 수 있다.

그리고 빨간색 부분을 보면 authenticationHandlerPrincipalResolverMap 라는 멤버변수에 handler를 keyprincipal을 value로 put 하는 것을 확인할 수 있다.

 

authenticationHandlerPrincipalResolverMap 변수는 인증처리를 할 때 사용된다.

사용되는 구체적인 코드는 인증 처리 과정에서 자세하게 설명하도록 하겠다.

 

지금까지 JDBC 인증방식 설정에 대해 알아 보았다.

 

다음 글에서는 Client Application을 생성하고 CAS 인증서버와 SSO 테스트를 해보겠다.

 

 

마지막으로 CasCoreAuthenticationConfiguration 에 대해 간략히 설명하고 마치겠다.

 

CasCoreAuthenticationConfiguration 설정 클래스는 cas-server-core-authentication 의존성이 추가되어 있어햐 하는데, 첫번째 글에서 언급했던 cas-server-support-json-service-registry 의존성을 추가하면 의존성 전이를 통해 프로젝트에 추가가 된다.

의존성 전이 순서는 다음과 같다.

cas-server-support-json-service-registry -> cas-server-core-services -> cas-server-core-authentication

다른 @Configuartion 클래스와 마찬가지로 cas-server-core-authentication 프로젝트에 있는 spring.factories 에 추가가 되어 있기 때문에 설정객체로 추가 된다.

 

 

 

 

  1. 펭수 2019.11.23 18:28

    펭하!

이전글 에서는 테스트를 위한 정적 사용자 계정을 추가해서 로그인까지 해봤다.

이어서 클라이언트 Application을 만들고 SSO 기본인증 테스트를 해도 되지만 그전에 해야할 일이 있다.

우선 로그인 화면에 정적 계정 사용에 대한 경고 문구가 뜨는 것이 보기 싫고, 무엇보다 사용자를 Database가 아닌 프로젝트에 넣어두는 것이 마음에 들지 않기 때문에 최소한 Database에서 계정을 찾아 인증하는 부분까지는 완성하고 나서 클라이언트 Application을 시작 하도록 하겠다.

 

인증 방식에는 여러가지가 있는데 나는 이중에서 Database Authentication의 Query를 사용하였다.

 

사용자 계정 Database

여러가지 Database가 있겠지만, 나는 mysql을 설치해서 사용했다.

PC에 mysql을 설치하고 아래 이미지처럼 사용자 table을 생성하고 테스트용 데이터를 저장하였다.

password 는 bCrypt로 암호화 하여 저장하였다.

Database에 계정이 생겼으니 이제 application.properties 파일에 설정 해주면 된다.

 

#cas.authn.accept.users=casuser::Mellon

cas.authn.jdbc.query[0].driverClass=com.mysql.cj.jdbc.Driver
cas.authn.jdbc.query[0].url=jdbc:mysql://localhost:3306/sso?autoReconnect=true&allowMultiQueries=true&serverTimezone=UTC
cas.authn.jdbc.query[0].sql=select id, name, password from sso.user where id = ?;
cas.authn.jdbc.query[0].user=root
cas.authn.jdbc.query[0].password=1234
cas.authn.jdbc.query[0].fieldPassword=password
cas.authn.jdbc.query[0].passwordEncoder.type=BCRYPT

먼저 casuser 라는 static 계정은 주석처리를 하였다.

로그인 화면에서 static 계정에 대한 경고 문구가 사라지게 하기 위함이다.

 

cas.authn.jdbc.query[0].passwordEncoder.type=BCRYPT

위 코드는 passwordEncoder를 설정하는 부분인데, 위에서 설명했듯이 password 를 bCrypt로 암호화 하여 저장했기 때문에 passwordEncoder 타입을 bCrypt로 설정 해줘야만 인증이 성공한다.

 

이전 글에서 설명했던 것 처럼. cas.authn.jdbc.query* 로 지정해서 사용 가능한 이유는 CasConfigurationProperties 클래스의 구조대로 입력했기 때문이다.

 

그리고 빌드를 다시 하고 서버를 다시 실행해보자.

기존에 확인됐던 static 계정에 대한 경고 문구가 사라졌다.

 

그리고 database에 저장한 계정으로 로그인해보자.

로그인이 성공적으로 완료 되었다.

 

다음 글에서는 jdbc인증이 어떤식으로 등록되었는지 확인해보자.

  1. 펭수 2019.11.23 18:11

    펭하!

지금까지 서버를 실행하고 SSL을 적용하여 경고 문구를 삭제 하였다.

 

이제 남은일은 로그인 하는 것이다.

 

물론 클라이언트를 통해서 로그인을 해야 진정한 SSO 라고 할 수 있지만, 우선 서버에서 로그인을 해보자.

 

서버에 로그인이 되고나면 그때부터 클라이언트 Application을 작성하도록 하겠다.

 

테스트 계정

application.properties에 다음과 같이 설정해보자.

놀랍게도 테스트 계정이 생성되었다.

ID : casuser

PW : Mellon

의 계정이 생성 되었다!!

 

서버를 실행시켜 보도록 하자.

새로운 경고 문구가 등장했다.

해석하면 "기본적인 인증을 위해 CAS에서는 정적 사용자 리스트를 허용 하지만 데모 프로젝트에서만 사용해라!!!"

이다.

 

저 경고 문구는 조금 뒤에 없애도록 하고 우선은 casuser로 로그인 해보자.

로그인이 성공하였다!!

 

하지만 아무 기능이 없다. 로그인만 되었다.

 

어떻게 로그인이 된 것일까??

 

테스트 계정이니까 그냥 되겠지~

뭔지 모르겠지만 application.properties 파일에 저런 형식으로 계정만 추가하면 되는구나~~

라고 하고 넘어가기엔 뭔가 찝찝하다.

 

우리가 테스트 계정을 사용하기 전에 프로젝트를 수정한 곳은 application.properties 파일 딱 하나이다.

cas.authn.accept.users=casuser::Mellon

이렇게 입력하면 ID가 casuser 이고 PW가 Mellon 인 계정이 생성된다는 것은 알았는데, 어떻게 동작하는지 궁금하다.

아니 궁금해야 한다.

이 내용은 앞으로

  • Database를 사용한 사용자 계정 추가
  • 사용자 계정 쿼리 수정
  • 사용자 정의 설정 

등에 필요한 내용이므로 잘 알아두도록 하자.

 

결론부터 말하자면 저 계정을 등록하는 일을 하는 것은 바로 아래 나오는 이녀석이다.

 

CasCoreAuthenticationHandlersConfiguration

먼저 CasCoreAuthenticationhandlersConfiguration의 클래스를 보자.

1. 이미지에 나오는 것 처럼 dependency로 추가 되어 있는 것을 알 수 있다.

어디서 추가 되었는지 아래 이미지를 보면서 확인해보자.

Intellij Plugin 중에 Dependency Analyzer 를 설치 하면 위 이미지처럼 의존성을 확인할 수 있다.

core-authentication 의존성은 여러곳에서 추가를 해놨기 때문에 현재 진행중인 CAS 인증서버에서 추가가 되어 있는 것이다.

저렇게 여러곳에서 의존성을 추가해도 문제가 되지 않는 것은 maven에서 정해놓은 규칙인데 궁금하면 댓글로 남기겠다.

 

2. 의존성은 추가가 되었다는 것은 알겠는데, 어떻게 해서 저 클래스가 설정파일로 인식이 되고 사용이 되는것일까??

아래 이미지를 보면서 확인해보자.

cas-server-core-authentication 프로젝트의 META-INF 디렉토리에 spring.factories 파일을 보자.

이곳에서 Spring boot 자동설정일 추가하고 있으며, 해당 클래스들 역시 cas-server-core-authentication 프로젝트에 존재한다.

따라서 해당 항목들이 설정으로 추가가 되고, 실제로 사용이 되는 것이다.

또한, 추가적인 설정 파일이 필요하다면 resources/META-INF 디렉토리 하위에 spring.factories 파일을 추가하여 사용 가능하다.

spring.factories에 대한 자세한 내용은 구글을 참조하길 바란다.

 

3. CasConfigurationProperties 클래스는 바로 application.properties 에서 추가했던 계정 내용을 가지고 있다.

빨간색 박스에 대해 모르겠으면 아래 링크를 참고하자.

https://www.baeldung.com/configuration-properties-in-spring-boot

 

Guide to @ConfigurationProperties in Spring Boot | Baeldung

A quick and practical guide to @ConfigurationProperties annotation in Spring Boot.

www.baeldung.com

간략하게 설명하면 application.properties(또는 application.yml) 파일에 설정값을 클래스 객체화 하여 사용할 수 있도록 하는 방식이다.

우리가 application.properties 파일에 cas.* 로 설정을 해서 사용하는 이유이다.

그리고 AuthenticationProperties 는 authn 이라는 이름의 필드로 되어 있다. 

이 또한 우리가 application.properties 파일에 cas.authn.* 로 사용 가능했던 것이다.

 

즉, cas.authn.accept.users 라고 우리가 설정한 값은 CasConfigurationProperties 파일에 객체화 되어있으며, 그 객체는 Bean으로 등록되어 @Autowired로 사용 가능한 것이다.

 

cas.authn.accept.users 의 클래스 구조

  • CasConfigurationProperties : cas
    • AuthenticationProperties : authn
      • AcceptAuthenticationProperties : accept
        • String : users

 

지금까지 샘플 프로젝트 인증서버가 완료 되었다.

 

솔직히 지금 만들어 놓은 인증서버를 실제로 사용하는 것은 불가능하다.

 

왜냐하면 계정을 프로젝트에 입력해서 사용하는 경우는 없기 때문이다.

따라서 Database를 통해 인증을 할 수 있도록 구현해야 한다.

 

다음은 Database에 있는 계정 정보를 통해 인증하는 방법에 대해서 알아 보도록 하자.

+ Recent posts