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 컨벤션을 따른다.
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 메소드를 자세히 살펴보자.
그리고 위에서 설명한 AuthenticationEventExecutionPlanConfigurer 인터페이스의 구현체들을 주입 받아서 각각의 구현체들을 DefaultAuthenticationEventExecutionPlan Bean 객체에 설정해주는 작업을 한다.
각각의 AuthenticationEventExecutionPlanConfigurer 구현체들은 DefaultAuthenticationEventExecutionPlan Bean 객체에 어떤 설정을 하는지 configureAuthenticationExecutionPlan 인터페이스의
configureAuthenticationExecutionPlan 메소드를 확인해보자.
쉬운 설명을 위해서 위에서 설명했던 JdbcAuthenticationEventExecutionPlanConfiguration 클래스를 확인해보자.
위에서 설명했던 것처럼 jdbcAuthenticationHandlers() 를 통해 설정된 jdbc 인증처리 handler를 가져온다.
그 다음 파라미터로 받은 DefaultAuthenticationEventExecutionPlan 클래스 인스턴스에handler와 PrincipalResolver를 등록해준다.
그리고 DefaultAuthenticationEventExecutionPlan 클래스를 확인해보자.
노란색 박스 부분을 보면 JdbcAuthenticationEventExecutionPlanConfiguration 에서 호출한 메소드 임을 확인할 수 있다.
그리고 빨간색 부분을 보면 authenticationHandlerPrincipalResolverMap 라는 멤버변수에 handler를 key로 principal을 value로 put 하는 것을 확인할 수 있다.
authenticationHandlerPrincipalResolverMap 변수는 인증처리를 할 때 사용된다.
사용되는 구체적인 코드는 인증 처리 과정에서 자세하게 설명하도록 하겠다.
지금까지 JDBC 인증방식 설정에 대해 알아 보았다.
다음 글에서는 Client Application을 생성하고 CAS 인증서버와 SSO 테스트를 해보겠다.
마지막으로 CasCoreAuthenticationConfiguration 에 대해 간략히 설명하고 마치겠다.
CasCoreAuthenticationConfiguration 설정 클래스는 cas-server-core-authentication 의존성이 추가되어 있어햐 하는데, 첫번째 글에서 언급했던 cas-server-support-json-service-registry 의존성을 추가하면 의존성 전이를 통해 프로젝트에 추가가 된다.
이어서 클라이언트 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
여러가지 요구사항에 대응하고, 여러가지 변수에 대처하기 위해서는 Spring과 Spring Security에 대한 어느정도 지식이 필요하다.
그렇다고 해서 Spring Security에 대한 모든 것을 알아야 하는 것은 아니므로, 어느정도 기반지식이 있다면 SSO 를 구현하면서 함께 습득할 수 있다.
Spring & CAS 동작 순서
여기에서는 요약만 하고 다음 글에서 소스와 함께 설명하겠다.
또한 Proxy에 대한 내용은 포함시키지 않았다.
사용자가 인증이 필요하지 않는 Public 페이지에 접근한다. (이 때는 CAS가 어떤 동작을 취하지 않는다.)
로그인 하지 않은 사용자가 인증이 필요한 페이지에 접근을 하게 되면, Spring Security의 ExceptionTranslationFilter가 AccessDeniedException 또는 AuthenticationException을 탐지한다.
인증이 완료되지 않은 사용자가 secured page에 접근하는 경우에는 AuthenticationException 을 throw 하게 되는데, ExceptionTranslationFilter 는 이 경우에 AuthenticationEntryPoint에 설정되어 있는 메소드를 호출하게 된다. CAS를 사용할 경우에는 CasAuthenticationEntryPoint 클래스가 사용된다.
CasAuthenticationEntryPoint는 사용자 브라우져를 Cas 인증 서버로 redirect 시킨다. redirect 시에 'service' parameter 를 함께 전달하는데, 이것은 현재(당신의) Application의 Spring Security Service로의 callback URL이다. 예)https://cas.server.com/cas/login?service=https://my.application.com/login/cas
CAS 인증서버로 redirection 되면 사용자 이름과 비밀번호를 묻는 화면이 나온다. 인증서버는 사용자 이름과 비밀번호의 유효성 여부를 결정하기 위해 PasswordHandler 또는 AuthenticationHandler(CAS 3.0을 사용할 경우)를 사용한다. (사용자가 이전에 같은 세션 쿠키로 로그인 되어 있다면 다시 로그인 하라는 화면은 표시되지 않는다.)
로그인에 성공하면 CAS는 사용자의 브라우저를 원래 서비스로 redirection 한다. redirect 시에 서비스 티켓을 의미 하는 'ticket' Parameter 를 함께 전달한다. 예)https://my.application.com/login/cas?ticket=ST-0-ER94xMjmn6qha35CQRoz
Application(client) 서버에서는 CasAuthenticationFilter 가 '/login/cas' 로의 요청을 처리하게 되어있다. 처리 필터는 서비스 티켓임을 나타내는 UsernamePasswordAuthenticationToken을 생성한다. 이때 Authentication 의 Principal은 CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER("_cas_stateful_")가 되고 credentials은 CAS 인증 서버에서 받은 'ticket'(서비스티켓)이 값이 된다. 이 인증 요청은 AuthenticationManager에 전달된다.
AuthenticationManager의 구현체는 ProviderManager이며 AuthenticationProvider(CasAuthenticationProvider)로 차례로 구성된다. CasAuthenticationProvider는 CAS 고유의 주체인 CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER 및 CasAuthenticationToken(나중에 설명)을 포함하는 UsernamePasswordAuthenticationToken에만 응답한다.
CasAuthenticationProvider는 TicketValidator의 구현체를 통해 서비스 티켓의 유효성을 검사한다. 일반적으로 CAS 클라이언트의 라이브러리에 포함 된 클래스 중 하나인 Cas20ServicetTicketValdator를 사용하는데, Application이 프록시 티켓의 유효성을 검사해야 하는 경우에는 Cas20ProxyTicketValidator가 사용된다. TicketValidator는 서비스 티켓의 유효성을 검사하기 위해 CAS 인증 서버에 HTTPS 요청을 보낸다. 예)https://cas.server.com/p3/serviceValidate?ticket=ST-1-exZxTmzeBMMcLHd9sJm2-kkt0929&service=https://my.application.com/login/cas
CAS 인증서버에는 유효성 검사 요청을 받게 되고, 요청에 포함된 서비스 티켓이 CAS 서버에서 발급한 서비스 URL과 일치하면 CAS는 사용자 이름을 포함하는 XML형식의 success 응답을 한다.
Application의 Cas20TicketValidator는 CAS 서버에서 받은 XML을 분석한 뒤, 사용자 이름(필수)을 포함한 TicketResponse(Assertion) 를 CasAuthenticationProvider로 리턴한다.
CasAuthenticationProvider는 Assertion에 포함 된 사용자에게 적용되는 GrantedAuthority 를 가져오기 위해 AuthenticationUserDetilasService에 요청한다.
문제가 없는 경우 CasAuthenticationProvider는 TicketResponse 및 GrantedAuthority에 포함 된 세부 정보를 포함하는 CasAuthenticationToken을 생성한다.
그리고 CasAuthenticationFilter로 돌아가서 생성된 CasAuthenticationToken을 Security Context에 저장한다.
사용자의 브라우저는 AuthenticationException을 발생시켰던 원래 페이지로 redirection 된다.
위에 설명한 15가지 순서가 Spring Security와 Cas가 동작하는 순서이다.
각각의 설명은 Application 프로젝트에 대해 글을 작성 할 때 자세하게 다룰 것이다.