Spring Boot and OAuth2

업데이트: Link

Spring Boot and OAuth2

이 가이드에서는 OAuth 2.0Spring Boot를 사용하여 “소셜 로그인”으로 다양한 작업을 수행하는 샘플 앱을 빌드하는 방법을 설명합니다.

간단한 단일 공급자 싱글 사인 온으로 시작하여 인증 공급자를 선택할 수 있는 클라이언트까지 작동합니다: GitHub 또는 Google.

샘플은 모두 백엔드에서 Spring Boot 및 Spring Security를 사용하는 단일 페이지 앱입니다. 또한 모두 프론트엔드에서 일반 jQuery를 사용합니다. 하지만 다른 JavaScript 프레임워크로 변환하거나 서버 측 렌더링을 사용하기 위해 필요한 변경 사항은 미미합니다.

모든 샘플은 Spring Boot의 네이티브 OAuth 2.0 지원을 사용하여 구현되었습니다.

여러 샘플이 서로를 기반으로 구축되어 각 단계마다 새로운 기능을 추가합니다:

  • simple: 홈페이지만 있는 매우 기본적인 정적 앱으로 Spring Boot의 OAuth 2.0 구성 속성을 통해 무조건 로그인합니다(홈페이지에 방문하면 자동으로 GitHub로 리디렉션됨).

  • click: 사용자가 클릭해야 로그인할 수 있는 명시적 링크를 추가합니다.

  • 로그아웃: 인증된 사용자를 위한 로그아웃 링크도 추가합니다.

  • 두 공급자: 두 번째 로그인 공급자를 추가하여 사용자가 홈페이지에서 어떤 공급자를 사용할지 선택할 수 있도록 합니다.

  • custom-error: 인증되지 않은 사용자에 대한 오류 메시지와 GitHub의 API에 기반한 사용자 지정 인증을 추가합니다.

기능 계층 구조의 한 앱에서 다음 앱으로 마이그레이션하는 데 필요한 변경 사항은 소스 코드에서 추적할 수 있습니다. 앱의 각 버전은 자체 디렉터리에 있으므로 차이점을 비교할 수 있습니다.

각 앱을 IDE로 가져올 수 있습니다. SocialApplication에서 main 메서드를 실행하여 앱을 시작할 수 있습니다. http://localhost:8080에 모두 홈페이지가 있습니다(로그인하여 콘텐츠를 보려면 최소한 GitHub 및 Google 계정이 있어야 합니다).

