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

이어서 클라이언트 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에 있는 계정 정보를 통해 인증하는 방법에 대해서 알아 보도록 하자.

이전 글에서 인증 서버를 로컬에서 실행 시켜봤다.

로그인 화면은 확인 했지만 계정이 존재하지 않기 때문에 로그인이 되지는 않았다.

그리고 HTTPS 로 연결해야 SSO를 제대로 사용할 수 있다고 했다.

 

아래 이미지는 이전 글에서 봤던 로그인 페이지이다.

현재는 로그인이 불가능하며, 노란색 박스를 보면 HTTPS 관련 경고 문구가 있다.

이번 글에서는

  • 로컬 SSL 키 저장소를 생성해서 HTTPS로 인증 서버를 구현해보자.

위 내용에 대해서 작성하겠다.

 

먼저 로그인 화면에서 보여지는 HTTPS 관련 에러 문구를 지워보도록 하자.

 

첫번째!!!!

우선 터미널을 실행시키고 cas-server/src/main/resources/etc/cas 디렉토리로 이동하자.

그리고 다음 명령어를 실행하자.

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

그리고 아래와 같이 입력한다.

** 입력 시 주의사항 **

이름과 성, 조직 이름단위에 대한 정보를 입력할 때에는 인증 서버의 domain을 입력하는 것이 다.

localhost로 테스트 할 것이라면 localhost를 사용해야 한다.

만약 cas.server.com 으로 테스트 하려면 cas.server.com 을 입력하고 hosts 파일(윈도우즈 기준) 을 수정해서 사용하면 된다.

이 작업을 수행하지 않으면 SSL 핸드 쉐이크 중에 오류가 발생할 수 있다!!

 

위 내용을 요약하면 현재 디렉토리에 이름thekeystore 이고,  Passwordchangeit 인 키 저장소를 생성한다.

 

두번째!!!!

다음 단계는 생성 된 저장소를 클라이언트 응용 프로그램에서 사용할 수 있도록 .crt 형식으로 export 하는 것이다.

동일한 디렉토리에서 다음 명령을 실행하여 생성 된 thekeystore 파일을 keystore.crt 로 export 하자.

암호는 동일하게 하자.

keytool -export -alias thekeystore -file thekeystore.crt -keystore thekeystore

디렉토리에 thekeystore.crt 파일이 생성된 것을 확인할 수 있다.

 

이제 export 된 thekeystore.crt를 Java cacerts 키 저장소로 import 하자.

 

아래와 같이 입력하자.

keytool -import -alias thekeystore -storepass changeit -file thekeystore.crt -keystore "C:\Program Files\Java\jdk1.8.0_151\jre\lib\security\cacerts"

 

이 때, Java cacerts 키 저장소의 위치는 클라이언트 Application에서 사용되는 JRE와 동일해야 한다.

 

확인을 해보자.

빨간색 박스로 표시한 파일은 인증서를 import 하기 전 원본 cacerts 파일이다.

파란색 박스로 표시한 파일은 인증서를 import 한 복사본 cacerts 파일이다.

 

몇번 더 추가해보면 알겠지만 파일 크기가 증가한다. 인증서가 추가되는 것이다.

다시 말하면 원본 cacerts 파일을 복사하지 않고 새 파일로 추가하게 되면 해당 JRE를 사용하는 다른 Application이 SSL 핸드쉐이크를 할 때 문제가 생길 수 있다는 말이다.

 

프로젝트를 thekeystore 키 저장소가 있다.

 

그리고 application.properties 에 key 정보를 입력하자.

 

그리고 다시 실행해보자.

 

https://localhost:8443/cas/login

 

이전 글에서 봤던 로그인 화면이랑 조금 달라졌다.

HTTPS 관련 경고 문구가 사라졌다. 그리고 URL을 확인하면 HTTPS 가 적용된 것을 확인할 수 있다.

 

 

추가로..

 

로컬에 설치 되어 있는 Tomcat을 사용할 경우 다음과 같이 적용하면 된다.

 

먼저 설치된 Tomcat 디렉토리 위치에서 conf 디렉토리로 이동한다.

그곳에 위치한 server.xml 파일을 다음과 같이 수정한다.

 

keystoreFile : 이 곳은 생성한 keystore의 위치를 입력하자.

keystorePass : keystore 생성 시 입력했던 암호를 입력하자.

 

그리고 나서 다음과 같이 주석을 하자.

그리고 실행 설정을 추가하자.

**주의 사항**

Windows에서는 application.properties 파일의 server.context-path 설정이 적용되지 않았다.

때문에 Tomcat 설정의 DEPLOYMENT 탭에서 Application context를 /cas로 설정해도, 'https://localhost:8443/cas/login'으로 접근이 가능했다.

하지만 MacOS에서는 두개 모두 적용이 되어서 'https://localhost:8443/cas/cas/login' 으로만 접근이 가능했다.

 

 

다음 글에서는 계정을 추가하는 방법에 대해서 쓰겠다.

SSO CAS Server 샘플 프로젝트를 만들어 보자.

 

https://www.baeldung.com/spring-security-cas-sso

 

CAS SSO With Spring Security | Baeldung

Learn how to integrate the Central Authentication Service (CAS) with Spring Security.

www.baeldung.com

Single Signe On 서비스를 위해서는 최소한 두 개의 프로젝트가 필요하다.

첫번째는 cas-server라고 하는 Spring 기반의 인증 서버이다.

두번째는 하나 이상의 클라이언트 이다.

 

이번 장에서는 인증 서버에 대한 기본적인 설치 및 실행 방법에 대해서만 설명할 것이다.

추가적인 커스터마이징에 대해서는 추후에 쓰도록 하겠다.

 

CAS Server 설치하기

기본적인 CAS 인증서버를 구성하는 것은 매우 간단하다.

결론만 말하자면, github에서 소스를 받아서 실행만 하면 인증서버가 실행된다.

 

Cas Server 프로젝트 받기

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

위에 적어놓은 것처럼 github 에서 바로 clone 해서 사용해도 되지만, 난 zip 파일을 다운받았다.

다운로드 하기 위해서는 아래 링크로 이동해서 진행하자.

