1. 개요

이 예제에서는 Apereo CAS(Central Authentication Service)를 살펴보고 Spring Boot 서비스가 이를 인증에 사용하는 방법을 알아봅니다. CAS 는 오픈 소스이기도 한 엔터프라이즈 SSO(Single Sign-On) 솔루션입니다.

SSO란 무엇입니까? 동일한 자격 증명으로 YouTube, Gmail 및 Map에 로그인하면 Single Sign-On이 됩니다. CAS 서버와 Spring Boot 앱을 설정하여 이를 시연할 것입니다. Spring Boot 앱은 인증을 위해 CAS를 사용합니다.

2. CAS 서버 설정

2.1. CAS 설치 및 의존성

서버는 Maven(Gradle) War Overlay 스타일을 사용하여 설정 및 배포를 용이하게 합니다.

git clone https://github.com/apereo/cas-overlay-template.git cas-server

이 명령은 cas-overlay-template을 cas-server 디렉토리 에 복제합니다 .

우리가 다룰 측면 중 일부는 JSON 서비스 등록 및 JDBC 데이터베이스 연결을 포함합니다. 따라서 build.gradle 파일 의 의존성 섹션 에 해당 모듈을 추가합니다 .

compile "org.apereo.cas:cas-server-support-json-service-registry:${casServerVersion}"
compile "org.apereo.cas:cas-server-support-jdbc:${casServerVersion}"

최신 버전의 casServer를 확인하자 .

2.2. CAS 서버 구성

CAS 서버를 시작하기 전에 몇 가지 기본 구성을 추가해야 합니다. cas-server/src/main/resources 폴더와 이 폴더를 생성하여 시작하겠습니다 . 그런 다음 폴더에 application.properties 도 생성됩니다  .

server.port=8443
spring.main.allow-bean-definition-overriding=true
server.ssl.key-store=classpath:/etc/cas/thekeystore
server.ssl.key-store-password=changeit

위 설정에서 참조한 키 저장소 파일 생성을 진행해 보겠습니다. 먼저 cas-server/src/main/resources/etc/cas 및  /etc/cas/config 폴더를 만들어야 합니다 .

그런 다음 디렉터리를 cas-server/src/main/resources/etc/cas 로 변경 하고 다음 명령을 실행하여 키 저장소를 생성 해야 합니다 .

keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore -storepass changeit -validity 360 -keysize 2048

SSL 핸드셰이크 오류가 발생하지 않도록 하려면 이름과 성 값으로 localhost를 사용해야 합니다. 조직 이름과 단위에도 동일하게 사용해야 합니다. 또한 클라이언트 앱을 실행하는 데 사용할 JDK/JRE로 thekeystore를 가져와야 합니다 .

keytool -importkeystore -srckeystore thekeystore -destkeystore $JAVA11_HOME/jre/lib/security/cacerts

소스 및 대상 키 저장소의 비밀번호는 changeit 입니다 . Unix 시스템에서는 관리자( sudo ) 권한 으로 이 명령을 실행해야 할 수 있습니다 . 가져온 후에는 실행 중인 모든 Java 인스턴스를 다시 시작하거나 시스템을 다시 시작해야 합니다.

CAS 버전 6.1.x에서 필요하기 때문에 JDK11을 사용하고 있습니다. 또한 홈 디렉토리를 가리키는 환경 변수 $JAVA11_HOME을 정의했습니다. 이제 CAS 서버를 시작할 수 있습니다.

./gradlew run -Dorg.gradle.java.home=$JAVA11_HOME

애플리케이션이 시작되면 터미널에 "READY"가 인쇄되고 https://localhost:8443 에서 서버를 사용할 수 있습니다 .

2.3. CAS 서버 사용자 구성

사용자를 구성하지 않았기 때문에 아직 로그인할 수 없습니다. CAS에는 독립 실행형 모드를 포함하여 다양한 구성 관리 방법이 있습니다. 속성 파일 cas.properties를 생성 할 구성 폴더 cas-server/src/main/resources/etc/cas/config를 생성해 보겠습니다 . 이제 속성 파일에서 정적 사용자를 정의할 수 있습니다.

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