명령줄에서 mvn spring-boot:run을 사용하여 모든 앱을 실행하거나, jar 파일을 빌드한 후 mvn packagejava -jar target/*.jar로 실행할 수도 있습니다(Spring Boot 문서 및 기타 사용 가능한 문서 참조). 예를 들어, 최상위 레벨에서 wrapper를 사용하는 경우 Maven을 설치할 필요가 없습니다.

$ cd simple
$ ../mvnw package
$ java -jar target/*.jar

앱은 모두 해당 주소에 대해 GitHub 및 Google에 등록된 OAuth 2.0 클라이언트를 사용하기 때문에 localhost:8080에서 작동합니다. 다른 호스트나 포트에서 앱을 실행하려면 해당 방식으로 앱을 등록해야 합니다. 기본값을 사용하면 로컬호스트를 넘어 자격 증명이 유출될 위험은 없습니다. 하지만 인터넷에 노출되는 정보에 주의하고, 앱 등록을 공개 소스 제어에 두지 마세요.

Single Sign On With GitHub

이 섹션에서는 인증에 GitHub를 사용하는 최소한의 애플리케이션을 만들어 보겠습니다. Spring Boot의 자동 구성 기능을 활용하면 매우 쉽게 만들 수 있습니다.

새 프로젝트 만들기

먼저 여러 가지 방법으로 Spring Boot 애플리케이션을 생성해야 합니다. 가장 쉬운 방법은 https://start.spring.io로 이동하여 빈 프로젝트를 생성하는 것입니다(“웹” 종속성을 시작점으로 선택). 마찬가지로 명령줄에서 이 작업을 수행합니다:

$ mkdir ui && cd ui
$ curl https://start.spring.io/starter.tgz -d style=web -d name=simple | tar -xzvf -

그런 다음 해당 프로젝트를 즐겨 사용하는 IDE로 가져오거나(기본적으로 일반 Maven Java 프로젝트), 명령줄에서 파일과 mvn을 사용하여 작업할 수 있습니다.

홈페이지 추가

새 프로젝트에서 src/main/resources/static 폴더에 index.html을 만듭니다. 스타일시트와 자바스크립트 링크를 추가하여 결과는 다음과 같아야 합니다:

index.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <title>Demo</title>
    <meta name="description" content=""/>
    <meta name="viewport" content="width=device-width"/>
    <base href="/"/>
    <link rel="stylesheet" type="text/css" href="/webjars/bootstrap/css/bootstrap.min.css"/>
    <script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script>
    <script type="text/javascript" src="/webjars/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
	<h1>Demo</h1>
	<div class="container"></div>
</body>
</html>

OAuth 2.0 로그인 기능을 시연하기 위해 이 모든 것이 필요한 것은 아니지만, 결국에는 쾌적한 UI를 갖는 것이 좋으므로 홈 페이지에서 몇 가지 기본 사항부터 시작하는 것이 좋습니다.

앱을 시작하고 홈 페이지를 로드하면 스타일시트가 로드되지 않은 것을 확인할 수 있습니다. 따라서 jQuery와 트위터 Bootstrap을 추가하여 스타일시트도 추가해야 합니다:

pom.xml

<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>jquery</artifactId>
	<version>3.4.1</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>bootstrap</artifactId>
	<version>4.3.1</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>webjars-locator-core</artifactId>
</dependency>

마지막 종속성은 webjars 사이트에서 라이브러리로 제공하는 webjars “locator” 입니다. Spring은 로케이터를 사용하여 정확한 버전을 알 필요 없이 웹자르에서 정적 자산을 찾을 수 있습니다(따라서 index.html의 버전이 없는 /webjars/** 링크). 웹 jar 로케이터는 MVC 자동 구성을 끄지 않는 한 Spring Boot 앱에서 기본적으로 활성화됩니다.

이러한 변경 사항을 적용하면 앱의 홈 페이지가 멋지게 보일 것입니다.

GitHub 및 Spring Security로 애플리케이션 보호

애플리케이션을 안전하게 만들려면 Spring Security를 종속성으로 추가하기만 하면 됩니다. “소셜” 로그인(GitHub에 위임)을 하고자 하므로 Spring Security OAuth 2.0 클라이언트 스타터를 포함해야 합니다:

pom.xml

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

이를 추가하면 기본적으로 OAuth 2.0으로 앱이 보호됩니다.

다음으로 GitHub를 인증 공급자로 사용하도록 앱을 구성해야 합니다. 이렇게 하려면 다음을 수행합니다:

Add a New GitHub App

로그인에 GitHub의 OAuth 2.0 인증 시스템을 사용하려면 먼저 새 GitHub 앱 추가를 해야 합니다.

“New OAuth App”을 선택하면 “Register a new OAuth application” 페이지가 표시됩니다. 앱 이름과 설명을 입력합니다. 그런 다음 앱의 홈 페이지를 입력합니다(이 경우 http://localhost:8080). 마지막으로 인증 콜백 URL을 ‘http://localhost:8080/login/oauth2/code/github`로 지정하고 신청서 등록을 클릭합니다.

OAuth 리디렉션 URI는 최종 사용자의 사용자 에이전트가 GitHub로 인증하고 애플리케이션 인증 페이지에서 애플리케이션에 대한 액세스 권한을 부여한 후 다시 리디렉션되는 애플리케이션의 경로입니다.

기본 리디렉션 URI 템플릿은 {baseUrl}/login/oauth2/code/{registrationId}입니다. registrationIdClientRegistration의 고유 식별자입니다.

Configure application.yml

그런 다음 GitHub에 대한 링크를 만들려면 ‘application.yml’에 다음을 추가합니다:

application.yml

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            clientId: github-client-id
            clientSecret: github-client-secret
# ...

방금 생성한 OAuth 2.0 자격 증명을 사용하여 github-client-id를 클라이언트 ID로, github-client-secret을 클라이언트 비밀 번호로 바꾸기만 하면 됩니다.

Boot Up the Application

이렇게 변경한 후 앱을 다시 실행하고 http://localhost:8080의 홈 페이지를 방문하면 됩니다. 이제 홈 페이지 대신 GitHub로 로그인하라는 리디렉션이 표시됩니다. 그렇게 한 후 요청하는 모든 권한을 수락하면 로컬 앱으로 다시 리디렉션되고 홈 페이지가 표시됩니다.

GitHub에 로그인한 상태를 유지하면 쿠키와 캐시된 데이터가 없는 새 브라우저에서 이 로컬 앱을 열어도 다시 인증할 필요가 없습니다. (이것이 바로 싱글 사인온의 의미입니다.)

이 섹션에서 샘플 애플리케이션으로 작업하는 경우 브라우저 캐시에서 쿠키와 HTTP 기본 자격 증명을 지워야 합니다. 단일 서버에서 이를 수행하는 가장 좋은 방법은 새 비공개 창을 여는 것입니다.

로컬에서 실행 중인 앱만 토큰을 사용할 수 있고 요청하는 범위가 제한되어 있으므로 이 샘플에 대한 액세스 권한을 부여하는 것이 안전합니다. 하지만 이와 같은 앱에 로그인할 때 승인하는 항목에 유의하세요: 앱에서 사용자가 원하는 것 이상의 권한을 요청할 수 있습니다(예: 사용자가 원하지 않을 수 있는 개인 데이터 변경 권한을 요청할 수 있음).

방금 무슨 일이 일어났나요?

방금 작성한 앱은 OAuth 2.0 용어로 _클라이언트 애플리케이션_이며, 인증 코드 부여를 사용하여 GitHub(인증 서버)에서 액세스 토큰을 얻습니다.

그런 다음 액세스 토큰을 사용하여 로그인 ID와 이름을 포함한 몇 가지 개인 정보(사용자가 허용한 것만)를 GitHub에 요청합니다. 이 단계에서 GitHub는 리소스 서버 역할을 수행하여 사용자가 전송한 토큰을 디코딩하고 앱에 사용자 세부 정보에 액세스할 수 있는 권한을 부여하는지 확인합니다. 이 프로세스가 성공하면 앱은 사용자 세부 정보를 Spring Security 컨텍스트에 삽입하여 사용자가 인증되도록 합니다.

브라우저 도구(Chrome 또는 Firefox의 경우 F12)에서 모든 홉에 대한 네트워크 트래픽을 따라가 보면 GitHub로 리디렉션되는 것을 볼 수 있으며, 마지막으로 새로운 Set-Cookie 헤더가 있는 홈 페이지로 다시 돌아옵니다. 이 쿠키(기본값은 JSESSIONID)는 Spring(또는 모든 서블릿 기반) 애플리케이션에 대한 인증 세부 정보를 위한 토큰입니다.

따라서 사용자가 콘텐츠를 보려면 외부 공급자(GitHub)를 통해 인증해야 한다는 의미에서 안전한 애플리케이션을 갖게 됩니다.

인터넷 뱅킹 웹사이트에는 사용하지 않을 것입니다. 하지만 기본적인 식별 목적과 사이트의 여러 사용자 간에 콘텐츠를 분리하는 데는 훌륭한 출발점이 될 수 있습니다. 그렇기 때문에 요즘 이러한 종류의 인증이 매우 인기가 있습니다.

다음 섹션에서는 애플리케이션에 몇 가지 기본 기능을 추가하겠습니다. 또한 사용자가 초기 리디렉션을 통해 GitHub로 이동할 때 어떤 일이 일어나는지 좀 더 명확하게 알 수 있도록 할 것입니다.

Add a Welcome Page

이 섹션에서는 방금 빌드한 simple 앱을 수정하여 GitHub로 로그인하는 명시적 링크를 추가합니다. 즉시 리디렉션되는 대신 새 링크가 홈 페이지에 표시되며, 사용자는 로그인하거나 인증되지 않은 상태로 유지하도록 선택할 수 있습니다. 사용자가 링크를 클릭한 경우에만 보안 콘텐츠가 렌더링됩니다.

홈페이지의 조건부 콘텐츠

사용자가 인증된 상태에서 콘텐츠를 렌더링하려면 서버 측 렌더링 또는 클라이언트 측 렌더링 중 하나를 선택할 수 있습니다.

여기서는 클라이언트 측을 JQuery로 변경하지만, 다른 것을 사용하려는 경우 클라이언트 코드를 번역하는 것은 그리 어렵지 않습니다.

동적 콘텐츠를 시작하려면 다음과 같이 몇 가지 HTML 요소를 표시해야 합니다:

index.html

<div class="container unauthenticated">
    With GitHub: <a href="/oauth2/authorization/github">click here</a>
</div>
<div class="container authenticated" style="display:none">
    Logged in as: <span id="user"></span>
</div>

기본적으로 첫 번째 <div>는 표시되고 두 번째는 표시되지 않습니다. id 속성이 있는 빈 <span>도 주목하세요.

잠시 후 로그인한 사용자 세부 정보를 JSON으로 반환하는 서버 측 엔드포인트를 추가합니다.

하지만 먼저 해당 엔드포인트에 도달할 다음 JavaScript를 추가합니다. 엔드포인트의 응답에 따라 이 자바스크립트는 <span> 태그를 사용자 이름으로 채우고 <div>를 적절하게 토글합니다:

index.html

<script type="text/javascript">
    $.get("/user", function(data) {
        $("#user").html(data.name);
        $(".unauthenticated").hide()
        $(".authenticated").show()
    });
</script>

이 자바스크립트는 서버 측 엔드포인트가 /user로 호출될 것으로 예상합니다.

The /user Endpoint

이제 방금 언급한 서버 측 엔드포인트를 추가하여 /user라고 부릅니다. 이것은 현재 로그인한 사용자를 반환할 것이며, 이는 메인 클래스에서 아주 쉽게 할 수 있습니다:

SocialApplication.java

@SpringBootApplication
@RestController
public class SocialApplication {

    @GetMapping("/user")
    public Map<String, Object> user(@AuthenticationPrincipal OAuth2User principal) {
        return Collections.singletonMap("name", principal.getAttribute("name"));
    }

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

}

핸들러 메서드에 주입된 @RestController, @GetMapping, OAuth2User의 사용에 유의하세요.

브라우저 클라이언트에 공개하고 싶지 않은 정보가 포함될 수 있으므로 엔드포인트에서 전체 OAuth2User를 반환하는 것은 좋은 생각이 아닙니다.

홈페이지 공개하기

마지막으로 한 가지 변경해야 할 사항이 있습니다.

이제 이 앱은 이전처럼 정상적으로 작동하고 인증되지만 페이지를 표시하기 전에 리디렉션됩니다. 링크를 표시하려면 WebSecurityConfigurerAdapter를 확장하여 홈 페이지에서 보안을 해제해야 합니다:

SocialApplication

@SpringBootApplication
@RestController
public class SocialApplication extends WebSecurityConfigurerAdapter {

    // ...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	// @formatter:off
        http
            .authorizeRequests(a -> a
                .antMatchers("/", "/error", "/webjars/**").permitAll()
                .anyRequest().authenticated()
            )
            .exceptionHandling(e -> e
                .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
            )
            .oauth2Login();
        // @formatter:on
    }

}

Spring Boot는 @SpringBootApplication으로 주석이 달린 클래스의 WebSecurityConfigurerAdapter에 특별한 의미를 부여합니다: 이 클래스를 사용하여 OAuth 2.0 인증 프로세서를 전달하는 보안 필터 체인을 구성합니다.

위의 구성은 허용된 엔드포인트의 화이트리스트를 나타내며, 다른 모든 엔드포인트는 인증이 필요합니다.

허용하려면:

  • / 방금 동적으로 만든 페이지이므로 인증되지 않은 사용자에게 일부 콘텐츠가 표시됩니다.

  • /error 오류를 표시하기 위한 Spring Boot 엔드포인트이며

  • /webjars/** 인증 여부와 관계없이 모든 방문자에 대해 자바스크립트가 실행되기를 원할 것이기 때문입니다.

하지만 이 구성에서는 /user에 대한 내용은 표시되지 않습니다. /user를 포함한 모든 항목은 마지막에 .anyRequest().authenticated() 구성으로 인해 표시되지 않는 한 안전하게 유지됩니다.

마지막으로 Ajax를 통해 백엔드와 인터페이스하기 때문에 로그인 페이지로 리디렉션하는 기본 동작 대신 401로 응답하도록 엔드포인트를 구성해야 합니다. authenticationEntryPoint를 구성하면 이를 달성할 수 있습니다.

이러한 변경 사항을 적용하면 애플리케이션이 완성되며, 애플리케이션을 실행하고 홈 페이지를 방문하면 멋진 스타일의 HTML 링크가 “login with GitHub”으로 표시될 것입니다. 이 링크는 GitHub로 바로 연결되는 것이 아니라 인증을 처리하는 로컬 경로로 이동합니다(GitHub로 리디렉션). 인증이 완료되면 로컬 앱으로 다시 리디렉션되며, 이제 로컬 앱에 이름이 표시됩니다(GitHub에서 해당 데이터에 대한 액세스를 허용하도록 권한을 설정했다고 가정할 때).

Add a Logout Button

이 섹션에서는 사용자가 앱에서 로그아웃할 수 있는 버튼을 추가하여 우리가 만든 클릭 앱을 수정합니다. 간단한 기능처럼 보이지만 구현하는 데 약간의 주의가 필요하므로 정확한 방법을 설명하는 데 시간을 할애할 가치가 있습니다. 대부분의 변경 사항은 앱을 읽기 전용 리소스에서 읽기/쓰기 리소스로 변환하는 것과 관련이 있으므로(로그아웃하려면 상태 변경이 필요함) 정적 콘텐츠가 아닌 실제 애플리케이션에서도 동일한 변경이 필요할 것입니다.

Client Side Changes

클라이언트에서는 로그아웃 버튼과 인증 취소를 요청하기 위해 서버에 다시 호출할 자바스크립트만 제공하면 됩니다. 먼저 UI의 “authenticated” 섹션에 버튼을 추가합니다:

index.html

<div class="container authenticated">
  Logged in as: <span id="user"></span>
  <div>
    <button onClick="logout()" class="btn btn-primary">Logout</button>
  </div>
</div>

를 호출한 다음 자바스크립트에서 참조하는 logout() 함수를 제공합니다:

index.html

var logout = function() {
    $.post("/logout", function() {
        $("#user").html('');
        $(".unauthenticated").show();
        $(".authenticated").hide();
    })
    return true;
}

logout() 함수는 /logout`에 POST를 수행한 다음 동적 콘텐츠를 지웁니다. 이제 서버 측으로 전환하여 해당 엔드포인트를 구현할 수 있습니다.

Adding a Logout Endpoint

Spring Security는 세션을 지우고 쿠키를 무효화하는 올바른 작업을 수행하는 /logout 엔드포인트를 기본적으로 지원합니다. 엔드포인트를 구성하려면 WebSecurityConfigurerAdapter의 기존 configure() 메서드를 확장하기만 하면 됩니다:

SocialApplication.java

@Override
protected void configure(HttpSecurity http) throws Exception {
	// @formatter:off
    http
        // ... existing code here
        .logout(l -> l
            .logoutSuccessUrl("/").permitAll()
        )
        // ... existing code here
    // @formatter:on
}

로그아웃` 엔드포인트를 사용하려면 해당 엔드포인트에 POST를 해야 하며, 크로스 사이트 요청 위조(CSRF, “sea surf”로 발음)로부터 사용자를 보호하기 위해 요청에 토큰을 포함시켜야 합니다. 토큰의 값은 현재 세션에 연결되어 보호 기능을 제공하므로 해당 데이터를 자바스크립트 앱으로 가져올 방법이 필요합니다.

많은 자바스크립트 프레임워크는 CSRF를 기본적으로 지원하지만(예: Angular에서는 XSRF라고 부름), Spring Security의 기본 동작과는 약간 다른 방식으로 구현되는 경우가 많습니다. 예를 들어, Angular에서 프런트 엔드는 서버가 “XSRF-TOKEN”이라는 쿠키를 보내기를 원하며, 서버가 이를 확인하면 “X-XSRF-TOKEN”이라는 헤더로 값을 다시 보냅니다. 간단한 jQuery 클라이언트로 동일한 동작을 구현할 수 있으며, 서버 측 변경 사항은 변경 사항이 없거나 거의 없이 다른 프런트 엔드 구현과 함께 작동합니다. Spring Security에 이를 알려주려면 쿠키를 생성하는 필터를 추가해야 합니다.

WebSecurityConfigurerAdapter`에서 다음을 수행합니다:

SocialApplication.java

@Override
protected void configure(HttpSecurity http) throws Exception {
	// @formatter:off
    http
        // ... existing code here
        .csrf(c -> c
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
        )
        // ... existing code here
    // @formatter:on
}

Adding the CSRF Token in the Client

이 샘플에서는 상위 프레임워크를 사용하지 않으므로 방금 백엔드에서 쿠키로 사용할 수 있도록 만든 CSRF 토큰을 명시적으로 추가해야 합니다. 코드를 좀 더 간단하게 만들려면 js-cookie 라이브러리를 포함하세요:

pom.xml

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>js-cookie</artifactId>
    <version>2.1.0</version>
</dependency>

그런 다음 HTML에서 참조할 수 있습니다:

index.html

<script type="text/javascript" src="/webjars/js-cookie/js.cookie.js"></script>

마지막으로 XHR에서 Cookies를 통해 편리한 방법을 사용할 수 있습니다:

index.html

$.ajaxSetup({
  beforeSend : function(xhr, settings) {
    if (settings.type == 'POST' || settings.type == 'PUT'
        || settings.type == 'DELETE') {
      if (!(/^http:.*/.test(settings.url) || /^https:.*/
        .test(settings.url))) {
        // Only send the token to relative URLs i.e. locally.
        xhr.setRequestHeader("X-XSRF-TOKEN",
          Cookies.get('XSRF-TOKEN'));
      }
    }
  }
});