https://github.com/apereo/cas-overlay-template

 

그리고 나는 maven을 더 잘 알고 있고 사용하기 편하기 때문에 master 브랜치가 아닌 5.1을 선택했다.

 

원래는 5.3을 선택 했었는데, 의존성을 추가하는 과정에서 exclusion 해야 하는 이슈가 있어서 5.1로 변경했다.

 

5.1 버전

소스 코드를 다운로드 받았고 이제 실행을 하면 되는데 그전에 소스가 어떻게 되어 있는지 확인해보자.

github에 있는 소스를 확인해보면 디렉토리가 두 개가 있는 것을 확인할 수 있다.

그리고 이제 Intellij로 다운로드 받은 소스를 열어보자.

 

다운로드 받은 CAS 인증 서버.

프로젝트를 실행하고 maven reimport(보통 pom.xml 파일이 있으면 maven 프로젝트로 설정된다.) 를 하게 되면 'overlays' 라는 github 사이트에서 확인할 수 없었던 디렉토리(녹색 박스)가 생겨난 것을 확인할 수 있다.

 

Maven Overlay

서버는 Maven(Gradle) War Overlay 스타일을 사용하기 때문에 소스만 다운받으면 바로 실행이 가능하다.

 

War Overlay를 간단하게 설명하면

Overlay는 여러 Web Application에서 공통 리소스를 공유하는데 사용된다.

war 프로젝트의 종속성들은 war 프로젝트 자체에 Overlay 된 artifact를 제외하고 WEB-INF/lib 에 수집된다.

쉽게 이야기 하면...의존성에 있는 lib 들과 공통 리소스를 가져와서 사용하는 것이다.

물론 같은 classpath에 같은 파일이라면 현재 프로젝트가 우선시 된다.

이정도만 알아도 CAS 인증서버를 실행하기에 충분하다.

아래 링크를 가면 자세한 설명이 있다. 난 이곳을 참고했다.

http://maven.apache.org/plugins/maven-war-plugin/overlays.html

 

Apache Maven WAR Plugin – Overlays

Overlays Overlays are used to share common resources across multiple web applications. The dependencies of a WAR project are collected in WEB-INF/lib, except for WAR artifacts which are overlayed on the WAR project itself. Overlays at a glance To demonstra

maven.apache.org

CAS Server 설정하기

의존성 추가

이제 pom.xml에 의존성을 추가 하도록 하자.

추가 될 의존성들은 JSON 구성을 통해서 서비스(Application) 등록을 가능하게 해준다.

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

프로젝트 설정 & SSL 적용

다음은 Application의 포트를 변경하는 등의 설정과 SSL 키 저장소 경로를 설정하는 작업을 해보자.

  1. CAS Server 프로젝트에 'src/main/resources' 폴더를 생성하자.
  2. CAS Server 프로젝트에 'etc' 폴더를 위에서 생성했던 'src/main/resources' 폴더로 복사하자.
  3. 'src/main/resources' 에 application.properties 파일을 생성하고 기본값을 작성해보자.

폴더를 생성하고 application.properties 를 생성했다.

Cas 인증서버 실행

터미널에서 빌드실행을 해보자.

maven에 대해서 어느정도 지식이 있다는 가정하에 진행 하도록 하겠다.

$ mvn package
$ java -jar target/cas.war

실행이 잘 되었다면 브라우져에서 확인해보자.

 

localhost:8443/cas/login 으로 접속하면 위의 화면을 확인할 수 있다.

Port 8443과 Context-path /cas 는 application.properties 에서 설정했다.

 

노란색 박스안에 뭔가 위험하다는 것을 표시하는 메시지가 있는데 해석하면

"지금 너의 인증 서버는 HTTPS가 적용되어 있지 않아서 Single Sign On이 제대로 동작하지 않을거야...SSO가 잘 동작하기 위해서는 반드시 HTTPS를 적용해야 할거야!!!!!"

라고 적혀 있다.

 

안전한 데이터를 주고 받기 위해서는 HTTPS가 반드시 적용되어 있어야 한다는 것은 아마 알고 있을 것이다.

특히나 SSO는 인증과 관련된 데이터를 주고 받는 일이기 때문에 반드시 HTTPS가 정상적으로 적용되어 있어야 한다.

실제로 HTTPS가 잘 못 적용되어 있을 경우에는 Single Sign On(단일 로그인) 은 문제 없이 테스트가 되었지만,

Single Sign Out(단인 로그아웃)은 제대로 동작하지 않았다.

 

현재 로그인 페이지에서는 로그인이 불가능하다.

너무 당연하게도 ID가 없기 때문이다.

 

다음 장에서는 HTTPS를 적용하는 방법과 테스트를 위한 계정을 만들고, 로그인 해보도록 하겠다.

Single Sign On (& Single Sign Out)

CAS를 이용한 SSO 기능구형에 대한 기록을 블로그에 남기기로 했다.

한글로 되어 있는 블로그를 찾기 힘들어서 다른 분들에게 조금이나마 도움이 되었으면 하는 마음에 남긴다.

 

SSO를 왜 써야 할까?

 

우선 시나리오를 만들어보자.


시나리오 : 

웹 솔루션 서비스를 제공하는 회사를 하나 만들었다고 생각해보자.

 

처음에는 하나의 제품만을 서비스 했다.

서비스가 성공적이었고 또 다른 제품을 만들어 서비스 하게 되었다.

서비스가 또 성공적이었고 또 다른 제품을 만들어 서비스 하게 되었다.

서비스가 또또 성공적....

 

이렇게 여러 제품을 만들어 서비스도 잘 되고 있는데 사용자들이 불만을 갖기 시작한다.

"어째서 케넨소프트의 서비스는 한 회사의 제품이고 아이디와 비밀번호가 같은데 매번 로그인 해야하죠?? 너무 불편해요!! (좋아요 23412)"

 

그렇다.

한 회사의 제품이고 아이디와 비밀번호가 같은데 매번 로그인을 수행하는 것은 사용자 입장에서는 참으로 비효율 적이고 불편한 일이다.