설정을 적용하려면 config 폴더의 위치를 ​​CAS 서버에 전달해야 합니다. 명령줄에서 위치를 JVM 인수로 전달할 수 있도록 tasks.gradle을 업데이트해 보겠습니다 .

task run(group: "build", description: "Run the CAS web application in embedded container mode") {
    dependsOn 'build'
    doLast {
        def casRunArgs = new ArrayList<>(Arrays.asList(
          "-server -noverify -Xmx2048M -XX:+TieredCompilation -XX:TieredStopAtLevel=1".split(" ")))
        if (project.hasProperty('args')) {
            casRunArgs.addAll(project.args.split('\\s+'))
        }
        javaexec {
            main = "-jar"
            jvmArgs = casRunArgs
            args = ["build/libs/${casWebApplicationBinaryName}"]
            logger.info "Started ${commandLine}"
        }
    }
}

그런 다음 파일을 저장하고 다음을 실행합니다.

./gradlew run
  -Dorg.gradle.java.home=$JAVA11_HOME
  -Pargs="-Dcas.standalone.configurationDirectory=/cas-server/src/main/resources/etc/cas/config"

cas.standalone.configurationDirectory 의 값은 절대 경로입니다 . 이제 https://localhost:8443 으로 이동하여 사용자 이름 casuser 및 비밀번호 Mellon 으로 로그인할 수 있습니다 .

3. CAS 클라이언트 설정

Spring Initializr를 사용하여 Spring Boot 클라이언트 앱을 생성합니다. Web , Security , FreemarkerDevTools 의존성이 있습니다 . 게다가 Spring Security CAS 모듈에 대한 의존성을 pom.xml추가합니다 .

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

마지막으로 다음 Spring Boot 속성을 추가하여 앱을 구성해 보겠습니다.

server.port=8900
spring.freemarker.suffix=.ftl

4. CAS 서버 서비스 등록

클라이언트 응용 프로그램은 인증 전에 CAS 서버에 등록해야 합니다 . CAS 서버는 YAML, JSON, MongoDB 및 LDAP 클라이언트 레지스트리 사용을 지원합니다.

이 사용방법(예제)에서는 JSON Service Registry 메서드를 사용합니다. 또 다른 폴더 cas-server/src/main/resources/etc/cas/services를 만들어 봅시다 . 서비스 레지스트리 JSON 파일을 저장할 폴더입니다.

클라이언트 애플리케이션의 정의가 포함된 JSON 파일을 생성합니다. 파일 이름 casSecuredApp-8900.json은 erviceName-Id.json  패턴을 따릅니다 .

{
  "@class" : "org.apereo.cas.services.RegexRegisteredService",
  "serviceId" : "http://localhost:8900/login/cas",
  "name" : "casSecuredApp",
  "id" : 8900,
  "logoutType" : "BACK_CHANNEL",
  "logoutUrl" : "http://localhost:8900/exit/cas"
}

serviceId 속성은 클라이언트 애플리케이션에 대한 정규식 URL 패턴을 정의합니다 . 패턴은 클라이언트 애플리케이션의 URL과 일치해야 합니다.

id 속성 고유해야 합니다. 즉,  동일한 CAS 서버에 동일한 id를 가진 서비스가 2개 이상 등록되어서는 안 됩니다. 중복 ID가 있으면 충돌이 발생하고 구성이 재정의됩니다.

또한 나중에 싱글 로그아웃을 할 수 있도록 로그아웃 유형을 BACK_CHANNEL 로 , URL을 http://localhost:8900/exit/cas 로 구성합니다.
CAS 서버가 JSON 구성 파일을 사용하려면 먼저 cas.properties에서 JSON 레지스트리를 활성화해야 합니다 .
cas.serviceRegistry.initFromJson=true
cas.serviceRegistry.json.location=classpath:/etc/cas/services

5. CAS 클라이언트 싱글 사인온 구성

다음 단계는 CAS 서버와 작동하도록 Spring Security를 ​​구성하는 것입니다. 또한 CAS 시퀀스라고 하는 상호 작용의 전체 흐름을 확인해야 합니다 .

Spring Boot 앱의 CasSecuredApplication 클래스 에 다음 빈 구성을 추가해 보겠습니다 .