준비 완료!

이러한 변경 사항이 적용되었으므로 앱을 실행하고 새로운 로그아웃 버튼을 사용해 볼 준비가 되었습니다. 앱을 시작하고 새 브라우저 창에서 홈 페이지를 로드합니다. “로그인” 링크를 클릭하면 GitHub로 이동합니다(이미 로그인한 경우 리디렉션이 표시되지 않을 수 있음). “로그아웃” 버튼을 클릭하면 현재 세션이 취소되고 앱이 인증되지 않은 상태로 돌아갑니다. 궁금하다면 브라우저가 로컬 서버와 교환하는 요청에서 새로운 쿠키와 헤더를 볼 수 있을 것입니다.

이제 로그아웃 엔드포인트가 브라우저 클라이언트와 함께 작동하면 다른 모든 HTTP 요청(POST, PUT, DELETE 등)도 정상적으로 작동한다는 점을 기억하세요. 따라서 좀 더 현실적인 기능을 갖춘 애플리케이션을 위한 좋은 플랫폼이 될 것입니다.

Login with Google

이 섹션에서는 이미 구축한 로그아웃 앱을 수정하여 최종 사용자가 여러 자격 증명 세트 중에서 선택할 수 있도록 스티커 페이지를 추가합니다.

최종 사용자를 위한 두 번째 옵션으로 Google을 추가해 보겠습니다.