로그인 뿐만 아니라 그렇게 각각 로그인 했을 경우 로그아웃도 각각 해줘야 하기 때문에 보안적으로도 취약하다.

그리고 

비밀번호 암호화 알고리즘을 변경해야 하는 일이 생겼다고 생각해보자.

모든 서비스 담당자들은 본인이 담당하고 있는 제품들의 로그인 부분을 수정해야 한다.

로그인 암호화 방식을 변경하는 일이기 때문에 출시 일정을 정해야 하는데 쉽지가 않다.

각자 일정이 잡혀 있기 때문이다.

 

그리고는 생각했다. 

로그인을 한곳에서 하면 어떨까??


위에 적은 시나리오 처럼 여러가지 애플리케이션을 서비스 하게 되면 반드시 필요한 것이 바로

통합인증 이다.

 

SSO 란?

SSO 란 여러가지 애플리케이션 서비스를 하나의 인증서버를 통해서 인증 할 수 있는 기술(또는 프로토콜)을 뜻한다.

 

SSO 장점

  • 한 번의 로그인으로 여러 서비스를 추가적인 로그인 작업을 하지 않고 사용 가능하기 때문에 사용자의 편의성이 향상된다.

  • 로그인 로직이 변경되거나 암호화 방식을 변경해야 할 경우 제품마다 적용할 필요 없이 인증 서버만 수정하면 되기 때문에 효율적이다.

  • 하나의 서비스에서 로그아웃을 할 경우에 다른 서비스들 역시 로그아웃(세션만료) 시킬 수 있기 때문에 보안적인 이점이 있다.

SSO 구현

SSO는 기술 또는 프로토콜을 뜻하고 구현 방법에는 몇가지가 있는데,

나는 SpringSecurity와 CAS(https://www.apereo.org/projects/cas) 를 이용했고 이를 설명할 것이다. 

 

Spring Security와 CAS를 이용한 SSO 에 대한 튜토리얼은 아래 링크를 통해 따라할 수 있다.

 https://www.baeldung.com/spring-security-cas-sso

 

CAS SSO With Spring Security | Baeldung

Learn how to integrate the Central Authentication Service (CAS) with Spring Security.

www.baeldung.com

튜토리얼대로 잘 따라하면 문제 없이 튜토리얼을 실행시킬 수 있을 것이다.

인증 서버 프로젝트, 클라이언트 프로젝트를 각각 생성해서 테스트 해보는 내용이다.

하지만 언제나 그렇듯... 튜토리얼은 튜토리얼이다.

여러가지 요구사항에 대응하고, 여러가지 변수에 대처하기 위해서는 Spring과 Spring Security에 대한 어느정도 지식이 필요하다.

그렇다고 해서 Spring Security에 대한 모든 것을 알아야 하는 것은 아니므로, 어느정도 기반지식이 있다면 SSO 를 구현하면서 함께 습득할 수 있다.

 

Spring & CAS 동작 순서

여기에서는 요약만 하고 다음 글에서 소스와 함께 설명하겠다.

또한 Proxy에 대한 내용은 포함시키지 않았다.

  1. 사용자가 인증이 필요하지 않는 Public 페이지에 접근한다. (이 때는 CAS가 어떤 동작을 취하지 않는다.)

  2. 로그인 하지 않은 사용자가 인증이 필요한 페이지에 접근을 하게 되면,
    Spring Security의 ExceptionTranslationFilterAccessDeniedException 또는 AuthenticationException을 탐지한다.

  3. 인증이 완료되지 않은 사용자가 secured page에 접근하는 경우에는 AuthenticationException 을 throw 하게 되는데, ExceptionTranslationFilter 는 이 경우에 AuthenticationEntryPoint 에 설정되어 있는 메소드를 호출하게 된다.
    CAS를 사용할 경우에는 CasAuthenticationEntryPoint 클래스가 사용된다.
  4. 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
  5. CAS 인증서버로 redirection 되면 사용자 이름과 비밀번호를 묻는 화면이 나온다.
    인증서버는 사용자 이름과 비밀번호의 유효성 여부를 결정하기 위해 PasswordHandler 또는 AuthenticationHandler(CAS 3.0을 사용할 경우)를 사용한다. 
    (사용자가 이전에 같은 세션 쿠키로 로그인 되어 있다면 다시 로그인 하라는 화면은 표시되지 않는다.)
  6. 로그인에 성공하면 CAS는 사용자의 브라우저를 원래 서비스로 redirection 한다.
    redirect 시에 서비스 티켓을 의미 하는 'ticket' Parameter 를 함께 전달한다.
    예)https://my.application.com/login/cas?ticket=ST-0-ER94xMjmn6qha35CQRoz
  7. Application(client) 서버에서는 CasAuthenticationFilter 가 '/login/cas' 로의 요청을 처리하게 되어있다.
    처리 필터는 서비스 티켓임을 나타내는 UsernamePasswordAuthenticationToken을 생성한다. 이때 Authentication 의 Principal은 CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER("_cas_stateful_")가 되고  credentials은 CAS 인증 서버에서 받은 'ticket'(서비스티켓)이 값이 된다.
    이 인증 요청은 AuthenticationManager에 전달된다.
  8. AuthenticationManager의 구현체는 ProviderManager이며 AuthenticationProvider(CasAuthenticationProvider)로 차례로 구성된다.
    CasAuthenticationProvider는 CAS 고유의 주체인 CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER 및 CasAuthenticationToken(나중에 설명)을 포함하는 UsernamePasswordAuthenticationToken에만 응답한다.
  9. CasAuthenticationProviderTicketValidator의 구현체를 통해 서비스 티켓의 유효성을 검사한다. 일반적으로 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
  10. CAS 인증서버에는 유효성 검사 요청을 받게 되고, 요청에 포함된 서비스 티켓이 CAS 서버에서 발급한 서비스 URL과 일치하면 CAS는 사용자 이름을 포함하는 XML형식의 success 응답을 한다.
  11. Application의 Cas20TicketValidator는 CAS 서버에서 받은 XML을 분석한 뒤,
    사용자 이름(필수)을 포함한 TicketResponse(Assertion) 를 CasAuthenticationProvider로 리턴한다.
  12. CasAuthenticationProvider는 Assertion에 포함 된 사용자에게 적용되는 GrantedAuthority 를 가져오기 위해 AuthenticationUserDetilasService에 요청한다.
  13. 문제가 없는 경우 CasAuthenticationProvider는 TicketResponse 및 GrantedAuthority에 포함 된 세부 정보를 포함하는 CasAuthenticationToken을 생성한다.
  14. 그리고 CasAuthenticationFilter로 돌아가서 생성된 CasAuthenticationToken을 Security Context에 저장한다.
  15. 사용자의 브라우저는 AuthenticationException을 발생시켰던 원래 페이지로 redirection 된다.

 

