Dev/SpringBoot

19. [SpringBoot] 버전 걱정 없는 SSO 구현 번역해보기 - 1

VIPeveloper 2020. 9. 21. 13:06
728x90
반응형

1. 서론

 

Simple Single Sign-On with Spring Security OAuth2 | Baeldung

A simple SSO implementation using Spring Security 5 and Boot.

www.baeldung.com

을 번역해보는 작업을 실시하였습니다. 이유는

  1. 코드를 한번 돌려보았는데 완벽하게 내가 원하는 작업과 일치하였기 때문입니다.
  2. 최신 코드이기 때문에 decperated 된 어노테이션이 없을 것이라고 생각하였습니다.
  • SSO 코드를 여러개 분석하면서 버전이 다르거나 변경된 코드로 인해 고생을 많이 했었고, 최신 코드로 하면 변경이 없을 것 같아 도전해보게 되었습니다.(번역해보니 이전 버전은 점점 사양화되고있다고 한다.)

2. 본론

2.1 개요

  • 이 튜토리얼에서는, 우리는 Keycloak을 인증서버로서 사용한 SSO(Security OAuth와 Springboot를 사용)를 구현할 것입니다.
  • 프로젝트에서는 4개의 분리된 앱을 사용합니다.
    • 인증서버 : 인증 매커니즘을 적용
    • 리소스 서버 : Foo에 대한 정보 제공
    • 2개의 클라이언트 앱 : SSO를 사용함
  • 순서
    • 유저가 클라이언트 앱을 통해서 리소스 서버로 접속하고자 할 때, 인증을 위해 인증서버로 먼저 리다이랙트됩니다.
    • Keycloak은 유저를 등록하고, 유저가 첫번째 앱에 로그인되어있다면, 두번째 앱이 같은 브라우저에 접속하고자 할 때, 유저는 자격증명을 다시 할 필요가 없습니다.
    • 우리는 OAuth stack을 위해 Spring Security 5를 이용할 것입니다. 그럼에도 만약 당신이 이전 버전을 사용하길 원한다면, 이전 기사를 보면 됩니다. (여기)

2.2 인증서버

  • OAuth stack은 버전이 바뀜에 따라 사양화될 것이기에, 우리는 이제 인증서버로 Keycloak라는 것을 이용할 것입니다.
  • 또한 클라이언트-1, 클라이언트-2를 각각 구성하였습니다.

2.3 리소스서버

  • 우리는 리소스 서버나 REST API 형식으로 제공될 리소스 서버가 필요하며, 예제에서는 Foo 리소스를 보여줄 예정입니다.

2.4 클라이언트 앱

  • 역시나 타임리프로 구현되어있습니다. 간단한설정과 함께 구현되어있고, SSO 동작을 잘 보여줄 수 있도록 기능적으로 설계하였습니다.

멀티모듈로 구성된 프로젝트

이렇게 4개의 앱으로 구성이 되어있습니다. maven 을 사용하였습니다.

2.4.1 pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<description>New OAuth2 Stack in Spring Security 5</description>

	<groupId>com.baeldung</groupId>
	<artifactId>oauth-sso</artifactId>
	<version>0.1.0-SNAPSHOT</version>
	<packaging>pom</packaging>

	<modules>
		<module>sso-authorization-server</module>
		<module>sso-resource-server</module>
		<module>sso-client-app-1</module>
		<module>sso-client-app-2</module>
	</modules>

</project>

2.4.2 client pom.xml

먼저, 클라이언트 pom.xml 부터 분석해보도록 하겠습니다. 