초기 설정

로그인에 Google의 OAuth 2.0 인증 시스템을 사용하려면 Google API 콘솔에서 프로젝트를 설정하여 OAuth 2.0 자격 증명을 얻어야 합니다.

인증을 위한 Google의 OAuth 2.0 구현OpenID Connect 1.0 사양을 준수하며 OpenID 인증을 받았습니다.

OpenID Connect 페이지의 “OAuth 2.0 설정하기” 섹션부터 지침을 따르세요.

“OAuth 2.0 자격 증명 가져오기” 안내를 완료하면 클라이언트 ID와 클라이언트 비밀로 구성된 자격 증명을 가진 새로운 OAuth 클라이언트가 생성됩니다.

리디렉션 URI 설정

또한 앞서 GitHub에서 했던 것처럼 리디렉션 URI를 제공해야 합니다.

“리디렉션 URI 설정” 하위 섹션에서 인증된 리디렉션 URI 필드가 http://localhost:8080/login/oauth2/code/google로 설정되어 있는지 확인합니다.

클라이언트 등록 추가

그런 다음 Google을 가리키도록 클라이언트를 구성해야 합니다. Spring Security는 여러 클라이언트를 염두에 두고 구축되었기 때문에 GitHub용으로 생성한 자격 증명과 함께 Google 자격 증명을 추가할 수 있습니다:

