본문 바로가기
Dev/SpringBoot

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

by VIPeveloper 2020. 9. 21.
반응형

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. 결론

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

 

 

 

 

 

반응형