개발놀이터

CAS 인증서버 구축하기 (4) : CAS 클라이언트 생성 본문

Spring/Spring Security

CAS 인증서버 구축하기 (4) : CAS 클라이언트 생성

마늘냄새폴폴 2024. 2. 25. 23:00

클라이언트를 개발하기 전에 한가지 해야할 일이 있습니다. 

 

바로 CAS 인증 서버에 우리가 사용할 서비스를 등록해줘야한다는 것이죠. 

 

우리가 첫번째 시간에 만들었던 services 폴더 기억하시나요? 그곳은 우리가 생성할 서비스들을 관리하는 폴더입니다. 

 

우리는 첫번째 시간에 의존성도 추가했는데 그때 json 기반 registry를 기억하실겁니다. 그것을 이용해 서비스를 json 파일로 등록할 수 있습니다. 

 

프로젝트/etc/cas/services 로 가서 파일을 하나 만들어줍니다. 

 

$ vim casSecuredApp-19991.json

 

 

자 간단하게 설명드리자면 @class는 Regex 즉 정규표현식을 이용해 서비스를 등록하는 레지스트리를 만들겠다는 뜻입니다. 때문에 serviceId에 맨처음 " ^ " 이것을 빼먹어서는 안됩니다. 이것때문에 삽질을 좀 했네요 ㅎㅎ..

 

제일 중요한 것은 @class와 serviceId 그리고 id입니다. 

 

serviceId에는 우리가 사용할 도메인 혹은 퍼블릭IP 그리고 해당 프로젝트의 포트, /login/cas까지 빠짐없이 적어야합니다. 

 

이때 중요한 점은 이 services 내부에 json 파일을 {프로젝트디렉토리}/src/main/resources로 복사해주어야 한다는 것입니다. 만약 /etc/cas/services에만 json파일을 생성한다면 이 json파일을 서버가 읽을 수 없습니다. 

 

이건 잘...

이건 검증해보지 않아서 잘 모르는 부분이긴 합니다만... /etc/cas/config에 가보면 log4j2.xml이 있습니다. config 폴더에 cas.properties를 만들었습니다. 

 

그리고 해당 내용은 다음과 같습니다. 

 

 

이 내용이 필요한 것인지 필요하지 않은 것인지는 모르겠습니다만 오래 걸리는 것도 아니니 혹시 모르니까 하나 만들어주시는 것을 추천드립니다. 

 

 

 

클라이언트 개발

 

이렇게 인증 서버를 사용할 서비스를 등록했습니다. 자 이제 본격적으로 클라이언트를 만들어보도록 하겠습니다. 위의 설정은 CAS 인증 서버에서 진행해주시고 우리는 MySQL을 설치한 새로운 서버로 들어와서 진행해주시면 됩니다. 

 

우리는 Spring Boot 프로젝트를 준비해줘야합니다. 의존성은 web, security를 추가해주세요. 

 

1. 의존성 추가

build.gradle에 다음과 같이 추가해줍니다. 

 

implementation 'org.springframework.security:spring-security-cas'

 

빌드 해주시고 다음과 같이 코딩해주시면 됩니다. 

 

package com.example.firstapplication.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HomeController {

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

 

package com.example.firstapplication.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SecuredController {

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

 

간단한 컨트롤러 두개를 만들었습니다. 하나는 로그인이 필요하지 않은 API, 그리고 나머지 하나는 로그인이 필요한 API입니다. 

 

package com.example.firstapplication.config;

import com.sun.xml.bind.v2.TODO;
import org.jasig.cas.client.validation.Cas30ServiceTicketValidator;
import org.jasig.cas.client.validation.TicketValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.AuthenticationEntryPoint;

@Configuration
public class CassClientConfiguration {

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

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

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

    @Bean
    public CasAuthenticationProvider casAuthenticationProvider() {
        CasAuthenticationProvider provider = new CasAuthenticationProvider();
        provider.setServiceProperties(serviceProperties());
        provider.setTicketValidator(ticketValidator());
        provider.setKey("CAS_PROVIDER_LOCALHOST_9000");
        // UserDetailService 가 반드시 등록되어야 CasAuthenticationProvider 가 정상 작동 한다.
        provider.setUserDetailsService(s -> new User("ks325", "123", AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
        return provider;
    }
}

 

CAS 서버 인증을 위해 사용할 빈들을 만들어줘야합니다. 

 

  • ServiceProperties에는 우리가 만든 서비스의 설정정보를 적어야합니다. 
  • AuthenticationEntryPoint는 설명하자면 길어서 간단하게 설명해드리자면 우리의 CAS 서버를 가리키게 하면 됩니다. 
  • TicketValidator의 구현체로 Cas30ServiceTicketValidator를 설정해주시면 됩니다. 
  • 마지막으로 Provider로 CasAuthenticationProvider를 만들어주면 됩니다. 

 

주석에 쓰여있듯이 UserDetailService를 구현해주셔서 주입해주어야합니다. Security에서 구현하듯이 구현해서 주입해주시면 되겠습니다. 저는 간단하게 테스트용이라 간단하게 만들어보았습니다. 

 

그리고 마지막으로 Security 설정을 하면 끝!

 

package com.example.firstapplication.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.AuthenticationEntryPoint;

import java.util.Arrays;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final AuthenticationEntryPoint authenticationEntryPoint;
    private final ServiceProperties serviceProperties;
    private final 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("/").permitAll()
                .and().httpBasic().authenticationEntryPoint(authenticationEntryPoint);
    }

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

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

 

뭔가 이상하긴 하실겁니다. 왜냐면 스프링 부트 3.x 버전이 아니기 때문입니다. 

 

저희 회사가 2.5.3을 사용하고 있기 때문이기도 하고... 프로젝트를 굳이 3.x 버전으로 하는 건 테스트용으로 적합하지 않다고 판단하여 다운그레이드 하여 사용했습니다. 

 

자 이제 해당 프로젝트를 배포하고 루트 주소로 들어가보면?

 

 

잘 작동합니다. 그리고 /secured로 들어가보면?

 

 

위에 주소를 보시면 특이한 점이 있을겁니다. 바로 서버가 다르다는 것입니다. IP주소도 다르고 포트도 다르네요. 그리고 뒤에 쿼리스트링으로 service=http 불라불라 이렇게 나와있죠. 

 

이 페이지에서 로그인을 마치고 나면 티켓을 발급해주고 해당 페이지로 리다이렉트를 시켜줘야 하기 때문에 저렇게 되어있는 것입니다. 

 

이 페이지가 뜬것만으로도 우리는 해당 서비스가 CAS 서버에 잘 적용되었다는 것을 알 수 있습니다. 

 

여기까지만 하면 거의 성공! 거의 다왔습니다. 이제 로그인을 해보면?

 

 

허허... 이게 뭐죠? 

 

에러 로그는 이렇게 되어있었습니다. 

 

 

위에 더 있는데 핵심은 이겁니다. 구글링해본 결과

 

네... ec2 인스턴스에 https 설정이 안되어있어서 그렇습니다. 

 

다음 포스팅에선 ec2에 https를 설정하고 이어가도록 하겠습니다.