RestTemplate가 사양화됨에 따라, WebClient로 대체하기로 하였고, 이 때문에 spring-webflux reactor-netty를 추가해준다고 합니다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.baeldung</groupId>
	<artifactId>sso-client-app-1</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>sso-client-app-1</name>
	<packaging>jar</packaging>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.6.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>

	<dependencies>

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

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

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

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

		<dependency>
			<groupId>org.thymeleaf.extras</groupId>
			<artifactId>thymeleaf-extras-springsecurity5</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webflux</artifactId>
		</dependency>

		<dependency>
			<groupId>io.projectreactor.netty</groupId>
			<artifactId>reactor-netty</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-surefire-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>**/*LiveTest.java</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>13</java.version>
	</properties>

</project>

 

보기만해도 마음이 편안해지는 spring-boot 2.2.6버전입니다. 그동안 1.5.2 ~ 2.0.3 버전들의 예제에서 사양화된 설정을 대체하느라 힘들었던 기억을 생각하니 눈물이 또륵 흘렀습니다.

2.4.3 보안 설정

가장 중요한 부분입니다. 첫번째 클라이언트 앱의 보안 설정부분입니다.

package com.baeldung.client.spring;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;

@EnableWebSecurity
public class UiSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {// @formatter:off
		http.authorizeRequests().antMatchers("/", "/login**").permitAll().anyRequest().authenticated().and()
				.oauth2Login();

	}// @formatter:on

    @Bean
    WebClient webClient(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) {
        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository, authorizedClientRepository);
        oauth2.setDefaultOAuth2AuthorizedClient(true);
        return WebClient.builder()
            .apply(oauth2.oauth2Configuration())
            .build();
    }

}
  • 설정에서 가장 중요한 부분은 oauth2Login() 메서드입니다.
  • 스프링 시큐리티 OAuth2.0 로그인 부분을 지원합니다. 우리가 Keycloak(Web app과 RESTful 웹 서비스를 위한 기본 SSO 솔루션)을 사용하기 때문에, 우리는 SSO에 대한 더이상의 설정은 필요 없습니다.(와우)
  • 마지막으로, 우리는 간단한 HTTP Client로서 요청사항을 우리의 리소스 서버로 보내기 위한 요청을 컨트롤하기위해 WebClient 빈을 설정해줍니다.
  • 웹 클라이언트 설정은 application.yml 에 저장해놓습니다.
  • 그럼 바로 application.yml로 가볼까요~?
spring:
  security:
    oauth2:
      client:
        registration:
          custom:
            client-id: ssoClient-1
            client-secret: ssoClientSecret-1
            scope: read,write
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8082/ui-one/login/oauth2/code/custom
        provider:
          custom:
            authorization-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/auth
            token-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token
            user-info-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/userinfo
            user-name-attribute: preferred_username
  thymeleaf:
    cache: false
  cache:
    type: NONE
    
server: 
  port: 8082
  servlet: 
    context-path: /ui-one
    
logging: 
  level: 
    org.springframework: INFO

resourceserver:
  api:
    url: http://localhost:8081/sso-resource-server/api/foos/
              
  • spring.security.oauth2.client.registration 은 클라이언트 등록의 root namespace 입니다.
  • 이 프로젝트에서는 클라이언트 등록 id를 custom 이라고 정의했습니다.
  • 또한, client-id, client-secret, scope, authorization-grant-type, redirect-uri 등으로 인증에 필요한 요건들을 미리 정의해놓았습니다.
  • 이후, 우리는 인증서버를 위한 provider 를 정의합니다. 지금은 custom 하나 뿐이지만, 깃허브나, 페이스북을 추가할 수도 있습니다.

2.4.4. 컨트롤러

리소스 서버로부터 Foos 객체를 받아오기 위한 클라이언트 앱을 컨트롤러에 구현해봅시다.

package com.baeldung.client.web.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.reactive.function.client.WebClient;

import com.baeldung.client.web.model.FooModel;

@Controller
public class FooClientController {

    // http://localhost:8081/sso-resource-server/api/foos/
    @Value("${resourceserver.api.url}")
    private String fooApiUrl;

    @Autowired
    private WebClient webClient;

    @GetMapping("/foos")
    public String getFoos(Model model) {
        List<FooModel> foos = this.webClient.get()
            .uri(fooApiUrl)
            .retrieve()
            .bodyToMono(new ParameterizedTypeReference<List<FooModel>>() {
            })
            .block();
        model.addAttribute("foos", foos);
        return "foos";
    }

}

foo template을 가져오기 위한 하나의 메서드만 필요합니다. 우리는 다른 로그인을 위한 코드는 필요하지 않습니다!

구현 설계 시, 로그인 로직이 끝나는 것을 넣으면 될 듯 하다. fooApiUrl에 로그인 uri를 넣어 실제 서비스와도 연동해보면 될 것 같네요.

2.4.5 프론트엔드

 클라이언트 앱의 프론트엔드 구성을 해봅시다.

구성은 매우 간단합니다.

index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Spring OAuth Client Thymeleaf - 1</title>
<link rel="stylesheet"
	href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" />
</head>
<body>
	<nav
		class="navbar navbar-expand-lg navbar-light bg-light shadow-sm p-3 mb-5">
		<a class="navbar-brand" th:href="@{/foos/}">Spring OAuth Client
			Thymeleaf - 1</a>
	</nav>

	<div class="container">
		<label>Welcome ! </label> <br /> <a th:href="@{/foos/}"
			class="btn btn-primary">Login</a>
	</div>
</body>
</html>

그냥 단순히 로그인 버튼 하나가 있습니다. 클릭하면, 로그인 인증페이지가 나옵니다. 이건 추후 구현할 것이니 일단 넘어갑시다.

 

foos.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Spring OAuth Client Thymeleaf - 1</title>
<link rel="stylesheet"
	href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" />
</head>
<body>
	<nav
		class="navbar navbar-expand-lg navbar-light bg-light shadow-sm p-3 mb-5">
		<a class="navbar-brand" th:href="@{/foos/}">Spring OAuth Client
			Thymeleaf -1</a>
		<ul class="navbar-nav ml-auto">
			<li class="navbar-text">Hi, <span sec:authentication="name">preferred_username</span>&nbsp;&nbsp;&nbsp;
			</li>
		</ul>
	</nav>

	<div class="container">
		<h1>All Foos:</h1>
		<table class="table table-bordered table-striped">
			<thead>
				<tr>
					<td>ID</td>
					<td>Name</td>
				</tr>
			</thead>
			<tbody>
				<tr th:if="${foos.empty}">
					<td colspan="4">No foos</td>
				</tr>
				<tr th:each="foo : ${foos}">
					<td><span th:text="${foo.id}"> ID </span></td>
					<td><span th:text="${foo.name}"> Name </span></td>
				</tr>
			</tbody>
		</table>
	</div>

</body>
</html>

이 페이지는 인증이 필요한 페이지입니다 .인증이 안되어있으면, 로그인 페이지로 리다이렉트 됩니다.

 

3. 결론

첫번째 클라이언트 앱까지 구성해 보았습니다. 다음 시간에는 두번째 클라이언트 앱을 구성하고, 인증서버, 리소스 서버를 구성해본 것을 번역해보겠습니다.

 

 

 

 

 

728x90
반응형