위에 설명한 15가지 순서가 Spring Security와 Cas가 동작하는 순서이다.

각각의 설명은 Application 프로젝트에 대해 글을 작성 할 때 자세하게 다룰 것이다.

 

Spring Security와 CAS 를 사용한 SSO 에 대한 설명은 여기까지만 하고 

실제로 구현해보도록 하자.

서비스 디스커버리

MSA 에서는 여러 서비스 간의 호출로 구성이 된다.
일반적으로 IP와 포트를 통해 호출을 하는데, 클라우드 환경에서는 IP가 동적으로 변경되는 일이 많기 때문에 정확한(유효한) 위치를 알아내는 기능이 필요하다.
서비스 디스커버리가 이 일을 한다.

책에 나오는 Netflix의 Eureka 를 사용해서 구성해 본다.

유레카와의 통합을 위한 스프링 클라우드 라이브러리는 클라이언트서버의 두 부분으로 구성돼 있다.


서버

  • 스프링 부트 애플리케이션으로 실행한다.
  • 서버 API 구성
    • 등록된 서비스의 목록을 수집하기 위한 API
    • 새로운 서비스를 네트워크 위치 주소와 함께 등록하기 위한 API
  • 서버의 상태를 다른 서버로 복제함으로써 안정성과 가용성을 높일 수 있다.

클라이언트

  • 마이크로서비스 애플리케이션에 의존성을 포함시켜 사용한다.
  • 기능
    • 애플리케이션 시작 후 서버에 등록한다.
    • 종료 전 서버에서 등록 해제를 담당한다.
    • 유레카 서버로부터 주기적으로 최신 서비스 목록을 받아온다.

서버 측에서 유레카 서버 실행하기


Spring Initializr 에서 Search for Dependencies 에 'Eureka Server' 를 검색한 뒤 선택하면 'Selected Dependencies'에 추가가 된다.
그리고 'Generate Project'를 하면 프로젝트가 생성된다. 그리고 실행하자.

그럼 프로젝트에 dependency가 추가되어 있다.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

Main Application에 @EnableEurekaServer를 추가해 유레카 서버를 활성화한다.
@SpringBootApplication
@EnableEurekaServer
//@EnableEurekaClient <-- Client 아님요
public class MsaServerApplication {
public static void main(String[] args) {
SpringApplication.run(MsaServerApplication.class);
}
}

eureka-server의 dependency만 추가 하였는데 @EnableEurekaClient 역시 활성화 가능하다.

하지만 이것은 디스커버리 인스턴스를 고가용성 모드로 동작할 경우 디스커버리 인스턴스 사이의 동료 간 통신(peer-to-peer)일 경우에만 유용하다.


이 상태로 실행을 하면, 에러 로그가 많이 찍힐 것이다.(그래도 브라우저에서 확인은 가능하다.)

책에서는 의존성에서 spring-cloud-netflix-eureka-client를 의존성에서 제외 하는것도 하나의 방법이라고 적혀 있는데 그렇게 하면 특정 Bean을 찾지 못해 에러가 나더라..

아무튼 아래와 같이 yml 파일에서 처럼 디스커버리 클라이언트를 비활성화 하는 것이 가장 좋은 방법이다.


server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false

이제 Spring Cloud Application을 시작한다.

그리고 localhost:{port} 로 이동하자.



위 이미지 처럼 Spring Cloud Application에 대한 정보를 확인 할 수 있다.


그리고 /eureka/apps 로 이동하여 서버에 등록된 마이크로서비스의 목록을 확인 가능하다.



이제 Eureka 서버에 등록될 서비스를 만들 차례다.



클라이언트에서 유레카 활성화 하기

클라이언트용 Spring Boot Application을 생성하자.
Spring Initializr에서 검색해보니 Eureka Discovery를 쓰면 될 것 같았다.
Eureka Discovery를 추가하고 프로젝트를 생성하자.


다음과 같이 dependency가 추가 되어 있다는걸 확인할 수 있다.

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

책에서는

spring-cloud-starter-netflix-eureka-client 만 추가되어 있지만, 실제로 client를 실행 시켜보면 등록 즉시 shutdown 되는 것을 확인할 수 있다. 따라서 아래와 같이 dependency를 하나 더 추가해야 한다.

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>


클라이언트는

    1. 자신을 Eureka 서버에 등록하고 호스트, 포트, 상태 정보, URL, 홈페이지 URL을 보낸다.
      • Eureka 서버는 서비스의 각 인스턴스로부터 생존신호(Heartbeat) 메시지를 받는다.
      • 설정된 기간 동안 생존신호 메시지를 받지 못하면 레지스트리에서 서비스가 삭제된다.
    2. 서버로부터 데이터를 가져와서 캐싱하고 주기적으로 변경사항을 점검한다.

유레카 클라이언트 활성화 시키는 방법은
    1. @EnableDiscoveryClient Annotation을 메인 클래스에 추가
    2. 컨설, 유레카, 주키퍼 등 다수의 클라이언트 구현체가 classpath에 있을 경우 @EnableEurekaClient Annotation을 추가
@EnableDiscoveryClient는 spring-cloud-commons에 존재하고,
@EnableEurekaClient는 spring-cloud-netflix에 존재하고 유레카만을 위해 작동한다.


나는 @EnableDiscoveryClient를 추가한다.

@SpringBootApplication
@EnableDiscoveryClient
public class MsaClientApplication {

public static void main(String[] args) {
SpringApplication.run(MsaClientApplication.class, args);
}

}