@Bean
public CasAuthenticationFilter casAuthenticationFilter(
  AuthenticationManager authenticationManager,
  ServiceProperties serviceProperties) throws Exception {
    CasAuthenticationFilter filter = new CasAuthenticationFilter();
    filter.setAuthenticationManager(authenticationManager);
    filter.setServiceProperties(serviceProperties);
    return filter;
}

@Bean
public ServiceProperties serviceProperties() {
    logger.info("service properties");
    ServiceProperties serviceProperties = new ServiceProperties();
    serviceProperties.setService("http://cas-client:8900/login/cas");
    serviceProperties.setSendRenew(false);
    return serviceProperties;
}

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

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

ServiceProperties 빈은 casSecuredApp-8900.jsonserviceId 와 동일한 URL을 가 집니다 . 이는 CAS 서버에 대해 이 클라이언트를 식별하기 때문에 중요합니다.

ServiceProperties 의 sendRenew 속성은 false 설정 됩니다 . 즉, 사용자는 서버에 로그인 자격 증명을 한 번만 제시하면 됩니다.

AuthenticationEntryPoint bean은 인증 예외 처리합니다. 따라서 인증을 위해 사용자를 CAS 서버의 로그인 URL로 리디렉션합니다.

요약하면 인증 흐름은 다음과 같습니다.

  1. 사용자가 인증 예외를 트리거하는 Security 페이지에 액세스하려고 시도합니다.
  2. 예외는 AuthenticationEntryPoint 를 트리거합니다 . 이에 대한 응답으로 AuthenticationEntryPoint는 사용자를 CAS 서버 로그인 페이지( https://localhost:8443/login) 로 안내합니다.
  3. 인증에 성공하면 서버는 티켓을 사용하여 클라이언트로 다시 리디렉션합니다.
  4. CasAuthenticationFilter는 리디렉션을 선택하고 CasAuthenticationProvider를 호출합니다.
  5. CasAuthenticationProvider는 TicketValidator를 사용하여 CAS 서버에서 제시된 티켓을 확인합니다.
  6. 티켓이 유효한 경우 사용자는 요청된 Security URL로 리디렉션됩니다.

마지막으로 WebSecurityConfig 에서 일부 경로를 보호하도록 HttpSecurity를 ​​구성해 보겠습니다 . 이 과정에서 예외 처리를 위한 인증 진입점도 추가합니다.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().antMatchers( "/secured", "/login").authenticated()
      .and()
      .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())
      .and()
      .addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class)
}

6. CAS 클라이언트 싱글 로그아웃 구성

지금까지 싱글 사인온을 다루었습니다. 이제 CAS 싱글 로그아웃(SLO)을 살펴보겠습니다.

사용자 인증 관리를 위해 CAS를 사용하는 애플리케이션은 다음 두 위치에서 사용자를 로그아웃할 수 있습니다.

  • 클라이언트 애플리케이션은 로컬에서 사용자를 로그아웃할 수 있습니다. 이는 동일한 CAS 서버를 사용하는 다른 애플리케이션의 사용자 로그인 상태에 영향을 미치지 않습니다.
  • 클라이언트 애플리케이션은 CAS 서버에서 사용자를 로그아웃할 수도 있습니다. 이렇게 하면 동일한 CAS 서버에 연결된 다른 모든 클라이언트 앱에서 사용자가 로그아웃됩니다.

먼저 클라이언트 애플리케이션에 로그아웃을 적용한 다음 이를 CAS 서버의 단일 로그아웃으로 확장합니다.

장면 뒤에서 무슨 일이 일어나는지 명확히 하기 위해 로컬 로그아웃을 처리하는 logout() 메서드를 만듭니다. 성공하면 싱글 로그아웃 링크가 있는 페이지로 리디렉션됩니다.

@GetMapping("/logout")
public String logout(
  HttpServletRequest request, 
  HttpServletResponse response, 
  SecurityContextLogoutHandler logoutHandler) {
    Authentication auth = SecurityContextHolder
      .getContext().getAuthentication();
    logoutHandler.logout(request, response, auth );
    new CookieClearingLogoutHandler(
      AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY)
      .logout(request, response, auth);
    return "auth/logout";
}