application.yml

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            clientId: github-client-id
            clientSecret: github-client-secret
          google:
            client-id: google-client-id
            client-secret: google-client-secret

보시다시피, Google은 Spring Security가 기본으로 지원하는 또 다른 제공업체입니다.

로그인 링크 추가

클라이언트에서 변경은 간단합니다. 다른 링크를 추가하기만 하면 됩니다:

index.html

<div class="container unauthenticated">
  <div>
    With GitHub: <a href="/oauth2/authorization/github">click here</a>
  </div>
  <div>
    With Google: <a href="/oauth2/authorization/google">click here</a>
  </div>
</div>

URL의 최종 경로는 application.yml의 클라이언트 등록 ID와 일치해야 합니다.

Spring Security는 기본 제공자 선택 페이지와 함께 제공되며, 이 페이지는 /oauth2/authorization/{registrationId} 대신 /login을 가리키면 연결할 수 있습니다.

로컬 사용자 데이터베이스 추가하기

인증이 외부 공급자에게 위임된 경우에도 많은 애플리케이션은 사용자에 대한 데이터를 로컬에 보관해야 합니다. 여기서는 코드를 보여주지 않지만 두 단계로 쉽게 할 수 있습니다.

  1. 데이터베이스의 백엔드를 선택하고, 필요에 적합하고 외부 인증을 통해 전체 또는 부분적으로 채울 수 있는 사용자 지정 User 객체에 대한 리포지토리(예: Spring 데이터 사용)를 설정합니다.

  2. 인증 서버와 데이터베이스를 호출하기 위해 OAuth2UserService를 구현하고 노출합니다. 구현을 기본 구현에 위임하면 권한 부여 서버 호출의 무거운 작업을 수행할 수 있습니다. 구현은 사용자 지정 User 개체를 확장하고 OAuth2User를 구현하는 것을 반환해야 합니다.