그리고 yml에 다음과 같이 설정을 한다.

spring:
application:
name: client-service
server:
port: ${PORT:8081}
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/


이제 빌드를 하고 실행을 해보자.


java -jar -DPORT=8081 target/msa-client-0.0.1-SNAPSHOT.jar

java -jar -DPORT=8082 target/msa-client-0.0.1-SNAPSHOT.jar


그리고 나서 아까 실행했던 Server의 대시보드 화면을 다시 확인해보자.


포트 8081, 8082로 실행한 두 개의 client 인스턴스가 존재한다.


종료 시 등록 해제

스프링 엑추에이터의 /shutdown API를 이용하여 애플리케이션을 '우아하게' 중지하기

  1. spring-boot-starter-actuator를 pom.xml 에 추가한다.
  2. 기본으로 비활성화 되어 있기 때문에 속성을 통해 활성화 시킨다.
의존성 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

.yml 설정 (책의 버전이랑 달라서 그런지 아래처럼 해줘야 한다.)

management:
endpoint:
shutdown:
enabled: true
endpoints:
web:
exposure:
include: shutdown,info


이렇게 추가를 한 다음에 다시 클라이언트를 실행한다.


이제 애플리케이션을 종료하려면 /actuator/shutdown API 를 POST 메서드로 호출한다. (포스트맨 등을 사용해서)

문제가 없다면 다음과 같은 Response를 받을 것이다.


{
"message": "Shutting down, bye..."
}

Client의 로그를 확인 해보자.

com.netflix.discovery.DiscoveryClient    : Shutting down DiscoveryClient ...
com.netflix.discovery.DiscoveryClient : Unregistering ...
com.netflix.discovery.DiscoveryClient : DiscoveryClient_CLIENT-SERVICE/kkt0929.MIDASIT.local:client-service:8081 - deregister status: 200
com.netflix.discovery.DiscoveryClient : Completed shut down of DiscoveryClient
o.apache.catalina.core.StandardService : Stopping service [Tomcat]
o.a.c.c.C.[Tomcat].[localhost].[/] : Destroying Spring FrameworkServlet 'dispatcherServlet'


이제 서버 대시보드를 가보면 포트 8082만 남아 있을 것이다.



이 외에도 유레카 서버 대시보드는 등록되고 취소된 서비스 이력을 확인 가능하다.



현실에선 항상 우아할 수 없다.

우아한 종료가 최선의 방법이지만 언제나 그렇듯이 항상 우아한 결과를 얻는 것은 아니다.

예를 들어,
    • 서버 머신이 재시작하거나 애플리케이션의 장애
    • 서버와 클라이언트 간의 네트워크 인터페이스 문제
이를 테스트하기 Intellij 에서 종료를 하고 서버 대시보드에서 확인을 해보면 UP 상태로 남아 있는 것을 확인할 수 있다.


위 이미지 처럼 빨간 박스 안에 있는 버튼을 클릭하면 바로 종료가 된다.
종료하고 유레카 서버 대시보드를 확인해보면 UP 상태로 남아 있다. 한번 해보십셔!

왜 이러는 걸까요??
유레카에는 특별한 메커니즘이 있다.

일반적으로 Eureka 서버에 등록된 instance는 주기적으로 heartbeat를 보내는데, heartbeat를 받지 못하면 서버의 registry에서 instance를 제거 한다. 
하지만 자신의 서비스 등록 상태를 제 시간에 갱신하지 않는 서비스의 수가 일정 수를 넘게 되면 '등록 만료'를 멈춘다.
즉 서버 Registry에서 instance를 제거하지 않는 것이다.

그 이유는 네트워크 장애가 발생했을 때 등록된 모든 서비스가 해제되는 것을 방지하기 위함이다.
그러니까 Eureka로의 네트워크는 단절되었지만, 해당 서비스 자체는 문제가 없는 경우가 있기 때문에 '자기 보존 모드'가 활성화 되어 있으면 registry에서 해당 instance를 정해진 기간 동안 제거하지 않게 된다.
이 메커니즘을 Self-preservation mode(자기 보존 모드)라고 한다.

Self-Preservation-mode 여부는 Expected heartebeats 수와 Actual heartbeats 수를 비교하여 결정된다.

application.yml 의 enableSelfPreservation 속성을 false로 설정하면 '자기 보존 모드'를 비활성화 시킬 수 있다.






하지만 운영 환경에서는 활성화 해야 한다.


프로그램 방식으로 디스커버리 클라이언트 사용하기

Client 애플리케이션이 시작될 때 유레카 서버로부터 등록된 서비스 목록을 가져온다. (자동으로)
그러나 실제로 개발자가 유레카 Client API를 필요로 할 경우가 있을 것이다.

그럴 때 두 가지의 방법이 있다.
    • com.netflix.discovery.EurekaClient
      • 유레카 서버가 노출하는 모든 HTTP API를 구현한다. 유레카 API 영역에 설명돼 있다.
    • org.springframework.cloud.client.discovery.DiscoveryClient
      • 넷플릭스 EurekaClient를 대체하는 스프링 클라우드의 구현체. 이것은 모든 디스커버리 클라이언트용으로 사용하는 간단한 범용 API다. 여기에는 getService와 getInstances의 두 가지 메서드가 있다.

여기에선 DiscoveryClient를 사용한다.
@RestController
public class MainController {
private static Logger LOGGER = LoggerFactory.getLogger(MainController.class);

@Autowired
private DiscoveryClient discoveryClient;

@GetMapping("/ping")
public List<ServiceInstance> ping(){
List<ServiceInstance> instances = discoveryClient.getInstances("client-service");
LOGGER.info("INSTANCES: count={}", instances.size());
instances.stream().forEach(
it -> LOGGER.info("INSTANCE: id={}, port={}", it.getServiceId(), it.getPort()));
return instances;
}
}

여기서 주의해야 할 사항이 있는데 DiscoveryClient의 package는 org.springframework.cloud.client.discovery  이다.

com.netflix.discovery.DiscoveryClient << 이놈으로 하지 말고..


/ping을 확인하면 다음과 같다.


고급 컨피규레이션 설정

