Ch02 Hello Spring Security

2024. 2. 25. 14:25Spring Security

spilca4-main-code (2).zip
2.04MB

 

이 장에서는 다음을 다룹니다.

  • Spring Security를 사용한 첫 번째 프로젝트 생성
  • 인증 및 권한 부여를 위한 기본 구성 요소를 사용하여 간단한 기능 설계
  • 기본 개념 및 특정 프로젝트에서의 사용 방법
  • 기본 인터페이스를 적용하고 이들이 어떻게 관련되는지 이해하기
  • 주요 책임에 대한 사용자 정의 구현 작성[커스터마이징]
  • Spring Boot의 기본 Spring Security 구성 재정의

 

Spring Boot는 Spring Framework를 사용한 애플리케이션 개발의 진화 단계로 나타났습니다. 모든 구성을 작성할 필요 없이 Spring Boot에는 일부 사전 구성된 구성이 함께 제공되므로 구현과 일치하지 않는 구성만 재정의할 수 있습니다. 우리는 이 접근 방식을 Convention over Configuration 이라고도 합니다. Spring Boot는 더 이상 새로운 개념이 아니며 오늘날 우리는 Spring Boot의 세 번째 버전을 사용하여 애플리케이션을 작성하는 것을 즐깁니다.


자동 구성(Convention over Configuration):
Spring Boot의 가장 큰 특징 중 하나는 자동 구성입니다. 이는 개발자가 모든 설정을 처음부터 끝까지 직접 작성할 필요가 없다는 의미입니다. 대신, Spring Boot는 애플리케이션을 개발할 때 필요한 대부분의 설정을 사전에 구성해 두고 제공합니다. 예를 들어, 데이터베이스 연결이나 보안 설정 등이 자동으로 설정될 수 있습니다.

개발자는 기본적으로 제공되는 설정을 그대로 사용할 수 있고, 필요한 부분만 선택적으로 변경하여 사용할 수 있습니다. 이런 방식을 '컨벤션 오버 구성'(Convention over Configuration)이라고 하는데, 이는 개발자가 규칙을 따르는 한, 복잡한 설정 없이도 애플리케이션을 효과적으로 개발할 수 있게 해 줍니다.


 

Spring Boot 이전에 개발자는 자신이 만들어야 하는 모든 앱에 대해 수십 줄의 코드를 반복해서 작성했습니다. 과거에는 대부분의 아키텍처를 모놀리식으로 개발했을 때 이러한 상황이 눈에 띄지 않았습니다. 모놀리식 아키텍처를 사용하면 처음에 이러한 구성을 한 번만 작성하면 되고 나중에는 거의 건드릴 필요가 없습니다. 서비스 지향 소프트웨어 아키텍처가 발전하면서 우리는 각 서비스를 구성하기 위해 작성해야 하는 상용구 코드에 대한 어려움을 느끼기 시작했습니다.
이런 부분이 재미있다면 Willie Wheeler가 Joshua White와 함께 쓴 Spring in Practice(Manning, 2013)의 3장을 확인해 보세요. 이전 책의 이 장에서는 Spring 3을 사용하여 웹 애플리케이션을 작성하는 방법을 설명합니다. 이렇게 하면 하나의 작은 한 페이지 웹 애플리케이션에 대해 얼마나 많은 구성을 작성해야 했는지 이해할 수 있습니다. 해당 장의 링크는 다음과 같습니다: https://livebook.manning.com/book/spring-in-practice/chapter-3/


위에서 언급한 이런 부분이 재미있다면이라는 부분은 만약 Spring과 관련된 개발 방식의 변화와 그 과정에서의 기술적 도전에 대해 더 깊이 이해하고 싶다면 추천하는 참고 자료나 깊이 있는 학습을 할 기회를 제안하는 것입니다.

이러한 제안은 독자가 Spring Framework와 관련된 더 전문적인 내용을 탐구하고 싶을 때 유용합니다. 특히 "Spring in Practice"의 3장에서는 Spring 3을 사용하여 웹 애플리케이션을 구축하는 과정에서 필요한 구성의 복잡성과 상용구 코드의 양을 예로 들어 설명하고 있기 때문에, 이전 방식과 비교했을 때 Spring Boot의 간소화된 접근 방식의 가치를 더 잘 이해할 수 있습니다.


 

Chapter 3. Building web applications with Spring Web MVC · Spring in Practice

Creating your first Spring Web MVC application · Serving and processing forms · Configuring Spring Web MVC · Spring Mobile technology preview

livebook.manning.com

이러한 이유로, 최근 앱과 특히 마이크로서비스용 앱의 개발로 인해 Spring Boot는 점점 더 인기를 얻게 되었습니다. Spring Boot는 프로젝트에 대한 자동 구성[Auto Configuration]을 제공하며 구성에 필요한 시간을 단축시킵니다. 저는 이것이 오늘날 소프트웨어 개발에 적합한 철학을 가지고 있다고 말하겠습니다. 이 장에서는 Spring Security를 사용하는 첫 번째 애플리케이션으로 시작할 것입니다. Spring Framework로 개발하는 앱에 대해, Spring Security는 애플리케이션 레벨의 보안을 구현하기 위한 탁월한 선택입니다. 우리는 Spring Boot를 사용하고, 관례에 의해 구성된 디폴트값과 이러한 디폴트 값을 재정의하는 간단한 소개를 논의할 것입니다. 기본 설정을 고려하는 것은 Spring Security에 대한 훌륭한 소개를 제공하며, 인증 개념을 설명하는 데에도 도움이 됩니다.

 

첫 번째 프로젝트를 시작하면, 우리는 인증에 대한 다양한 옵션을 더 자세히 논의할 것입니다. 3장부터 6장까지, 우리는 이 첫 번째 예제에서 볼 수 있는 각기 다른 책임에 대한 더 구체적인 구성을 계속할 것입니다. 또한, 아키텍처 스타일에 따라 이러한 구성을 적용하는 다양한 방법을 볼 수 있을 것입니다. 현재 장에서 접근할 단계는 다음과 같습니다:
1. Spring Security와 Web 디펜던시만 있는 프로젝트를 생성하여, 어떠한 구성도 추가하지 않았을 때 어떻게 동작하는지 확인합니다. 이를 통해, 인증 및 권한 부여에 대한 기본 구성에서 기대해야 할 사항을 이해하게 됩니다.
2. 이러한 디폴트 값을 재정의하여 사용자 정의 사용자와 비밀번호를 정의함으로써 사용자 관리 기능을 프로젝트에 추가합니다.
3. 애플리케이션이 기본적으로 모든 엔드포인트를 인증한다는 것을 관찰한 후, 이 또한 사용자 지정될 수 있다는 것을 배웁니다.
4. 동일한 구성에 대해 다른 스타일을 적용하여 최선의 방법을 이해합니다.

 

 

2.1 첫 번째 프로젝트 시작하기

첫 번째 예제를 위해 작업할 첫 번째 프로젝트를 생성합시다. 이 프로젝트는 작은 웹 애플리케이션으로, REST 엔드포인트를 노출합니다. 많은 작업을 하지 않아도, Spring Security가 HTTP Basic Authentication[인증]을 사용하여 이 엔드포인트를 보호하는 방법을 볼 수 있습니다. HTTP Basic은 웹 앱이 HTTP 요청의 헤더에서 얻은 자격 증명[Credential](사용자 이름과 비밀번호) 세트를 통해 사용자를 인증하는 방법입니다.

프로젝트를 생성하고 올바른 종속성을 추가함으로써, Spring Boot는 애플리케이션을 시작할 때 사용자 이름과 비밀번호를 포함한 기본 구성을 적용합니다.