힌트: 외부 공급업체의 고유 식별자(사용자 이름이 아니라 외부 공급업체의 계정에 고유한 것)에 연결할 수 있도록 User 개체에 필드를 추가하세요.

미인증 사용자에 대한 오류 페이지 추가

이 섹션에서는 앞서 만든 두 공급자 앱을 수정하여 인증할 수 없는 사용자에게 몇 가지 피드백을 제공하겠습니다. 동시에 인증 로직을 확장하여 사용자가 특정 GitHub 조직에 속한 경우에만 사용자를 허용하는 규칙을 포함하도록 합니다. “organization”은 GitHub 도메인에 특정한 개념이지만 다른 제공업체에서도 유사한 규칙을 고안할 수 있습니다. 예를 들어 Google의 경우 특정 도메인의 사용자만 인증하고 싶을 수 있습니다.

GitHub로 전환하기

두 공급자 샘플은 GitHub를 OAuth 2.0 공급자로 사용합니다:

application.yml

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: bd1c0a783ccdd1c9b9e4
            client-secret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
          # ...

클라이언트에서 인증 실패 감지하기

클라이언트에서 인증에 실패한 사용자에게 몇 가지 피드백을 제공하고 싶을 수 있습니다. 이를 용이하게 하기 위해 정보 메시지를 추가할 DIV를 추가할 수 있습니다.