유레카의 컨피규레이션 설정은 세 부분으로 나뉜다.

서버

    • 서버의 행동을 재정의 한다.
    • eureka.server.* 를 접두어로 사용하는 모든 속성을 포함한다.
    • 전체 속성 목록 URL

클라이언트

    • 유레카 클라이언트에서 사용할 수 있는 두 가지 속성 중 하나다. 이것은 클라이언트가 레지스트리에서 다른 서비스의 정보를 얻기 위해 질의하는 방법의 컨피규레이션을 담당한다.
    • eureka.client.* 를 접두어로 사용하는 모든 속성을 포함한다.
    • 전체 속성 목록 URL

인스턴스

    • 이것은 포트나 이름 등의 현재 유레카 클라이언트의 행동을 재정의한다.
    • eureka.instance.* 를 접두어로 사용하는 모든 속성을 포함한다.
    • 전체 속성 목록 URL



레지스트리 갱신하기

이전 내용중에 '우아하게' 종료하기를 하면서 '자기 보존 모드(self-preservation-mode)' 가 기억이 나는가?

'self-preservation-mode'를 비활성화 시켜도 여전히 서버가 임대를 취소하는 시간은 오래 걸린다.

                                                                                                                                                                                                                                             

그 이유는

    1. 모든 클라이언트 서비스가 30초(default)마다 서버로 하트비트를 보내기 때문이다.
      • eureka.instance.leaseRenewalIntervalInSeconds 속성으로 설정 가능하다.
      • 서버가 하트비트를 받지 못하면 레지스트리에서 인스턴스를 제거하기 전에 90초를 기다린다.
    2. 등록을 해제해서 인스턴스로 더 이상 트래픽이 가지 못하도록 차단할 수 있다.
      • eureka.instance.leaseExpirationDurationInSeconds 속성으로 설정 가능하다.
      • leaseExpirationDurationInSeconds 에 지정된 기간 동안 heartbeat가 수신되지 않으면 eureka 서버에서 instance를 제거한다.

클라이언트 Configuration

위에 설명했던 두 속성을 작은 값으로 설정해보자.
eureka:
instance:
lease-renewal-interval-in-seconds: 1
lease-expiration-duration-in-seconds: 2

서버 Configuration

서버 측에서도 변경을 해줘야 한다. 
이유는 Evict(퇴거) 이라는 백그라운드 태스크 때문이다.
이것이 하는 일은 클라이언트로부터 하트비트가 계속 수신 되는지 점검하는 일이다.
기본값으러 60초마다 실행되기 때문에 클라이언트에서 설정했던 위에 두 값을 작은 값으로 설정해도 서비스 인스턴스를 제거하는 데 최악의 경우 60초가 걸린다.
이 값은 evictionIntervalTimerInMs 속성으로 설정 가능하며 millisecond 단위다.
eureka:
server:
enable-self-preservation: false
eviction-interval-timer-in-ms: 3000

이제 디스커버리 서버를 다시 실행하고 클라이언트 인스턴스를 다시 실행해보자.

그리고 아까 처럼 클라이언트를 강제 종료한다.

이번에는 비활성화 된 클라이언트가 유레카 서버에서 제거 되는 것을 확인할 수 있다.


다음은 유레카 서버의 로그 일부다.

: Evicting 1 items (expired=1, evictionLimit=1)
: DS: Registry: expired lease for CLIENT-SERVICE/kkt0929.MIDASIT.local:client-service:8081
: Cancelled instance CLIENT-SERVICE/kkt0929.MIDASIT.local:client-service:8081 (replication=false)


이런 속성들을 조작해서 서비스의 등록과 만료에 대한 유지 관리를 정의할 수 있다.

하지만 중요한 것은 정의된 Configuration으로 인해 시스템의 성능에 악영향을 미치지 않도록 하는 것이다.

이런 설정에 민감한 요소에는

    • 부하 분산
    • 게이트웨이
    • 서킷 브레이커
들이 있다.

인스턴스 식별자 변경하기

유레카 서버에 등록된 인스턴스는 이름이 있다.
중요한 것은 각 인스턴스는 서버가 인식할 수 있도록 유일한 ID를 보내야만 한다.
스프링 클라우드 유레카는 다음과 같은 조합으로 식별자를 자동으로 생성한다.
${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:$server.port}}}.
우리는 식별자를 재정의 할 수 있다.
eureka.instance.instanceId 속성을 재정의 하면 된다.
책에서는 클라이언트 Application의 application.yml을 다음과 같이 수정해사 테스트 하고 있다.
server:
port: 808${SEQUENCE_NO}
eureka:
instance:
instance-id: ${spring.application.name}-${SEQUENCE_NO}

그리고 실행을 해보자. 한...3개정도 해보자...

예) 

java -jar -DSEQUENCE_NO=1 target/msa-client-0.0.1-SNAPSHOT.jar

java -jar -DSEQUENCE_NO=2 target/msa-client-0.0.1-SNAPSHOT.jar

java -jar -DSEQUENCE_NO=3 target/msa-client-0.0.1-SNAPSHOT.jar


Eureka 대시보드를 확인해보면

설정한대로 instanceId 가 만들어졌고 실행이 되었다. 유레카 서버에 잘 나온다.



IP 주소 우선하기

마이크로서비스 환경을 구성하는 데에는 여러 서버를 위한 DNS가 없는 것이 일반적이라고 한다. 그렇겠지..
이 경우 모든 리눅스 머신의 /etc/hosts 파일에 호스트명과 IP 주소를 추가하는 것이 있는데 불편하단다. 그렇겠지..
이를 위해 유레카의 등록 절차 설정 시 호스트명 대신 서비스의 IP 주소를 사용하는 방법이 있단다. 오호..
eureka.instance.preferIpAddress 속성을 true로 설정하면 서버 대시보드의 서비스 인스턴스는 호스틈여을 담은 instanceId를 사용하지만, 링크를 클릭하면 IP주소 기반으로 redirect 된다고 한다.

그런데 여기엔 문제가 있다. 무엇인고 하면..
한대의 서버에 하나 이상의 네트워크 인터페이스가 있는 경우 발생한다.
예를 들어,
    • eth01 : 내부 네트워크
    • eht01 : 공인 IP