참고로, Spring Boot 프로젝트를 생성하기 위한 다양한 대안이 있습니다. 일부 개발 환경에서는 프로젝트를 직접 생성할 수 있는 기능을 제공합니다. Spring Boot 프로젝트 생성에 도움이 필요하다면, 부록에서 여러 가지 방법을 찾을 수 있습니다. 더 자세한 내용을 원한다면, Mark Heckler의 Spring Boot Up & Running(O'Reilly Media, 2021)과 Somnath Musib의 Spring Boot in Practice(Manning, 2022)를 추천합니다. 또는 제가 쓴 또 다른 책인 Spring Start Here(Manning, 2021)도 좋습니다.

 

이 책의 예제들은 책의 동반 소스 코드를 참조합니다. 각 예제와 함께, pom.xml 파일에 추가해야 할 종속성을 명시합니다. 권장하는 바이지만, 책과 함께 제공되는 프로젝트와 사용 가능한 소스 코드를 https://www.manning.com/downloads/2105 에서 다운로드할 수 있습니다. 프로젝트는 무엇인가에 막혔을 때 도움이 될 것입니다. 또한, 이들을 사용하여 최종 솔루션을 검증할 수도 있습니다.

 

첫 번째 프로젝트는 가장 작은 프로젝트입니다. 언급했듯이, 이것은 호출한 후 그림 2.1에서 설명된 대로 응답을 받을 수 있는 REST 엔드포인트를 노출하는 간단한 애플리케이션입니다. 이 프로젝트는 Spring Security와 Spring Boot를 사용하여 애플리케이션을 개발할 때 첫 단계를 배우기에 충분합니다. 이것은 인증 및 권한 부여를 위한 Spring Security 아키텍처의 기본을 제시합니다.

 

 

그림 2.1 우리의 첫 번째 애플리케이션은 HTTP Basic을 사용하여 엔드포인트에 대한 사용자의 인증 및 권한 부여를 수행합니다. 애플리케이션은 정의된 경로(/hello)에서 REST 엔드포인트를 노출합니다. 성공적인 호출의 경우, 응답은 HTTP 200 상태 메시지와 본문을 반환합니다. 이 예제는 Spring Security와 함께 기본적으로 구성된 인증 및 권한 부여가 어떻게 작동하는지 보여줍니다.

 

Spring Security 학습을 시작하기 위해 빈 프로젝트를 생성하고 그것에 'ssia-ch2-ex1'이라는 이름을 붙입니다. (이 예제는 책과 함께 제공되는 프로젝트에서도 동일한 이름으로 찾을 수 있습니다.) 첫 번째 프로젝트에 필요한 유일한 종속성은 spring-boot-starter-web과 spring-boot-starter-security입니다. 이는 Listing 2.1에서 보여줍니다. 프로젝트를 생성한 후, 이러한 종속성을 pom.xml 파일에 추가했는지 확인하세요.

Listing 2.1 Spring Security dependencies for our first web app

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


이 프로젝트를 진행하는 주요 목적은 디폴트 Spring Security로 구성된 애플리케이션의 동작을 확인하는 것입니다. 또한, 이 디폴트 구성의 일부인 컴포넌트와 그 목적을 이해하고자 합니다.

 

이제 바로 애플리케이션을 시작할 수 있습니다. Spring Boot는 프로젝트에 추가한 종속성을 바탕으로 우리를 위해 Spring 컨텍스트의 디폴트 구성을 적용합니다. 하지만, 보안에 대해 많이 배우지 못할 것입니다, 적어도 하나의 보안된 엔드포인트가 없다면 말이죠. 간단한 엔드포인트를 생성하고 호출해 보아서 무슨 일이 일어나는지 확인합시다. 이를 위해, 빈 프로젝트에 클래스를 추가하고 이 클래스를 HelloController라고 이름짓습니다. 이를 위해, Spring Boot 프로젝트의 메인 네임스페이스 어딘가에 controllers라는 패키지 안에 클래스를 추가합니다.

 

참고로, Spring Boot는 @SpringBootApplication으로 어노테이션이 적용된 클래스가 포함된 패키지(및 그 하위 패키지) 내의 컴포넌트만 스캔합니다. 만약 메인 패키지 외부에서 Spring의 스테레오타입 컴포넌트 중 하나로, 어떤 클래스에 이러한 어노테이션들을 적용할 경우, @ComponentScan 어노테이션을 사용하여 위치를 명시적으로 선언해야 합니다.

 

다음 Listing에서, HelloController 클래스는 우리 예제를 위한 REST 컨트롤러와 REST 엔드포인트를 정의합니다.

Listing 2.2 The HelloController class and a REST endpoint

@RestController
public class HelloController {
 @GetMapping("/hello")
 public String hello() {
 return "Hello!";
 }
}

 

@RestController 어노테이션은 스프링 컨텍스트에 빈을 등록하고 애플리케이션이 이 인스턴스를 웹 컨트롤러로 사용한다는 것을 Spring에 알립니다. 또한, 이 어노테이션은 애플리케이션이 HTTP 응답의 응답 본문을 이 클래스의 엔드 포인트 메소드의 반환 값에서 설정해야 한다고 명시합니다. @GetMapping 어노테이션은 GET 요청을 통해 구현된 메소드[Http Method]를 /hello 경로에 매핑합니다. 애플리케이션을 실행하면, 콘솔의 출력 라인들 외에도 다음과 유사한 출력을 볼 수 있어야 합니다:

 

 

Using generated security password: a753068f-6697-4535-88bb-3fdab495d9b0

 

애플리케이션을 실행할 때마다 새로운 비밀번호를 생성하고 이전 코드 스니펫에서 제시된 것처럼 콘솔에 이 비밀번호를 출력합니다. 이 비밀번호를 사용하여 HTTP Basic 인증으로 애플리케이션의 특정 엔드포인트를 호출해야 합니다. 

 

로그인이 정상적으로 수행이 된다면 다음과 같은 에러 웹 페이지를 확인할 수 있습니다.

현재는 / 에 대한 처리를 담당하는 컨트롤러 핸들러 메서드를 정의하지 않았기 때문입니다.

 

먼저, Authorization 헤더를 사용하지 않고 엔드포인트를 호출해 보겠습니다:

curl http://localhost:8080/hello

 

참고로, 이 책에서는 모든 예제에서 엔드포인트를 호출하기 위해 cURL을 사용합니다. 저는 cURL이 가장 읽기 쉬운 솔루션이라고 생각합니다. 하지만 원한다면, 선호하는 도구를 사용할 수 있습니다. 예를 들어, 더 편안한 그래픽 인터페이스를 원할 수 있습니다. 이 경우, Postman이 훌륭한 선택입니다. 사용하는 운영 체제에 이러한 도구들 중 어느 것도 설치되어 있지 않다면, 아마도 직접 설치해야 할 것입니다.

 

그리고 호출에 대한 응답은 다음과 같습니다:

{
  "status": 401,
  "error": "Unauthorized",
  "message": "Unauthorized",
  "path": "/hello"
}


응답 상태는 HTTP 401 Unauthorized입니다. 우리는 적절한 인증 자격증명[ credentials ]을 사용하지 않았기 때문에 이 결과를 예상했습니다. 기본적으로, Spring Security는 디폴트 사용자 이름(user)과 제공된 비밀번호(제 경우에는 93a01로 시작하는 것)를 예상합니다. 이제 적절한 자격증명[credentials]으로 다시 시도해 봅시다:

curl -u user:a753068f-6697-4535-88bb-3fdab495d9b0 
http://localhost:8080/hello

 

이번  호출은 다음과 같습니다

Hello!

 

HTTP 401 Unauthorized 상태 코드는 약간 모호합니다. 일반적으로, 이것은 권한 부여보다는 인증 실패를 나타내는 데 사용됩니다. 개발자들은 자격 증명이 누락되었거나 잘못된 경우와 같은 애플리케이션 설계에서 이를 사용합니다. 권한 부여가 실패한 경우, 우리는 아마도 403 Forbidden 상태를 사용할 것입니다. 일반적으로, HTTP 403은 서버가 요청의 호출자를 식별했지만, 그들이 시도하고 있는 호출에 필요한 권한이 없다는 것을 의미합니다.

 

올바른 자격증명을 전송하면, Http response 본문에서 앞서 정의한 HelloController 메소드가 정확히 무엇을 반환하는지 볼 수 있습니다.


HTTP BASIC 인증을 사용하여 엔드포인트 호출하기

cURL에서는 -u 플래그로 HTTP Basic 사용자 이름과 비밀번호를 설정할 수 있습니다. 내부적으로, cURL은 <username>:<password> 문자열을 Base64로 인코딩하고 이를 Basic 문자열이 접두어인 Authorization 헤더의 값으로 전송합니다. 그리고 cURL을 사용할 때 -u 플래그를 사용하는 것이 더 쉬울 수 있지만, 실제 요청이 어떤 모습인지 아는 것도 중요합니다. 그러니 한번 시도해 보고 Authorization 헤더를 수동으로 생성해 보겠습니다.

첫 번째 단계에서, <username>:<password> 문자열을 가져와 Base64로 인코딩합니다. 애플리케이션이 호출을 보낼 때, Authorization 헤더의 올바른 값을 어떻게 형성하는지 알아야 합니다. 이 작업은 리눅스 콘솔에서 Base64 도구를 사용하여 수행합니다. https://www.base64encode.org와 같이 Base64로 문자열을 인코딩하는 웹 페이지를 찾을 수도 있습니다. 이 스니펫은 리눅스 또는 Git Bash 콘솔에서의 명령어를 보여줍니다(-n 파라미터는 끝에 new line을 추가하지 않음을 의미합니다):

echo -n user:a753068f-6697-4535-88bb-3fdab495d9b0 | base64

 

이 명령어를 실행하면 다음과 같은 Base64 인코딩된 문자열을 반환합니다:

dXNlcjphNzUzMDY4Zi02Njk3LTQ1MzUtODhiYi0zZmRhYjQ5NWQ5YjA=

 

이제 Base64 인코딩된 값을 request의 Authorization 헤더 값으로 사용할 수 있습니다. 이 호출은 -u 옵션을 사용하는 것과 동일한 결과를 생성해야 합니다:

curl -H "Authorization: Basic 
dXNlcjo5M2EwMWNmMC03OTRiLTRiOTgtODZlZi01
[CA]NDg2MGYzNmY3ZjM=" localhost:8080/hello

 

위 커맨드를 통해 생성되는 Http Request Header 내용은 다음과 같습니다.

GET /hello HTTP/1.1
Host: localhost:8080
Authorization: Basic dXNlcjo5M2EwMWNmMC03OTRiLTRiOTgtODZlZi01NDg2MGYzNmY3ZjM=
User-Agent: curl/7.68.0
Accept: */*

 

 

이 요청의 결과는

Hello!

 

디폴트 프로젝트와 관련하여 논의할 중요한 보안 구성은 없습니다. 우리는 주로 올바른 dependencies가 제자리에 있는지 증명하기 위해 default configurations를 사용합니다. 이것은 authentication 및 authorization에 대해 거의 도움이 되지 않습니다. 이 구현은 우리가 production 레벨의 애플리케이션에서 보고 싶어하는 것이 아닙니다. 하지만 디폴트 프로젝트는 시작하기에 좋은 예시입니다.

이 첫 번째 예제가 작동하면 최소한, Spring Security가 제대로 구성되어 있다는 것을 알 수 있습니다. 다음 단계는 구성을 변경하여 이러한 구성이 우리 프로젝트의 요구 사항에 적용되도록 하는 것입니다. 먼저, Spring Boot가 Spring Security 측면에서 어떤 것을 구성하는지 더 깊이 파고들 것이고, 그 다음에는 구성을 어떻게 재정의할 수 있는지 살펴볼 것입니다.

 

 

2.2 Spring Security 클래스 설계의 큰 그림

이 섹션에서는 인증 및 권한 부여 과정에 참여하는 전체 아키텍처의 주요 행위자들에 대해 논의합니다. 애플리케이션의 요구 사항에 맞게 이러한 사전 구성된 컴포넌트를 재정의해야 하기 때문에 이러한 측면을 알아야 합니다. 인증 및 권한 부여를 위한 Spring Security 아키텍처가 어떻게 작동하는지 설명을 시작할 것이고, 그 다음에는 이 장의 프로젝트에 그것을 적용할 것입니다. 이 모든 것을 한 번에 논의하는 것은 너무 많으므로, 이 장에서의 학습 노력을 최소화하기 위해 각 컴포넌트에 대한 고수준의 그림을 논의할 것입니다. 다음 장에서 각각에 대한 세부 사항을 배울 것입니다.

2.1 섹션에서, 인증 및 권한 부여를 위한 일부 로직이 실행되는 것을 보았습니다. 우리는 default user를 가지고 있었고, 애플리케이션을 시작할 때마다 무작위 비밀번호를 받았습니다. 이 default user와 비밀번호를 사용하여 엔드포인트를 호출할 수 있었습니다. 하지만 이 모든 로직은 어디에 구현되어 있을까요? 아마도 이미 알고 있듯이, Spring Boot는 사용하는 종속성에 따라 몇몇 컴포넌트를 설정해 줍니다.


그림 2.2에서는 Spring Security 아키텍처에서 주요 행위자(컴포넌트)들의 큰 그림과 이들 간의 관계를 볼 수 있습니다. 이 컴포넌트들은 첫 번째 프로젝트에서 사전 구성된 구현을 가지고 있습니다. 이 장에서는 Spring Boot가 Spring Security 측면에서 애플리케이션에 어떤 것을 구성하는지 인지시킬 것입니다. 또한, 제시된 인증 흐름의 일부인 엔티티들 간의 관계에 대해서도 논의할 것입니다.

그림 2.2에서 볼 수 있듯이
1. authentication filter는 인증 요청을 authentication manager에게 위임하고, 응답에 기반하여 security context를 구성합니다.
2. authentication manager 는 인증 과정을 처리하기 위해 authentication provider를 사용합니다.
3. authentication provider는 인증 로직을 구현합니다.
4. user details service는 authentication provider가 인증 로직에서 사용하는 사용자 관리 책임을 구현합니다.
5. 비밀번호 인코더는 authentication provider 가 인증 로직에서 사용하는 비밀번호 관리를 구현합니다.
6. security context는 인증 과정 후 인증 데이터를 유지합니다.

 

다음 단락에서는 이러한 자동 구성된 Bean들에 대해 논의할 것입니다:

  • UserDetailsService
  • PasswordEncoder

이것들도 그림 2.2에서 볼 수 있습니다. authentication provider는 이 빈들을 사용하여 사용자를 찾고 그들의 비밀번호를 확인합니다. 인증에 필요한 자격 증명[ credentials ]을 제공하는 방법으로 시작해 보겠습니다.

Spring Security와 함께 UserDetailsService 인터페이스를 구현하는 객체는 사용자에 대한 세부 정보를 관리합니다. 지금까지 우리는 Spring Boot에서 제공하는 디폴트 구현을 사용했습니다. 이 구현은 애플리케이션의 내부 메모리에 디폴트 자격 증명만 등록합니다. 이러한 디폴트 자격 증명은 "user"로 되어 있고 디폴트 비밀번호는 범용 고유 식별자(UUID)입니다. 이 디폴트 비밀번호는 Spring 컨텍스트가 로드될 때(앱 시작 시) 무작위로 생성됩니다. 이 시점에, 애플리케이션은 콘솔에 비밀번호를 출력하며,  콘솔에서 볼 수 있습니다. 따라서, 이번 챕터에서 방금 작업한 예제에서 사용할 수 있습니다.

 

이 디폴트 구현은 개념 증명을 위한 것일 뿐이며, 종속성이 제대로 설치되어 있는지 확인할 수 있게 해줍니다. 디폴트 구현은 자격 증명을 메모리 내에 저장합니다—애플리케이션은 자격 증명을 영구적으로 저장하지 않습니다. 이 접근 방식은 예제나 개념 증명에는 적합하지만, production 레벨의 준비가 된 애플리케이션에서는 피해야 합니다.

그리고 우리는 PasswordEncoder를 가지고 있습니다. PasswordEncoder는 두 가지를 수행합니다:

  • 비밀번호를 인코딩합니다 (보통 암호화나 해싱 알고리즘을 사용하여)
  • 비밀번호가 기존 인코딩과 일치하는지 확인합니다

UserDetailsService 객체만큼 명확하지 않더라도, PasswordEncoder는 Basic 인증 흐름에 있어 필수적입니다. 가장 간단한 구현은 비밀번호를 평문으로 관리하며 이를 인코딩하지 않습니다. 이 객체의 구현에 대한 더 자세한 내용은 4장에서 논의할 것입니다. 현재로서는, 기본 UserDetailsService와 함께 PasswordEncoder가 존재한다는 것을 인지해야 합니다. UserDetailsService의 디폴트 구현을 교체할 때, PasswordEncoder도 명시해야 합니다.

Spring Boot는 또한 디폴트 값을 구성할 때 인증 방법, 즉 HTTP Basic 액세스 인증을 선택합니다. 이는 가장 간단한 액세스 인증 방법입니다. Basic 인증은 클라이언트가 HTTP Authorization 헤더를 통해 사용자 이름과 비밀번호를 보내기만 하면 됩니다. 헤더의 값에서 클라이언트는 접두어 Basic을 붙이고, 그 뒤에 사용자 이름과 비밀번호가 콜론(:)으로 구분되어 포함된 문자열의 Base64 인코딩을 첨부합니다.

참고로, HTTP Basic 인증은 자격 증명의 기밀 유지를 제공하지 않습니다. Base64는 전송의 편의를 위한 단순한 인코딩 방법일 뿐, 암호화나 해싱 방법이 아닙니다. 전송 중에 가로채지면 누구나 자격 증명을 볼 수 있습니다. 일반적으로, 우리는 적어도 HTTPS를 통한 기밀 유지 없이는 HTTP Basic 인증을 사용하지 않습니다. HTTP Basic의 자세한 정의는 RFC 7617(https://tools.ietf.org/html/rfc7617)에서 읽을 수 있습니다.

 

AuthenticationProvider는 사용자와 비밀번호 관리를 위임하는 인증 로직을 정의합니다. AuthenticationProvider의 디폴트 구현은 UserDetailsService와 PasswordEncoder에 대해 제공된 디폴트 구현을 사용합니다. 암묵적으로, 여러분의 애플리케이션은 모든 엔드포인트를 보호합니다. 따라서, 우리의 예제에서 해야 할 유일한 일은 엔드포인트를 추가하는 것입니다. 또한, 엔드포인트에 접근할 수 있는 사용자가 단 한 명뿐이므로, 이 경우에 권한 부여에 대해 할 일이 거의 없다고 할 수 있습니다.

※ 하지만, HTTP Basic 인증은 단일 사용자만을 대상으로 제한되지 않습니다. 여러 사용자 계정을 설정하여 각각의 사용자에게 개별 자격 증명을 부여할 수 있습니다.

Spring Security에서 HTTP Basic 인증을 사용하는 경우, 여러 사용자 계정을 UserDetailsService를 통해 정의하거나 application.properties 파일에 여러 사용자 계정을 설정하여 다양한 사용자로 인증을 수행할 수 있습니다.

예를 들어, 여러 사용자 계정을 application.yml 파일에 설정할 수 있습니다.

spring:
  security:
    user:
      name: user1
      password: password1
    additional-users:
      - name: user2
        password: password2
      - name: admin
        password: adminpass

제시된 예제에서 HTTP만 사용하는 것을 관찰했을 수 있습니다. 하지만 실제로, 여러분의 애플리케이션은 HTTPS를 통해서만 통신합니다. 이 책에서 논의하는 예제의 경우, Spring Security와 관련된 구성은 HTTP를 사용하든 HTTPS를 사용하든 다르지 않습니다. 따라서 Spring Security와 관련된 예제에 집중할 수 있도록 예제의 엔드포인트에 HTTPS를 구성하지 않을 것입니다. 하지만 원한다면, 이 사이드바에서 제시된 대로 어떤 엔드포인트에든 HTTPS를 활성화할 수 있습니다.

HTTPS를 시스템에 구성하는 데에는 여러 패턴이 있습니다. 경우에 따라, 개발자들은 애플리케이션 레벨에서 HTTPS를 구성하거나, 서비스 메시를 사용하거나, 인프라 수준에서 HTTPS를 설정할 수 있습니다. Spring Boot를 사용하면, 다음 예제에서 배우게 될 것처럼, 애플리케이션 수준에서 쉽게 HTTPS를 활성화할 수 있습니다.

이러한 구성 시나리오에서는, 인증 기관(CA)에 의해 서명된 인증서가 필요합니다. 이 인증서를 사용하여, 엔드포인트를 호출하는 클라이언트는 응답이 인증 서버로부터 오는 것인지, 그리고 통신이 가로채지지 않았는지를 알 수 있습니다. 필요한 경우 이러한 인증서를 구매할 수 있습니다. 애플리케이션을 테스트하기 위해 HTTPS를 구성할 필요가 있다면, OpenSSL( https://www.openssl.org/ )과같은 도구를 사용하여 자체 서명된 인증서를 생성할 수 있습니다. 자체 서명된 인증서를 생성한 다음 프로젝트에 구성해 보겠습니다:

openssl req -newkey rsa:2048 -x509 -keyout key.pem -out cert.pem -days 365


openssl 명령어를 터미널에서 실행하면, 비밀번호와 CA에 대한 세부 정보를 입력하라는 요청을 받게 됩니다. 테스트용 자체 서명된 인증서이기 때문에, 거기에 어떤 데이터든 입력할 수 있습니다; 단지 비밀번호를 기억해야 합니다. 명령어는 두 개의 파일을 출력합니다: key.pem(개인 키)과 cert.pem(공개 인증서). 이 파일들을 사용하여 HTTPS를 활성화하기 위한 자체 서명된 인증서를 생성할 것입니다. 대부분의 경우, 인증서는 공개 키 암호화 표준 #12(PKCS12)입니다. 덜 자주, Java KeyStore(JKS) 형식을 사용합니다. PKCS12 형식으로 예제를 계속해 보겠습니다. 암호학에 대한 훌륭한 논의를 원한다면, David Wong의 Real-World Cryptography(Manning, 2020)를 추천합니다.

openssl pkcs12 -export -in cert.pem -inkey key.pem -out certificate.p12 -name "certificate"


두 번째 명령어는 첫 번째 명령어로 생성된 두 파일을 입력으로 받고 자체 서명된 인증서를 출력합니다.

Windows 시스템에서 Bash 쉘을 사용하여 이 명령어들을 실행하는 경우, 다음 코드 스니펫에 표시된 대로 앞에 winpty를 추가해야 할 수 있습니다:

winpty openssl req -newkey rsa:2048 -x509 -keyout key.pem -out cert.pem -days 365
winpty openssl pkcs12 -export -in cert.pem

 -inkey key.pem -out certificate.p12 -name "certificate"


마지막으로, 자체 서명된 인증서를 갖게 되면, 엔드포인트에 HTTPS를 구성할 수 있습니다. certificate.p12 파일을 Spring Boot 프로젝트의 resources 폴더에 복사하고 application.properties 파일에 다음 줄을 추가하세요:

server.ssl.key-store-type=PKCS12
server.ssl.key-store=classpath:certificate.p12
server.ssl.key-store-password=12345                           #A


#A 비밀번호 값은 PKCS12 인증서 파일을 생성하기 위해 두 번째 명령어를 실행할 때 지정한 값입니다.

비밀번호(제 경우에는 “12345”)는 인증서를 생성하기 위해 명령어를 실행한 후 프롬프트에서 요청되었습니다. 이것이 명령어에 보이지 않는 이유입니다. 이제 애플리케이션에 테스트 엔드포인트를 추가한 다음 HTTPS를 사용하여 호출해 보겠습니다:

@RestController
public class HelloController {
 @GetMapping("/hello")
 public String hello() {
 return "Hello!";
 }
}


자체 서명된 인증서를 사용하는 경우, 엔드포인트 호출을 위해 사용하는 도구를 구성하여 인증서의 진위를 테스트하지 않도록 해야 합니다. 도구가 인증서의 진위를 테스트하는 경우, 인증서를 진짜로 인식하지 못하고 호출이 작동하지 않을 것입니다. cURL을 사용하는 경우, -k 옵션을 사용하여 인증서의 진위성 테스트를 건너뛸 수 있습니다:

curl -k -u user:93a01cf0-794b-4b98-86ef-54860f36f7f3 https://localhost:8080/hello


호출에 대한 응답은

Hello!


입니다.

HTTPS를 사용한다고 해도 시스템의 구성 요소 간 통신이 완전히 안전한 것은 아닙니다. 많은 경우에 사람들이 "이제 더 이상 암호화하지 않아도 돼, HTTPS를 사용하니까!"라고 말하는 것을 들었습니다. 통신을 보호하는 데 도움이 되는 HTTPS이지만, 시스템의 보안 벽의 한 벽돌일 뿐입니다. 항상 시스템의 보안을 책임감 있게 다루고 그것에 관련된 모든 계층을 돌보세요.


 

2.3 Overriding default configurations

이제 첫 번째 프로젝트의 디폴트값을 알게 되었으니, 이를 어떻게 대체할 수 있는지 살펴볼 시간입니다. 디폴트 컴포넌트를 재정의할 수 있는 옵션을 이해해야 하는데, 이는 여러분이 커스텀 구현을 통합하고 애플리케이션에 적합한 보안을 적용하는 방법입니다. 그리고 이 섹션에서 배우게 될 것처럼, 개발 과정은 또한 애플리케이션을 높은 유지 보수성을 가지도록 구성을 작성하는 방법에 관한 것입니다. 우리가 작업할 프로젝트에서는 종종 구성을 재정의하는 여러 가지 방법을 발견할 것입니다. 이러한 유연성은 혼란을 야기할 수 있습니다. 동일한 애플리케이션에서 Spring Security의 다른 부분을 다른 스타일로 구성하는 혼합을 자주 보게 되는데, 이는 바람직하지 않습니다. 그러므로 이 유연성은 주의가 필요합니다. 이 중에서 어떻게 선택해야 하는지 배우는 것이 필요하므로, 이 섹션은 또한 여러분이 가진 옵션을 아는 것에 대해서도 다룹니다.

일부 경우에 개발자들은 구성을 위해 Spring Context 내의 빈을 사용하기로 선택합니다. 다른 경우에는 동일한 목적을 위해 다양한 메소드를 재정의합니다. Spring 생태계가 빠르게 발전한 속도는 아마도 이러한 다양한 접근 방식을 생성한 주요 요인 중 하나일 것입니다. 스타일의 혼합으로 프로젝트를 구성하는 것은 코드를 이해하기 어렵게 만들고 애플리케이션의 유지 보수성에 영향을 미치므로 바람직하지 않습니다. 여러분의 옵션과 그 사용 방법을 아는 것은 소중한 기술이며, 프로젝트에서 애플리케이션 수준의 보안을 어떻게 구성해야 하는지 더 잘 이해하는 데 도움이 됩니다.

이 섹션에서는 UserDetailsService와 PasswordEncoder를 구성하는 방법을 배울 것입니다. 이 두 컴포넌트는 보통 인증에 참여하며, 대부분의 애플리케이션은 요구 사항에 따라 이들을 커스터마이즈합니다. 3장과 4장에서 이들을 커스터마이징하는 세부 사항에 대해 논의할 예정이지만, 커스텀 구현을 통합하는 방법을 보는 것이 중요합니다. 이 장에서 사용하는 구현은 모두 Spring Security에 의해 제공됩니다.

 

2.3.1 Customizing user details management

이 장에서 처음 언급한 컴포넌트는 UserDetailsService였습니다. 보셨듯이, 애플리케이션은 인증 과정에서 이 컴포넌트를 사용합니다. 이 섹션에서는 UserDetailsService 타입의 커스텀 빈을 정의하는 방법을 배울 것입니다. 우리는 이를 통해 Spring Boot에 의해 구성된 디폴트 값을 재정의할 것입니다. 3장에서 더 자세히 볼 수 있듯이, 자체 구현을 만들거나 Spring Security에 의해 제공되는 사전 정의된 것을 사용할 옵션이 있습니다. 이 장에서는 Spring Security에 의해 제공된 구현에 대해 자세히 다루거나 자체 구현을 만들지는 않을 것입니다. 저는 Spring Security에 의해 제공되는 InMemoryUserDetailsManager라는 구현을 사용할 것입니다. 이 예제를 통해 이러한 종류의 객체를 여러분의 아키텍처에 통합하는 방법을 배울 것입니다.

참고로, Java의 인터페이스는 객체 간의 인터페이스를 정의합니다. 애플리케이션의 클래스 디자인에서는 인터페이스를 사용하여 서로를 사용하는 객체를 분리합니다. 이 책에서 논의할 때 이러한 인터페이스 특성을 강화하기 위해 나는 주로 계약[contracts]이라고 부릅니다.

 

우리가 선택한 구현으로 이 컴포넌트를 재정의하는 방법을 보여드리기 위해, 첫 번째 예제에서 했던 것을 변경할 것입니다. 이렇게 하면 자체 관리하는 인증 자격 증명을 가질 수 있습니다. 이 예제에서는 우리 자신의 클래스를 구현하지 않고 Spring Security에서 제공하는 구현을 사용합니다.

이 예제에서는 InMemoryUserDetailsManager 구현을 사용합니다. 이 구현이 단순한 UserDetailsService보다 약간 더 많은 기능을 제공하지만, 현재로서는 UserDetailsService의 관점에서만 언급합니다. 이 구현은 메모리에 자격 증명을 저장하며, 이후 Spring Security가 요청을 인증하는 데 사용할 수 있습니다.

 

참고로, InMemoryUserDetailsManager 구현은 production 레벨을 준비하는 애플리케이션을 위한 것이 아니지만, 예제나 개념 증명을 위한 훌륭한 도구입니다. 경우에 따라서는 사용자만 필요할 수 있습니다. 이 기능의 일부를 구현하는 데 시간을 소비할 필요가 없습니다. 우리의 경우에는 기본 UserDetailsService 구현을 어떻게 재정의하는지 이해하기 위해 이를 사용합니다.

 

자바 기반 Configuration 클래스를 정의함으로써 시작합니다. 일반적으로, 우리는 config라는 별도의 패키지에 구성 클래스를 선언합니다. 목록 2.3은 구성 클래스에 대한 정의를 보여줍니다. 이 예제는 프로젝트 ssia-ch2-ex2에서도 찾아볼 수 있습니다.

 

Listing 2.3 The configuration class for the UserDetailsService bean

@Configuration                                                  #A
public class ProjectConfig {
 @Bean                                                          #B
 UserDetailsService userDetailsService() {
 return new InMemoryUserDetailsManager(); 
 }
}

 

#A  @Configuration 어노테이션은 클래스를 구성 클래스로 표시합니다.
#B  @Bean 어노테이션은 팩토리 메서드가 리턴한 값을 Spring Context의 빈으로 추가하도록 지시합니다.

 

코드를 지금 그대로 실행하면, 더 이상 콘솔에서 자동 생성된 비밀번호를 보지 못할 것입니다. 애플리케이션은 이제 디폴트 자동 구성 대신, 컨텍스트에 추가한 UserDetailsService 타입의 인스턴스를 사용합니다. 하지만 동시에, 두 가지 이유로 엔드포인트에 더 이상 접근할 수 없게 됩니다:

  • 사용자가 없습니다.
  • PasswordEncoder가 없습니다.

그림 2.2에서 볼 수 있듯이, 인증은 PasswordEncoder에도 의존합니다. 이 두 가지 문제를 단계별로 해결해 봅시다. 우리는 다음을 수행해야 합니다:
1. 자격증명(사용자 이름과 비밀번호)이 있는 적어도 한 명의 사용자를 생성합니다.
2. UserDetailsService의 구현으로 관리될 사용자를 추가합니다.
3. 애플리케이션이 주어진 비밀번호를 UserDetailsService에 의해 저장 및 관리되는 비밀번호와 비교하는 데 사용할 수 있는 PasswordEncoder 타입의 빈을 정의합니다.

먼저, 인증에 사용할 수 있는 자격증명 세트를 선언하고 InMemoryUserDetailsManager 인스턴스에 추가합니다. 3장에서는 사용자와 그 관리 방법에 대해 더 자세히 논의할 것입니다. 현재로서는, 사전 정의된 빌더를 사용하여 UserDetails 타입의 객체를 생성해 봅시다.

참고로 코드에서 때때로 var을 사용하는 것을 볼 수 있습니다. 자바 10에서는 예약된 타입 이름 var을 도입했으며, 이는 로컬 선언에서만 사용할 수 있습니다. 이 책에서는 문법을 더 짧게 만들고 변수 타입을 숨기기 위해 var을 사용합니다. var에 의해 숨겨진 타입들은 나중 장에서 논의할 것이므로, 제대로 분석할 때까지 그 타입에 대해 걱정할 필요가 없습니다. 하지만 var을 사용하면 현재 논의 중인 주제에 집중할 수 있도록 필요하지 않은 세부 사항을 제거하는 데 도움이 됩니다.

 

이 InMemoryUserDetailsManager 인스턴스를 구축할 때, 사용자 이름, 비밀번호, 그리고 최소한 하나의 권한을 제공해야 합니다. "권한"은 해당 사용자에게 허용된 행동을 의미하며, 이를 위해 어떤 문자열이든 사용할 수 있습니다. 목록 2.4에서는 권한을 read라고 명명했지만, 우리가 현재 이 권한을 사용하지 않을 것이기 때문에, 이 이름은 실제로 중요하지 않습니다.

 

Listing 2.4 Creating a user with the User builder class for UserDetailsService

@Configuration
public class ProjectConfig {
 @Bean
 UserDetailsService userDetailsService() {
 var user = User.withUsername("john")                        #A
 .password("12345")                                          #A
 .authorities("read")                                        #A
 .build();                                                   #A
 return new InMemoryUserDetailsManager(user);                #B
 }
}

 

#A Builds the user with a given username, password, and authorities list

#B Adds the user to be managed by UserDetailsService

 

참고로 User 클래스는 org.springframework.security.core.userdetails 패키지에서 찾을 수 있습니다. 이것은 사용자를 나타내는 객체를 생성하기 위해 사용하는 빌더 구현체입니다. 또한, 이 책에서 일반적인 규칙으로, 코드 목록에서 클래스 작성 방법을 제시하지 않는다면, 그것은 Spring Security가 제공한다는 의미입니다.

 

Listing 2.4에서 제시된 바와 같이, 우리는 사용자 이름, 비밀번호에 대한 값을 제공해야 하며, 최소한 하나의 권한을 제공해야 합니다. 하지만 이것만으로는 엔드포인트를 호출할 수 있을 만큼 충분하지 않습니다. 우리는 또한 PasswordEncoder를 선언해야 합니다.

디폴트 UserDetailsService를 사용할 때, PasswordEncoder도 자동으로 구성됩니다. 우리가 UserDetailsService를 재정의했기 때문에, PasswordEncoder도 선언해야 합니다. 지금 예제를 시도해보면, 엔드포인트를 호출할 때 예외가 발생하는 것을 볼 수 있습니다. 인증을 시도할 때, Spring Security는 비밀번호를 어떻게 관리해야 할지 모르고 실패합니다. 예외는 다음 코드 스니펫과 같으며, 애플리케이션의 콘솔에서 이를 볼 수 있어야 합니다. 클라이언트는 HTTP 401 Unauthorized 메시지와 텅빈 응답 본문을 받게 됩니다:

curl -u john:12345 http://localhost:8080/hello

 

The result of the call in the app’s console is

java.lang.IllegalArgumentException: 
There is no PasswordEncoder mapped for the id "null"
 at 
org.springframework.security.crypto.password
[CA].DelegatingPasswordEncoder$
[CA]UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:289) 
~[spring-security-crypto-6.0.0.jar:6.0.0]
 at org.springframework.security.crypto.password
[CA].DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:237) 
~[spring-security-crypto-6.0.0.jar:6.0.0]

 

이 문제를 해결하기 위해, UserDetailsService와 마찬가지로 컨텍스트에 PasswordEncoder 빈을 추가할 수 있습니다. 이 빈에 대해서는 PasswordEncoder의 기존 구현을 사용합니다:

@Bean
public PasswordEncoder passwordEncoder() {
 return NoOpPasswordEncoder.getInstance();
}

 

참고로, NoOpPasswordEncoder 인스턴스는 비밀번호를 평문으로 취급합니다. 이것은 비밀번호를 암호화하거나 해싱하지 않습니다. 매칭을 위해, NoOpPasswordEncoder는 String 클래스의 기본 equals(Object o) 메소드를 사용하여 문자열을 비교하기만 합니다. 이 타입의 PasswordEncoder는 production 준비 앱에서 사용해서는 안 됩니다. NoOpPasswordEncoder는 비밀번호의 해싱 알고리즘에 집중하고 싶지 않은 예제에서 좋은 옵션입니다. 따라서, 이 클래스의 개발자들은 이를 @Deprecated로 표시했으며, 여러분의 개발 환경에서는 이 이름이 취소선과 함께 표시될 것입니다.

 

다음 리스팅에서 구성 클래스의 전체 코드를 볼 수 있습니다.

 

Listing 2.5 The full definition of the configuration class

@Configuration
public class ProjectConfig {
 @Bean
 UserDetailsService userDetailsService() {
 var user = User.withUsername("john")
 .password("12345")
 .authorities("read")
 .build();
 return new InMemoryUserDetailsManager(user);
 }
 @Bean                                              #A
 PasswordEncoder passwordEncoder() {
 return NoOpPasswordEncoder.getInstance();
 }
}

 

#A A new method annotated with @Bean to add a PasswordEncoder to the context

 

새로운 사용자를 사용하여 엔드포인트를 시도해봅시다. 사용자 이름은 John이고 비밀번호는 12345입니다:

curl -u john:12345 http://localhost:8080/hello
Hello!

 

Postman을 사용한 테스트

 

 

 

웹 브라우저에서의 테스트 결과입니다

 

 

참고로, 단위 테스트와 통합 테스트의 중요성을 알고 있는 일부 독자들은 우리의 예제에 대해서도 테스트를 작성하지 않는 이유를 궁금해할 수 있습니다. 실제로, 이 책과 함께 제공되는 모든 예제에 관련된 Spring Security 통합 테스트를 찾을 수 있습니다. 그러나, 각 장에서 제시된 주제에 집중할 수 있도록 돕기 위해, Spring Security 통합에 대한 논의와 18장에서 이에 대한 세부 사항을 분리했습니다.

 

2.3.2 Applying authorization at the endpoint level

2.3.1절에서 설명한 대로 새로운 사용자 관리 방식이 도입된 이후, 이제 엔드포인트에 대한 인증 방법 및 구성에 대해 논의할 수 있게 되었습니다. 7장부터 12장까지의 내용에서는 권한 부여 구성에 대해 많은 것을 배울 수 있습니다. 하지만 세부 사항에 들어가기 전에, 전체적인 그림을 이해해야 합니다. 이를 달성하는 가장 좋은 방법은 첫 번째 예제를 통한 것입니다. 디폴트 구성을 사용할 때, 모든 엔드포인트는 애플리케이션이 관리하는 유효한 사용자를 가지고 있다고 가정합니다. 또한, 기본적으로 앱은 HTTP Basic 인증을 사용하지만, 이 구성은 쉽게 재정의할 수 있습니다.

다음 장에서 배우게 될 것처럼, HTTP Basic 인증은 대부분의 애플리케이션 아키텍처에 적합하지 않습니다. 때로는 우리의 애플리케이션에 맞게 변경하고 싶을 수 있습니다. 마찬가지로, 애플리케이션의 모든 엔드포인트를 보안 처리할 필요는 없으며, 보안 처리가 필요한 엔드포인트의 경우 다른 인증 방법과 권한 부여 규칙을 선택해야 할 수도 있습니다. 인증 및 권한 부여를 커스터마이즈하기 위해서는 SecurityFilterChain 타입의 빈을 정의해야 합니다. 이 예제의 경우, 코드를 ssia-ch2-ex3 프로젝트에 계속 작성할 것입니다.

 

Listing 2.6 Defining a SecurityFilterChain bean

@Configuration
public class ProjectConfig {
 @Bean
 SecurityFilterChain configure(HttpSecurity http) 
 throws Exception {
 return http.build();
 }
 // Omitted code
}

 

그런 다음 다음 목록에 표시된 것처럼 HttpSecurity 객체의 다른 메소드를 사용하여 구성을 변경할 수 있습니다.

 

Listing 2.7 Using the HttpSecurity parameter to alter the configuration

@Configuration
public class ProjectConfig {
 @Bean
 SecurityFilterChain configure(HttpSecurity http) 
 throws Exception {
 http.httpBasic(Customizer.withDefaults());                         #A
 http.authorizeHttpRequests(
 c -> c.anyRequest().authenticated()                                #B
 );
 return http.build(); 
 }
 // Omitted code
}

 

#A App uses HTTP Basic authentication.

#B All the requests require authentication.

 

리스팅 2.7의 코드는 디폴트 값과 동일한 동작으로 엔드포인트 권한 부여를 구성합니다. 엔드포인트를 다시 호출하여 2.3.1절의 이전 테스트와 동일하게 동작하는지 확인할 수 있습니다. 약간의 변경으로, 자격 증명이 필요 없이 모든 엔드포인트에 접근할 수 있게 만들 수 있습니다. 다음 리스팅에서 이를 어떻게 하는지 알아볼 것입니다.

 

Listing 2.8 Using permitAll() to change the authorization configuration

@Configuration
public class ProjectConfig {
 @Bean
 public SecurityFilterChain configure(HttpSecurity http) 
 throws Exception {
 http.httpBasic(Customizer.withDefaults()); 
 http.authorizeHttpRequests(
 c -> c.anyRequest().permitAll() #A
 );
 return http.build();
 }
 // Omitted code
}

 

#A 요청 중 어느 것도 인증을 받을 필요가 없습니다.

 

이제, 자격 증명 없이 /hello 엔드포인트를 호출할 수 있습니다. 구성에서 permitAll() 호출과 anyRequest() 메소드의 조합이 모든 엔드포인트를 자격 증명 없이 접근 가능하게 만듭니다:

curl http://localhost:8080/hello

 

그리고 호출의 응답 본문은

Hello!

 

이 예제에서는 두 가지 구성 방법을 사용했습니다.
1. httpBasic() - 여기서는 인증 접근 방식을 구성하는 데 도움이 되었습니다. 이 메서드를 호출하여 애플리케이션이 HTTP Basic 인증을 인증 방법으로 수락하도록 지시했습니다.
2. authorizeHttpRequests() - 여기서는 엔드포인트 레벨에서 권한 부여 규칙을 구성하는 데 도움이 되었습니다. 이 메서드를 호출하여 특정 엔드포인트에서 수신된 요청을 어떻게 인가해야 하는지 애플리케이션에 지시했습니다.


두 메서드 모두 매개변수로 Customizer 객체를 사용해야 했습니다. Customizer는 구성하는 Spring Security 요소를 정의하기 위해 구현하는 계약입니다: 인증, 권한 부여 또는 CSRF 또는 CORS와 같은 특정 보호 메커니즘(9장과 10장에서 다룰 예정)을 구성합니다. 다음 코드 조각은 Customizer 인터페이스의 정의를 보여줍니다. Customizer는 함수형 인터페이스이며(listing 2.8에서 사용한) withDefaults() 메서드는 사실 아무것도 수행하지 않는 Customizer 구현체입니다.

@FunctionalInterface
public interface Customizer<T> {
 	void customize(T t);
 	static <T> Customizer<T> withDefaults() {
 		return (t) -> {
 		};
 	}
}

 

Spring Security의 이전 버전에서는 Customizer 객체를 사용하지 않고도 Chaining 구문을 사용하여 구성을 적용할 수 있었습니다. 다음 코드 조각에서 확인할 수 있듯이 authorizeHttpRequests() 메서드에 Customizer 객체를 제공하는 대신에 구성이 메서드 호출을 따라 직접 이어집니다.

http.authorizeHttpRequests() 
 .anyRequest().authenticated()

 

이 접근 방식이 사용되지 않는 이유는 Customizer 객체를 사용하면 필요한 곳으로 구성을 이동할 수 있는 더 많은 유연성을 제공하기 때문입니다. 단순한 예제에서는 람다 표현식을 사용하는 것이 편리할 수 있습니다. 그러나 현실 세계의 앱에서는 구성이 크게 증가할 수 있습니다. 이러한 경우에 이러한 구성을 별도의 클래스로 이동할 수 있는 능력은 구성을 유지하고 테스트하기 쉽게 도와줍니다.

이 예제의 목적은 디폴트 구성을 재정의하는 방법에 대한 느낌을 제공하는 것입니다. 권한에 대한 자세한 내용은 7장부터 10장까지 다룰 예정입니다.

Spring Security의 이전 버전에서는 시큐리티 구성 클래스가 WebSecurityConfigurerAdapter라는 클래스를 확장해야 했습니다. 하지만 오늘날에는 이러한 방식을 더 이상 사용하지 않습니다. 만약 여러분의 애플리케이션이 이전 코드베이스를 사용하고 있거나 이전 코드베이스를 업그레이드해야 하는 경우, 이전 버전의 Spring Security in Action도 참고하시기를 권장합니다.

 

 

2.3.3 Configuring in different ways

Spring Security를 사용하여 구성을 생성하는 데 혼란스러운 측면 중 하나는 동일한 것을 구성하는 여러 가지 방법이 있습니다. 이 섹션에서는 UserDetailsService와 PasswordEncoder를 구성하는 대안을 배우게 됩니다. 이러한 옵션을 알아두는 것은 이 책이나 블로그 및 기사와 같은 다른 소스에서 찾는 예제에서 이를 인식할 수 있어야 하므로 중요합니다. 또한 여러분의 애플리케이션에서 이를 언제, 어떻게 사용해야 하는지 이해하는 것이 중요합니다. 이후 장에서는 이 섹션에서 제공된 정보를 확장한 다양한 예제를 볼 수 있습니다.

첫 번째 프로젝트를 살펴보겠습니다. 우리는 기본 애플리케이션을 생성한 후에 Spring 컨텍스트에 새로운 구현을 빈으로 추가함으로써 UserDetailsService와 PasswordEncoder를 재정의할 수 있었습니다. UserDetailsService와 PasswordEncoder를 동일하게 구성하는 또 다른 방법을 찾아보겠습니다. SecurityFilterChain 빈을 직접 사용하여 UserDetailsService와 PasswordEncoder를 설정할 수 있습니다. 아래 목록에서 이를 보여줍니다. 이 예제는 프로젝트 ssia-ch2-ex3에서 찾을 수 있습니다.

 

Listing 2.9 Setting UserDetailsService with the SecurityFilterChain bean

@Configuration
public class ProjectConfig {

  @Bean
  SecurityFilterChain configure(HttpSecurity http) 
    throws Exception {
    
    http.httpBasic(Customizer.withDefaults());

    http.authorizeHttpRequests(
        c -> c.anyRequest().authenticated()
    );

    var user = User.withUsername("john")                                #A
        .password("12345")
        .authorities("read")
        .build();

    var userDetailsService = 
    	new InMemoryUserDetailsManager(user);                          #B

    http.userDetailsService(userDetailsService);                       #C

    return http.build();
  }

  // Omitted code

}

 

#A Defines a user with all its details

#B Declares a UserDetailsSevice to store the users in memory and adds the user to be managed by our UserDetailsSevice

#C The UserDetailsService is now set up using the SecurityFilterChain bean.

 

listing 2.9에서는 UserDetailsService를 listing 2.5와 동일한 방식으로 선언하는 것을 볼 수 있습니다. 차이점은 이제 이것이 SecurityFilterChain을 생성하는 빈 메서드 내에서 로컬로 수행된다는 것입니다. 또한 HttpSecurity에서 userDetailsService() 메서드를 호출하여 UserDetailsService 인스턴스를 등록합니다. 리스팅 2.10에는 구성 클래스의 전체 내용이 표시됩니다.

 

Listing 2.10 Full definition of the configuration class

@Configuration
public class ProjectConfig {

  @Bean
  SecurityFilterChain configure(HttpSecurity http)
          throws Exception {

    http.httpBasic(Customizer.withDefaults());

    http.authorizeHttpRequests(
        c -> c.anyRequest().authenticated()
    );

    var user = User.withUsername("john")                                 #A
        .password("12345")
        .authorities("read")
        .build();

    var userDetailsService =
            new InMemoryUserDetailsManager(user);                        #B

    http.userDetailsService(userDetailsService);                         #C

    return http.build();
  }

  @Bean
  PasswordEncoder passwordEncoder() {    
    return NoOpPasswordEncoder.getInstance();
  }

}

 

#A Creates a new user

#B Adds the user to be managed by our UserDetailsService

#C Configures UserDetailsService

 

이러한 구성 옵션 중 어느 것이든 올바릅니다. 첫 번째 옵션에서는 빈을 컨텍스트에 추가하여 필요한 다른 클래스에 이 값을 주입할 수 있습니다. 그러나 해당 값이 다른 클래스에게 필요하지 않은 경우 두 번째 옵션도 동등하게 좋을 것입니다.

 

2.3.4 Defining custom authentication logic

지금까지 Spring Security 구성 요소가 어떻게 유연성을 제공하는지를 이미 알아 보았으며, 이는 우리가 이를 애플리케이션 아키텍처에 적응시킬 때 다양한 옵션을 제공합니다. 지금까지 Spring Security 아키텍처에서 UserDetailsService 및 PasswordEncoder의 목적을 배웠으며, 이를 구성하는 몇 가지 방법을 보았습니다. 이제는 이러한 구성 요소를 위임하는 구성 요소, 즉 AuthenticationProvider를 사용자 지정할 수도 있다는 것을 배워야 합니다.

Figure 2.3에서는 AuthenticationProvider가 인증 로직을 구현하는 것을 보여줍니다. 이는 AuthenticationManager로부터 요청을 받고, 사용자를 찾는 작업은 UserDetailsService에게 위임하고, 비밀번호를 확인하는 작업은 PasswordEncoder에게 위임합니다.

 

Figure 2.3에서는 AuthenticationProvider가 인증 로직을 구현하고 사용자 및 비밀번호 관리를 위해 UserDetailsService와 PasswordEncoder에게 위임하는 것을 보여줍니다. 따라서 이 섹션에서는 AuthenticationProvider를 사용하여 사용자 정의 인증 로직을 구현하는 방법을 더 자세히 살펴보게 됩니다. 이것은 첫 번째 예시이기 때문에 아키텍처 내 구성 요소 간의 관계를 더 잘 이해할 수 있도록 간략히 보여줍니다. 그러나 우리는 3장에서 6장까지 더 자세히 설명할 것입니다.

 

제가 권장하는 것은 Spring Security 아키텍처에서 설계된 책임[ responsibilities ]을 고려해 보시기 바랍니다. 이 아키텍처는 느슨하게 결합된 세밀한 책임으로 구성되어 있습니다. 이 설계는 Spring Security를 유연하고 쉽게 응용 프로그램에 통합할 수 있게 만드는 요소 중 하나입니다. 그러나 이러한 유연성을 어떻게 활용하느냐에 따라 설계를 변경할 수도 있습니다. 이러한 접근 방식은 솔루션을 복잡하게 만들 수 있으므로 주의해야 합니다. 예를 들어, 디폴트 AuthenticationProvider를 오버라이딩하 방식을 선택하여 UserDetailsService나 PasswordEncoder가 더 이상 필요하지 않게 할 수 있습니다. 이를 기억하고 다음 코드는 사용자 정의 인증 제공자를 만드는 방법을 보여줍니다. 이 예시는 프로젝트 ssia-ch2-ex4에서 찾을 수 있습니다.

 

Listing 2.11 Implementing the AuthenticationProvider interface

@Component
public class CustomAuthenticationProvider 
		implements AuthenticationProvider {
 	@Override
 	public Authentication authenticate
 	(Authentication authentication) throws AuthenticationException {
 		// authentication logic here
 	}
 	@Override
 	public boolean supports(Class<?> authenticationType) {
 	// type of the Authentication implementation here
 	}
}

 

authenticate(Authentication authentication) 메서드는 인증에 대한 모든 로직을 나타내므로, 이를 리스팅 2.12와 같이 구현할 것입니다. supports() 메서드의 사용 방법에 대한 설명은 6장에서 자세히 다룰 예정입니다. 현재 예제에는 중요하지 않으므로 이 구현을 당연시하는 것이 좋습니다.

 

Listing 2.12 Implementing the authentication logic

@Override
public Authentication authenticate(
 						Authentication authentication) 
 						throws AuthenticationException {
 
 	String username = authentication.getName();                #A
 	String password = String.valueOf(
 	authentication.getCredentials());
 	if ("john".equals(username) &&                             #B
 		"12345".equals(password)) {
 		return new UsernamePasswordAuthenticationToken(
 				username, 
 				password, 
 				Arrays.asList());
 	} else {
 		throw new AuthenticationCredentialsNotFoundException("Error!");
 	}
}

 

#A The getName() method is inherited by Authentication from the Principal interface.

#B This condition generally calls UserDetailsService and PasswordEncoder to test the username and password.

 

위의 코드에서 볼 수 있듯이, if-else 절의 조건은 UserDetailsService와 PasswordEncoder의 책임을 대체합니다. 이 두 개의 빈을 사용할 필요는 없지만 사용자와 암호의 관리 로직을 분리하는 것이 좋습니다. Spring Security 아키텍처가 설계한대로 적용하십시오. 심지어 인증 구현을 재정의할 때도 마찬가지입니다.
디폴트 구현이 애플리케이션 요구 사항과 완전히 일치하지 않는 경우 사용자 정의 인증 로직을 구현하여 인증 로직을 대체하는 것이 유용할 수 있습니다. 전체 AuthenticationProvider 구현은 다음 리스팅과 같습니다.

 

Listing 2.13 The full implementation of the authentication provider

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = String.valueOf(authentication.getCredentials());

        if ("john".equals(username) && "12345".equals(password)) {
            return new UsernamePasswordAuthenticationToken(username, password, Arrays.asList());
        } else {
            throw new AuthenticationCredentialsNotFoundException("Error!");
        }
    }

    @Override
    public boolean supports(Class<?> authenticationType) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authenticationType);
    }
}

 

구성 클래스에서는 다음 리스팅에 표시된 configure(AuthenticationManagerBuilder auth) 메서드를 사용하여 AuthenticationProvider를 등록할 수 있습니다.

@Configuration
public class ProjectConfig {

    private final CustomAuthenticationProvider authenticationProvider;

    public ProjectConfig(CustomAuthenticationProvider authenticationProvider) {
        this.authenticationProvider = authenticationProvider;
    }

    @Bean
    SecurityFilterChain configure(HttpSecurity http) throws Exception {
        http.httpBasic(Customizer.withDefaults());
        http.authenticationProvider(authenticationProvider);

        http.authorizeHttpRequests(
            c -> c.anyRequest().authenticated()
        );

        return http.build();
    }
}

 

이제 유일하게 인식된 사용자로 정의된 엔드포인트를 호출할 수 있습니다. 이 사용자는 John이고 암호는 12345입니다.

curl -u john:12345 http://localhost:8080/hello

 

제 6장에서는 AuthenticationProvider에 대한 자세한 내용과 인증 프로세스에서의 동작을 재정의하는 방법에 대해 더 자세히 배우게 될 것입니다. 해당 장에서는 또한 Authentication 인터페이스와 UserPasswordAuthenticationToken과 같은 해당 인터페이스의 구현에 대해 논의할 것입니다.

 

2.3.5 Using multiple configuration classes

이전에 구현된 예제에서는 구성 클래스만을 사용했습니다. 그러나 구성 클래스의 책임을 분리하는 것이 좋은 실천 방법입니다. 구성이 점점 복잡해지기 때문에 이 분리가 필요합니다. production 수준의 애플리케이션에서는 첫 번째 예제보다 더 많은 선언이 있을 수 있습니다. 또한 프로젝트를 읽기 쉽게 만들기 위해 구성 클래스를 하나 이상 두는 것이 유용할 수 있습니다.

각 책임 당 하나의 클래스만 가지는 것이 항상 좋은 실천 방법입니다. 이 예제에서는 사용자 관리 구성을 권한 부여 구성으로부터 분리하기 위해 두 개의 구성 클래스를 정의합니다: UserManagementConfig(리스팅 2.15에서 정의됨) 및 WebAuthorizationConfig(리스팅 2.16에서 정의됨). 이 예제는 ssia-ch2-ex5 프로젝트에서 찾을 수 있습니다.

 

Listing 2.15 Defining the configuration class for user and password management

@Configuration
public class UserManagementConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        var userDetailsService = new InMemoryUserDetailsManager();

        var user = User.withUsername("john")
                .password("12345")
                .authorities("read")
                .build();

        userDetailsService.createUser(user);
        return userDetailsService;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

 

이 경우, UserManagementConfig 클래스에는 사용자 관리를 담당하는 두 개의 빈만 포함됩니다: UserDetailsService와 PasswordEncoder입니다. 다음 리스팅에는 이 정의가 나와 있습니다.

 

Listing 2.16 Defining the configuration class for authorization management

@Configuration
public class WebAuthorizationConfig {

  @Bean
  SecurityFilterChain configure(HttpSecurity http) throws Exception {
    http.httpBasic(Customizer.withDefaults());

    http.authorizeHttpRequests(
        c -> c.anyRequest().authenticated()
    );

    return http.build();
  }
}

 

여기서 WebAuthorizationConfig 클래스는 인증 및 권한 부여 규칙을 구성하기 위해 SecurityFilterChain 타입의 빈을 정의해야 합니다.

 

2.4 요약

  • Spring Boot는 애플리케이션에 Spring Security를 의존성으로 추가할 때 일부 디폴트 구성을 제공합니다.
  • 인증 및 권한 부여를 위한 기본 구성 요소를 구현합니다: UserDetailsService, PasswordEncoder 및 AuthenticationProvider입니다.
  • User 클래스를 사용하여 사용자를 정의할 수 있습니다. 사용자는 최소한 사용자 이름, 비밀번호 및 권한을 가져야 합니다. 권한은 애플리케이션의 문맥에서 사용자가 수행할 수 있는 작업입니다.
  • Spring Security가 제공하는 UserDetailsService의 간단한 구현은 InMemoryUserDetailsManager입니다. 이러한 UserDetailsService 인스턴스에 사용자를 추가하여 애플리케이션의 메모리에서 사용자를 관리할 수 있습니다.
  • NoOpPasswordEncoder는 암호를 평문으로 사용하는 PasswordEncoder 계약의 구현입니다. 이 구현은 학습용 예제 및 (아마도) 개념 증명에는 적합하지만, 프로덕션에 사용하기에는 적합하지 않습니다.
  • AuthenticationProvider 계약을 사용하여 애플리케이션에 사용자 지정 인증 로직을 구현할 수 있습니다.
  • 구성을 작성하는 여러 가지 방법이 있지만, 단일 애플리케이션에서는 하나의 접근 방식을 선택하고 고수해야 합니다. 이렇게 하면 코드를 더 깔끔하고 이해하기 쉽게 만들 수 있습니다.

 

 

 

 

 

 

 

 

 

 

'Spring Security' 카테고리의 다른 글

ch04 Managing passwords  (0) 2024.02.25
ch03 Managing users  (0) 2024.02.25
CORS : Non-Simple Request / Simple Request  (0) 2023.06.20
Options  (0) 2023.06.10
CSRF  (0) 2023.06.08