index.html

<div class="container text-danger error"></div>

그런 다음 /error 엔드포인트에 호출을 추가하여 <div>에 결과를 채웁니다:

index.html

$.get("/error", function(data) {
    if (data) {
        $(".error").html(data);
    } else {
        $(".error").html('');
    }
});

오류 함수는 표시할 오류가 있는지 백엔드에 확인합니다.

Adding an Error Message

오류 메시지 검색을 지원하려면 인증이 실패할 때 오류 메시지를 캡처해야 합니다. 이를 위해 다음과 같이 AuthenticationFailureHandler를 구성할 수 있습니다:

protected void configure(HttpSecurity http) throws Exception {
	// @formatter:off
	http
	    // ... existing configuration
	    .oauth2Login(o -> o
            .failureHandler((request, response, exception) -> {
			    request.getSession().setAttribute("error.message", exception.getMessage());
			    handler.onAuthenticationFailure(request, response, exception);
            })
        );
}

위와 같이 하면 인증에 실패할 때마다 세션에 오류 메시지가 저장됩니다.

그런 다음 다음과 같이 간단한 /error 컨트롤러를 추가할 수 있습니다:

SocialApplication.java

@GetMapping("/error")
public String error(HttpServletRequest request) {
	String message = (String) request.getSession().getAttribute("error.message");
	request.getSession().removeAttribute("error.message");
	return message;
}

