1. 서론
을 번역해보는 작업을 실시하였습니다. 이유는
- 코드를 한번 돌려보았는데 완벽하게 내가 원하는 작업과 일치하였기 때문입니다.
- 최신 코드이기 때문에 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>
</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. 결론
첫번째 클라이언트 앱까지 구성해 보았습니다. 다음 시간에는 두번째 클라이언트 앱을 구성하고, 인증서버, 리소스 서버를 구성해본 것을 번역해보겠습니다.
'Dev > SpringBoot' 카테고리의 다른 글
21. [SpringBoot] 간단하게 비밀번호 암호화해보기 (2) | 2020.10.01 |
---|---|
20. [SpringBoot] 버전 걱정 없는 SSO 구현 번역해보기 - 2 (3) | 2020.09.22 |
18. [SpringBoot] 환경설정별로 다르게 실행해보자 (0) | 2020.09.09 |
17. [SpringBoot] 스프링부트 카카오 로그인하기 구현(따라치기만하면됨)(2) (1) | 2020.07.24 |
16. [SpringBoot] 스프링부트 카카오 로그인하기 구현(따라치기만하면됨)(1) (0) | 2020.07.23 |