단일 로그아웃 프로세스에서 CAS 서버는 먼저 사용자의 티켓을 만료한 다음 등록된 모든 클라이언트 앱에 비동기 요청을 보냅니다. 이 신호를 수신하는 각 클라이언트 앱은 로컬 로그아웃을 수행합니다. 따라서 한 번 로그아웃의 목표를 달성하면 모든 곳에서 로그아웃이 발생합니다.

그런 다음 클라이언트 앱에 일부 빈 구성을 추가해 보겠습니다. 특히  CasSecuredApplicaiton 에서 :

@Bean
public SecurityContextLogoutHandler securityContextLogoutHandler() {
    return new SecurityContextLogoutHandler();
}

@Bean
public LogoutFilter logoutFilter() {
    LogoutFilter logoutFilter = new LogoutFilter("https://localhost:8443/logout",
      securityContextLogoutHandler());
    logoutFilter.setFilterProcessesUrl("/logout/cas");
    return logoutFilter;
}

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

logoutFilter는 / logout/cas 에 대한 요청을 가로채고 응용 프로그램을 CAS 서버로 리디렉션합니다. SingleSignOutFilter는 CAS 서버에서 오는 요청을 가로채고 로컬 로그아웃을 수행합니다.

7. 데이터베이스에 CAS 서버 연결

MySQL 데이터베이스에서 자격 증명을 읽도록 CAS 서버를 구성할 수 있습니다. 로컬 시스템에서 실행 중인 MySQL 서버의 테스트 데이터베이스를 사용합니다 . cas-server/src/main/resources/etc/cas/config/cas.properties를 업데이트해 보겠습니다 .

cas.authn.accept.users=

cas.authn.jdbc.query[0].sql=SELECT * FROM users WHERE email = ?
cas.authn.jdbc.query[0].url=
  jdbc:mysql://127.0.0.1:3306/test?
  useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialect
cas.authn.jdbc.query[0].user=root
cas.authn.jdbc.query[0].password=root
cas.authn.jdbc.query[0].ddlAuto=none
cas.authn.jdbc.query[0].driverClass=com.mysql.cj.jdbc.Driver
cas.authn.jdbc.query[0].fieldPassword=password
cas.authn.jdbc.query[0].passwordEncoder.type=NONE

cas.authn.accept.users 를 공백으로 설정했습니다 . 이렇게 하면 CAS 서버의 정적 사용자 저장소 사용이 비활성화됩니다.

위의 SQL에 따르면 사용자의 자격 증명은 users 테이블에 저장됩니다. 이메일 열 사용자의 Security 주체( username )를 나타냅니다.

지원되는 데이터베이스, 사용 가능한 드라이버 및 방언 List을 확인하십시오 . 또한 비밀번호 인코더 유형을 NONE 으로 설정합니다 . 다른 암호화 메커니즘 과 고유한 속성도 사용할 수 있습니다.

CAS 서버 데이터베이스의 Security 주체는 클라이언트 애플리케이션의 Security 주체와 동일해야 합니다.

CAS 서버와 동일한 사용자 이름을 갖도록 CasAuthenticationProvider를 업데이트해 보겠습니다 .

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

CasAuthenticationProvider는 인증에 암호를 사용하지 않습니다. 그럼에도 불구하고 인증에 성공하려면 사용자 이름이 CAS 서버의 사용자 이름과 일치해야 합니다. CAS 서버는 MySQL 서버가 포트 3306 의 localhost 에서 실행 중이어야 합니다 . 사용자 이름과 암호는 root 여야 합니다 .

CAS 서버와 Spring Boot 앱을 다시 한 번 다시 시작합니다. 그런 다음 인증을 위해 새 자격 증명을 사용합니다.

8. 결론

우리는 CAS SSO를 Spring Security와 함께 사용하는 방법과 관련된 많은 구성 파일을 살펴보았습니다. 구성 가능한 CAS SSO의 다른 많은 측면이 있습니다. 테마 및 프로토콜 유형에서 인증 정책에 이르기까지 다양합니다.

이것들과 다른 것들은 문서 에 있습니다 . CAS 서버Spring Boot 앱 의 소스 코드는 GitHub에서 사용할 수 있습니다.

res – Security (video) (cat=Security/Spring Security)