이렇게 하면 앱의 기본 /error 페이지가 대체되는데, 저희의 경우에는 괜찮지만 사용자의 요구에 충분히 정교하지 않을 수 있습니다.

서버에서 401 생성

사용자가 GitHub에 로그인할 수 없거나 원하지 않는 경우 Spring Security에서 이미 401 응답을 보내므로, 인증에 실패(예: 토큰 부여 거부)해도 앱은 이미 작동하고 있는 것입니다.

인증 규칙을 확장하여 올바른 조직에 속하지 않은 사용자를 거부할 수 있습니다.

GitHub API를 사용하여 사용자에 대한 자세한 정보를 찾을 수 있으므로 이를 인증 프로세스의 올바른 부분에 연결하기만 하면 됩니다.

다행히도 이러한 간단한 사용 사례에 대해 Spring Boot는 OAuth2UserService 유형의 @Bean을 선언하면 사용자 주체를 식별하는 데 사용되는 쉬운 확장 지점을 제공합니다. 이 훅을 사용하여 사용자가 올바른 조직에 속해 있는지 확인하고 그렇지 않은 경우 예외를 던질 수 있습니다:

SocialApplication.java

@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService(WebClient rest) {
    DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
    return request -> {
        OAuth2User user = delegate.loadUser(request);
        if (!"github".equals(request.getClientRegistration().getRegistrationId())) {
        	return user;
        }

        OAuth2AuthorizedClient client = new OAuth2AuthorizedClient
                (request.getClientRegistration(), user.getName(), request.getAccessToken());
        String url = user.getAttribute("organizations_url");
        List<Map<String, Object>> orgs = rest
                .get().uri(url)
                .attributes(oauth2AuthorizedClient(client))
                .retrieve()
                .bodyToMono(List.class)
                .block();

        if (orgs.stream().anyMatch(org -> "spring-projects".equals(org.get("login")))) {
            return user;
        }

        throw new OAuth2AuthenticationException(new OAuth2Error("invalid_token", "Not in Spring Team", ""));
    };
}

이 코드는 인증된 사용자를 대신하여 GitHub API에 액세스하기 위해 WebClient 인스턴스에 종속되어 있다는 점에 유의하세요. 그런 다음 조직을 반복하여 “spring-projects”(Spring 오픈 소스 프로젝트를 저장하는 데 사용되는 조직)와 일치하는 조직을 찾습니다. 인증에 성공하고 싶지만 Spring 엔지니어링 팀에 속해 있지 않은 경우 자신의 값으로 대체할 수 있습니다. 일치하는 항목이 없으면 OAuth2AuthenticationException이 발생하고, Spring Security에서 이를 포착하여 401 응답으로 반환합니다.

WebClient도 빈으로 생성해야 하지만, spring-boot-starter-oauth2-client를 사용했기 때문에 그 구성 요소는 모두 자동 생성 가능하기 때문에 사소한 문제입니다:

@Bean
public WebClient rest(ClientRegistrationRepository clients, OAuth2AuthorizedClientRepository authz) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(clients, authz);
    return WebClient.builder()
            .filter(oauth2).build();
}

물론 위의 코드는 다른 인증 규칙으로 일반화할 수 있으며, 일부는 GitHub에, 일부는 다른 OAuth 2.0 공급자에게 적용될 수 있습니다. 필요한 것은 WebClient와 공급자의 API에 대한 약간의 지식뿐입니다.

결론

지금까지 스프링 부트와 스프링 시큐리티를 사용하여 아주 적은 노력으로 다양한 스타일의 앱을 빌드하는 방법을 살펴봤습니다. 모든 샘플을 관통하는 주요 테마는 외부 OAuth 2.0 공급자를 사용한 인증입니다.

모든 샘플 앱은 일반적으로 구성 파일 변경만으로 보다 구체적인 사용 사례에 맞게 쉽게 확장 및 재구성할 수 있습니다. 자체 서버에서 샘플 버전을 사용하여 GitHub(또는 이와 유사한 서비스)에 등록하고 자체 호스트 주소에 대한 클라이언트 자격 증명을 얻는 경우 기억하세요. 그리고 해당 자격 증명을 소스 제어에 넣지 마세요!

댓글남기기