로 연결이 되어 있을 경우, 서버는 다른 IP 접두사를 갖는 두 개의 네트워크 인터페이스를 갖게 된단다.
아래 이미지 거다. 근데 여긴 eth0만 있네


암튼.. 이럴 때는 네트워크 인터페이스를 선택해야 한다. 

application.yml 에 무시할 패턴의 목록을 정의 하면 된다.

spring:
cloud:
inetutils:
ignored-interfaces:
- eth1*
또는 원하는 네트워크 주소를 정의하는 방법도 있다.
spring:
cloud:
inetutils:
preferred-networks:
- 192.168


응답 캐시 

서버

유레카 서버는 기본적으로 응답을 캐시한다. 그리고 30초마다 캐시 데이터를 지운다.


유레카 서버를 실행하고 /eureka/apps API를 호출해보자. 아무것도 안나온다.

그리고 클라이언트 인스턴스를 실행 하고 서버의 /eureka/apps를 다시 호출해보자. 여전히 안나온다.

30초가 지나고 나서 다시 호출해보자 추가된 클라이언트 인스턴스가 조회될 것이다.


Cache Timeout 은 reponseCacheUpdateIntervalMs 속성으로 재정의 가능하다.

eureka:
server:
response-cache-update-interval-ms: 3000

재미있는 것은 REST API 와는 반대로 유레카 서버의 대시보드에 등록된 인스턴스가 표실될 때에는 캐시를 사용하지 않는다.


클라이언트

클라이언트 측에서도 유레카 레지스트리를 캐싱을 한다.
서버에서 Cache Timeout을 변경해도 클라이언트에서 갱신되는 데에는 시간이 걸린다.
서버의 Registry는 30초마다 실행되는 백그라운드 태스크에 의해 비동기로 갱신이 되기 때문이다.
registryFetchIntervalSeconds 속성으로 변경 가능하다.

shouldDisableDelta 속성을 통해 마지막으로 시도한 값에서 변경된 내용만 가져오도록 할 수 있다.
shouldDisableDelta을 false로 지정할 수 있지만 이것은 대역폭 낭비다.


Client의 registryFetchIntervalSeconds 을 3초로 변경해보자.

책에서는 shouldDisableDelta 로 되어 있지만 내가 사용하는 버전에는 아래와 같이 disable-delta로 된다.

eureka:
client:
registry-fetch-interval-seconds: 3
disable-delta: true


클라이언트와 서버 간의 보안 통신 사용학

개발중에는 보안이 크게 필요하지 않지만 그래도 예제에서도 하고 하니 나도 한다.
보안을 위해 Spring Security를 적용하자.

또 추가하자.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>


아래 취소선으로 되어 있는 내용은 책의 내용이다.

이렇게 해도 된다. 하지만 난 이렇게 안할거다.


취소선 내용은 건너뛰자.


그리고 yml 에도 또 추가하자.

spring:
security:
user:
name: admin
password: admin123

책에서는 security가 root 지만 옛날 버전간다. spring이 루트다.

그리고 책에서는 basic.enabled 가 있지만 deprecated 된 속성이다.


뭐 이렇게만 추가한 뒤에 Eureka 서버를 다시 실행해보자. 그럼 로그인을 해야 할 것이다.

yml 파일에 정의 한 대로 Username Password 를 입력하고 로그인 하자. 

그럼 대시보드로 잘 들어갈거다.


이제 클라이언트 에서 인증된 상태로 서버에 접근 하려면 다음 설정과 같이 URL 연결 주소에 자격 증명을 제공한다.

eureka:
client:
serviceUrl:
defaultZone: http://admin:admin123@localhost:8761/eureka/


yml에 설정하지 말고 Security 설정을 JavaConfig로 하자.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
PasswordEncoder passwordEncoder;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password(passwordEncoder.encode("admin123"))
.roles("SYSTEM");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/login", "/logout", "favicon.ico").permitAll()
.and().authorizeRequests().anyRequest().authenticated()
.and().httpBasic().and().formLogin()
.and().csrf().disable();
}
}

사용자 인증은 inMemory를 사용하자. passwordEncoder를 이용하는데 Bcrypt를 사용 할 것이다.


그리고 configure 에서는 인증 방식에 대한 내용을 정의하자.

authorizeRequests().antMatchers("/login", "/logout", "favicon.ico").permitAll()

"login", "/logout", "/favicon.ico" 얘네들은 인증이 필요 없이 접근 가능하다. 말 그대로 전부 허용이다.

authorizeRequests().anyRequest().authenticated()

그 외에는 다 인증이 필요하게 설정했다. 

httpBasic()

httpBasic 을 추가 하면 BasicAuthenticationFilter(Basic Auth) 가 Spring Security Filter Chain에 추가가 되는데 

filter 내용은 다음과 같다. 필요한 부분만 몇줄만 적는다.


@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
final boolean debug = this.logger.isDebugEnabled();

String header = request.getHeader("Authorization");

if (header == null || !header.toLowerCase().startsWith("basic ")) {
chain.doFilter(request, response);
return;
}

~~~~~~~~~~~~~~~~~~블라블라

request 객체의 Header 정보중에 Authorization 을 가져온다.

가져와서 Null 이거나 Authorization으로 시작 되는 것이 아니면 Filter Chain으로 넘긴다.


Authorization 헤더 정보가 존재하면 다음과 같이 진행된다.

String[] tokens = extractAndDecodeHeader(header, request);
assert tokens.length == 2;

String username = tokens[0];

if (debug) {
this.logger
.debug("Basic Authentication Authorization header found for user '"
+ username + "'");
}

if (authenticationIsRequired(username)) {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, tokens[1]);
authRequest.setDetails(
this.authenticationDetailsSource.buildDetails(request));
Authentication authResult = this.authenticationManager
.authenticate(authRequest);

if (debug) {
this.logger.debug("Authentication success: " + authResult);
}

SecurityContextHolder.getContext().setAuthentication(authResult);

this.rememberMeServices.loginSuccess(request, response, authResult);

onSuccessfulAuthentication(request, response, authResult);
}

요약하면 인증을 할 필요가 있는지 체크를 하고 필요하면 Authorization 정보를 가지고 인증을 완료한다.


Authorization 에 어떤 정보가 있는가 하면....이것도 요약하면...

사용자 username과 password 를 가지고 base64로 인코딩 한 다음 앞에 Basic+공백+{인코딩값} 으로 만들어서

그 전체를 Authorization header에 추가해서 요청을 보내는 것이다. 

예) admin:admin123 = Basic YWRtaW46YWRtaW4xMjM=

이야기가 나온김에 조금 더 하면 Postman에서 테스트 해볼수 있다.




요청을 보내기 전에 인증 타입을 Basic Auth 로 선택하고 Username, Password 를 입력하자. 그리고 요청을 보내보자.


그리고 나서 Headers 탭으로 이동해 보면, Authorization Header에 Basic YWRtaW46YWRtaW4xMjM= 이 추가된 것을 확인 할 수 있다.


또 다른 방법이 있는데 다음과 같이 url에 username과 password 를 포함 시키는 것이다.


이렇게 보내면 Basic Auth 방식으로 요청을 보내게 된다.

잠시 후 설명할 Client 에서 이 방식을 사용한다.


갑자기 이야기가 산으로 갔지만 괜찮다. 내가 블로그 주인이니까.

formLogin()

그리고 이건 formLogin을 사용하겠다라는 이야기다.

eureka Server에 로그인 하는 방식이다.


아무튼 SecurityConfig를 만들고 나면 passwordEncoder 에 빨간줄이 생길 것이다.

왜냐하면 Bean으로 추가하지 않았기 때문이다.


PasswordEncoder를  Bean으로 등록하자.

@SpringBootApplication
@EnableEurekaServer
public class MsaServerApplication {
public static void main(String[] args) {
SpringApplication.run(MsaServerApplication.class);
}

@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}

이제 실행해보자 그럼 아래처럼 나올 것이다.



yml 파일에 정의 한 대로 Username 과 Password 를 입력하고 로그인 하자. 

그럼 대시보드로 잘 들어갈거다.


이제 클라이언트 에서 인증된 상태로 서버에 접근 하려면 다음 설정과 같이 URL 연결 주소에 자격 증명을 제공한다.

eureka:
client:
serviceUrl:
defaultZone: http://admin:admin123@localhost:8761/eureka/


이렇게 클라이언트에서 serviceUrl의 defaultZone만 변경하면 이제 기본적인 인증이 추가 된 초간단 MSA 샘플이 되었다.


'멍멍이' 카테고리의 다른 글

말을 알아듣는 기언  (0) 2019.01.25
잠자고 싶다.  (0) 2019.01.23
애국견  (0) 2019.01.23
약육강식  (1) 2019.01.23

API 문서화

API를 개발 하고 나면 문서화를 해야 한다.
문서화는 작업 했던 것을 기록하는것 뿐만 아니라 함께 작업하는 개발자들에게 공유하는 내용이기도 하다.
회사마다, 사람마다 API 문서화 방식이 다르기 때문에 잘 맞는 것을 찾아서 사용하면 된다.

특히 공유가 되는 내용이라면 코드의 수정이 생겼을 경우 API 문서 역시 수정을 해줘야 한다.
예를들어, Parameter가 String 에서 Integer로 바꼈을 경우에 API 문서 역시 수정을 해야 한다.
하지만 생각보다 API문서의 수정작업을 잊는 일이 많이 발생한다.

그래서 이 책에선 이런 문제를 해결하기 위한 여러가지 프로젝트 중에서 스웨거2를 소개한다.

스웨거 2를 스프링 부트와 같이 사용하기

의존성 추가

스웨거2를 스프링 부트에서 사용하려면 아래 2개의 의존성을 추가해야 한다.

<!--Swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.2.2</version>
</dependency>


라고 책에 나와 있지만 이후 작업을 위해서는 하나가 더 필요하다.

<!--Maven Model-->
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-model</artifactId>
<version>2.2.0</version>
</dependency>


애플케이션이 시작될 때 스웨거 라이브러리에 의해 API 문서가 소스코드로부터 자동으로 생성된다고 한다.

이 처리는 메인 클래스에 선언된 Docket 빈에 의해 조절 되는데 Docket을 사용하기 위해서는 maven-model dependency 가 필요하다.


Docket Bean 등록

그리고 다음과 같이 SwaggerConfig를 @Configuration, @EnableSwagger2 애노테이션을 추가하여 등록한다.

@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build();
}
}

Docket Bean을 추가했다.

select() 메소드는 ApiSelectorBuilder를 리턴하는데 이것은 스웨거에 의해 노출되는 endpoint를 제어하는 방법을 제공한다.

RequestHandlersSelectors.any(), PathSelectors.any() 로 지정을 하면 전체 API가 스웨거를 통해 문서화 된다.


실행 화면

서버를 실행한다.

그리고 나서 http://localhost:8080/swagger-ui.html 로 이동하면 swagger 화면이 뜰 것이다.




특정 패키지에 있는 API만 문서화 하고 싶다면 아래 코드처럼 설정하면 된다.

.apis(RequestHandlerSelectors.basePackage("com.midasit.msa.controller"))


basic-error-controller 가 사라진 것을 확인할 수 있다.



특정 메소드만 문서화 하고 싶담녀 아래 코드처럼 설정하면 된다.

.apis(RequestHandlerSelectors.basePackage("com.midasit.msa.controller"))
.paths(PathSelectors.ant("/user/name"))



user-controller의 경로가 '/user/name' 만 문서화 된 것을 확인할 수 있다.



API 항목을 클릭해서 자세하게 확인해 보도록 하자.



'/user/name' 을 클릭하면 위의 이미지 처럼 상세 화면이 나온다.

파라미터를 입력할 수 있고, Try it out! 을 통해 요청을 보낼 수 있다. 그리 하면 요청정보와 응답정보를 확인할 수 있다.

매우 간단하면서도 매우 편리하다.


더 자세한 내용은 https://www.baeldung.com/swagger-2-documentation-for-spring-rest-api 이곳을 참고하면 된다.

+ Recent posts