Java Spring Boot 애플리케이션에 인증 (Authentication)을 추가하세요
이 가이드는 Logto를 Java Spring Boot 애플리케이션에 통합하는 방법을 보여줍니다.
- 이 가이드의 샘플 코드는 spring-boot-sample GitHub 저장소에서 찾을 수 있습니다.
- Java Spring Boot 애플리케이션에 Logto를 통합하기 위해 공식 SDK가 필요하지 않습니다. 우리는 Logto와의 OIDC 인증 흐름을 처리하기 위해 Spring Security 및 Spring Security OAuth2 라이브러리를 사용할 것입니다.
사전 준비 사항
- Logto Cloud 계정 또는 셀프 호스팅 Logto.
- 우리의 샘플 코드는 Spring Boot securing web starter를 사용하여 생성되었습니다. 웹 애플리케이션이 없는 경우 새 웹 애플리케이션을 부트스트랩하는 지침을 따르세요.
- 이 가이드에서는 Logto와의 OIDC 인증 흐름을 처리하기 위해 Spring Security 및 Spring Security OAuth2 라이브러리를 사용할 것입니다. 개념을 이해하기 위해 공식 문서를 반드시 읽어보세요.
Java Spring Boot 애플리케이션 구성
종속성 추가
gradle 사용자는 build.gradle
파일에 다음 종속성을 추가하세요:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}
maven 사용자는 pom.xml
파일에 다음 종속성을 추가하세요:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
OAuth2 클라이언트 구성
Logto Console에서 새로운 Java Spring Boot
애플리케이션을 등록하고 웹 애플리케이션에 대한 클라이언트 자격 증명 및 IdP 구성을 얻으세요.
다음 구성을 application.properties
파일에 추가하세요:
spring.security.oauth2.client.registration.logto.client-name=logto
spring.security.oauth2.client.registration.logto.client-id={{YOUR_CLIENT_ID}}
spring.security.oauth2.client.registration.logto.client-secret={{YOUR_CLIENT_ID}}
spring.security.oauth2.client.registration.logto.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.logto.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.logto.scope=openid,profile,offline_access
spring.security.oauth2.client.registration.logto.provider=logto
spring.security.oauth2.client.provider.logto.issuer-uri={{LOGTO_ENDPOINT}}/oidc
spring.security.oauth2.client.provider.logto.authorization-uri={{LOGTO_ENDPOINT}}/oidc/auth
spring.security.oauth2.client.provider.logto.jwk-set-uri={{LOGTO_ENDPOINT}}/oidc/jwks
구현
세부 사항을 살펴보기 전에, 최종 사용자 경험에 대한 간단한 개요를 소개합니다. 로그인 과정은 다음과 같이 간소화될 수 있습니다:
- 귀하의 앱이 로그인 메서드를 호출합니다.
- 사용자는 Logto 로그인 페이지로 리디렉션됩니다. 네이티브 앱의 경우, 시스템 브라우저가 열립니다.
- 사용자가 로그인하고 귀하의 앱으로 다시 리디렉션됩니다 (리디렉션 URI로 구성됨).
리디렉션 기반 로그인에 관하여
- 이 인증 과정은 OpenID Connect (OIDC) 프로토콜을 따르며, Logto는 사용자 로그인을 보호하기 위해 엄격한 보안 조치를 시행합니다.
- 여러 앱이 있는 경우, 동일한 아이덴티티 제공자 (Logto)를 사용할 수 있습니다. 사용자가 한 앱에 로그인하면, Logto는 사용자가 다른 앱에 접근할 때 자동으로 로그인 과정을 완료합니다.
리디렉션 기반 로그인에 대한 이론적 배경과 이점에 대해 더 알고 싶다면, Logto 로그인 경험 설명을 참조하세요.
사용자가 로그인한 후 애플리케이션으로 다시 리디렉션되도록 하려면 이전 단계에서 client.registration.logto.redirect-uri
속성을 사용하여 리디렉션 URI를 설정해야 합니다.
리디렉션 URI 구성
Logto Console의 애플리케이션 세부 정보 페이지로 이동합니다. 리디렉션 URI http://localhost:3000/callback
를 추가하세요.
로그인과 마찬가지로, 사용자는 공유 세션에서 로그아웃하기 위해 Logto로 리디렉션되어야 합니다. 완료되면 사용자를 다시 웹사이트로 리디렉션하면 좋습니다. 예를 들어, 로그아웃 후 리디렉션 URI 섹션에 http://localhost:3000/
를 추가하세요.
그런 다음 "저장"을 클릭하여 변경 사항을 저장하세요.
WebSecurityConfig 구현
프로젝트에 새로운 클래스 WebSecurityConfig
생성
WebSecurityConfig
클래스는 애플리케이션의 보안 설정을 구성하는 데 사용됩니다. 이는 인증 및 인가 흐름을 처리하는 핵심 클래스입니다. 자세한 내용은 Spring Security 문서를 참조하세요.
package com.example.securingweb;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
// ...
}
idTokenDecoderFactory
빈 생성
Logto는 기본 알고리즘으로 ES384
를 사용하기 때문에, 동일한 알고리즘을 사용하도록 기본 OidcIdTokenDecoderFactory
를 덮어써야 합니다.
import org.springframework.context.annotation.Bean;
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
public class WebSecurityConfig {
// ...
@Bean
public JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
OidcIdTokenDecoderFactory idTokenDecoderFactory = new OidcIdTokenDecoderFactory();
idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> SignatureAlgorithm.ES384);
return idTokenDecoderFactory;
}
}
로그인 성공 이벤트를 처리하기 위한 LoginSuccessHandler 클래스 생성
성공적인 로그인 후 사용자를 /user
페이지로 리디렉션합니다.
package com.example.securingweb;
import java.io.IOException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class CustomSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
response.sendRedirect("/user");
}
}
로그아웃 성공 이벤트를 처리하기 위한 LogoutSuccessHandler 클래스 생성
세션을 지우고 사용자를 홈 페이지로 리디렉션합니다.
package com.example.securingweb;
import java.io.IOException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
public class CustomLogoutHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
HttpSession session = request.getSession();
if (session != null) {
session.invalidate();
}
response.sendRedirect("/home");
}
}
securityFilterChain
으로 WebSecurityConfig
클래스 업데이트
securityFilterChain은 들어오는 요청과 응답을 처리하는 필터 체인입니다.
우리는 securityFilterChain
을 구성하여 홈 페이지에 대한 접근을 허용하고 다른 모든 요청에 대해 인증을 요구할 것입니다. 로그인 및 로그아웃 이벤트를 처리하기 위해 CustomSuccessHandler
및 CustomLogoutHandler
를 사용하세요.
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
public class WebSecurityConfig {
// ...
@Bean
public DefaultSecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.antMatchers("/", "/home").permitAll() // 홈 페이지에 대한 접근 허용
.anyRequest().authenticated() // 다른 모든 요청은 인증 필요
)
.oauth2Login(oauth2Login ->
oauth2Login
.successHandler(new CustomSuccessHandler())
)
.logout(logout ->
logout
.logoutSuccessHandler(new CustomLogoutHandler())
);
return http.build();
}
}
홈 페이지 생성
(프로젝트에 이미 홈 페이지가 있는 경우 이 단계를 건너뛸 수 있습니다)
package com.example.securingweb;
import java.security.Principal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping({ "/", "/home" })
public String home(Principal principal) {
return principal != null ? "redirect:/user" : "home";
}
}
이 컨트롤러는 사용자가 인증된 경우 사용자 페이지로 리디렉션하고, 그렇지 않으면 홈 페이지를 표시합니다. 홈 페이지에 로그인 링크를 추가하세요.
<body>
<h1>Welcome!</h1>
<p><a th:href="@{/oauth2/authorization/logto}">Login with Logto</a></p>
</body>
사용자 페이지 생성
사용자 페이지를 처리하기 위한 새로운 컨트롤러를 생성하세요:
package com.example.securingweb;
import java.security.Principal;
import java.util.Map;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/user")
public class UserController {
@GetMapping
public String user(Model model, Principal principal) {
if (principal instanceof OAuth2AuthenticationToken) {
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) principal;
OAuth2User oauth2User = token.getPrincipal();
Map<String, Object> attributes = oauth2User.getAttributes();
model.addAttribute("username", attributes.get("username"));
model.addAttribute("email", attributes.get("email"));
model.addAttribute("sub", attributes.get("sub"));
}
return "user";
}
}
사용자가 인증되면, 인증된 주체 객체에서 OAuth2User
데이터를 가져옵니다. 자세한 내용은 OAuth2AuthenticationToken 및 OAuth2User를 참조하세요.
사용자 데이터를 읽고 user.html
템플릿에 전달하세요.
<body>
<h1>User Details</h1>
<div>
<p>
<div><strong>name:</strong> <span th:text="${username}"></span></div>
<div><strong>email:</strong> <span th:text="${email}"></span></div>
<div><strong>id:</strong> <span th:text="${sub}"></span></div>
</p>
</div>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Logout" />
</form>
</body>
추가 클레임 요청
principal (OAuth2AuthenticationToken)
에서 반환된 객체에 일부 사용자 정보가 누락된 것을 발견할 수 있습니다.
이는 OAuth 2.0 및 OpenID Connect (OIDC)가 최소 권한 원칙 (PoLP)을 따르도록 설계되었기 때문이며,
Logto는 이러한 표준을 기반으로 구축되었습니다.
기본적으로 제한된 클레임 (Claim)만 반환됩니다. 더 많은 정보를 원하시면, 추가적인 스코프 (Scope)를 요청하여 더 많은 클레임에 접근할 수 있습니다.
"클레임 (Claim)"은 주체에 대해 주장하는 내용이며, "스코프 (Scope)"는 클레임의 그룹입니다. 현재의 경우, 클레임은 사용자에 대한 정보입니다.
다음은 스코프 - 클레임 관계의 비규범적 예시입니다:
"sub" 클레임은 "주체"를 의미하며, 이는 사용자의 고유 식별자 (즉, 사용자 ID)입니다.
Logto SDK는 항상 세 가지 스코프를 요청합니다: openid
, profile
, 그리고 offline_access
.
추가 사용자 정보를 검색하려면 application.properties
파일에 추가 스코프를 추가할 수 있습니다. 예를 들어, email
, phone
, 및 urn:logto:scope:organizations
스코프를 요청하려면 application.properties
파일에 다음 줄을 추가하세요:
spring.security.oauth2.client.registration.logto.scope=openid,profile,offline_access,email,phone,urn:logto:scope:organizations
그런 다음 OAuth2User
객체에서 추가 클레임에 접근할 수 있습니다.
애플리케이션 실행 및 테스트
애플리케이션을 실행하고 http://localhost:8080
으로 이동하세요.
- 로그인 링크가 있는 홈 페이지를 볼 수 있습니다.
- 링크를 클릭하여 Logto로 로그인하세요.
- 인증이 성공하면 사용자 세부 정보가 포함된 사용자 페이지로 리디렉션됩니다.
- 로그아웃 버튼을 클릭하여 로그아웃하세요. 홈 페이지로 다시 리디렉션됩니다.
스코프 및 클레임
Logto는 OIDC 스코프 및 클레임 규약을 사용하여 ID 토큰 및 OIDC userinfo 엔드포인트에서 사용자 정보를 가져오기 위한 스코프 및 클레임을 정의합니다. "스코프"와 "클레임"은 OAuth 2.0 및 OpenID Connect (OIDC) 사양의 용어입니다.
간단히 말해, 스코프를 요청하면 사용자 정보에서 해당하는 클레임을 받게 됩니다. 예를 들어, email
스코프를 요청하면 사용자의 email
및 email_verified
데이터를 받게 됩니다.
지원되는 스코프와 해당 클레임 (Claims) 목록은 다음과 같습니다:
openid
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
sub | string | 사용자의 고유 식별자 | 아니요 |
profile
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
name | string | 사용자의 전체 이름 | 아니요 |
username | string | 사용자의 사용자 이름 | 아니요 |
picture | string | 최종 사용자의 프로필 사진 URL. 이 URL은 이미지 파일 (예: PNG, JPEG, 또는 GIF 이미지 파일)을 참조해야 하며, 이미지를 포함하는 웹 페이지를 참조해서는 안 됩니다. 이 URL은 최종 사용자를 설명할 때 표시하기 적합한 프로필 사진을 구체적으로 참조해야 하며, 최종 사용자가 찍은 임의의 사진을 참조해서는 안 됩니다. | 아니요 |
created_at | number | 최종 사용자가 생성된 시간. 시간은 유닉스 에포크 (1970-01-01T00:00:00Z) 이후 밀리초 수로 표현됩니다. | 아니요 |
updated_at | number | 최종 사용자의 정보가 마지막으로 업데이트된 시간. 시간은 유닉스 에포크 (1970-01-01T00:00:00Z) 이후 밀리초 수로 표현됩니다. | 아니요 |
다른 표준 클레임인 family_name
, given_name
, middle_name
, nickname
, preferred_username
, profile
, website
, gender
, birthdate
, zoneinfo
, 및 locale
도 사용자 정보 엔드포인트를 요청할 필요 없이 profile
스코프에 포함됩니다. 위의 클레임과의 차이점은 이러한 클레임은 값이 비어 있지 않을 때만 반환되며, 위의 클레임은 값이 비어 있을 경우 null
을 반환한다는 점입니다.
표준 클레임과 달리, created_at
및 updated_at
클레임은 초 대신 밀리초를 사용합니다.
email
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
string | 사용자의 이메일 주소 | 아니요 | |
email_verified | boolean | 이메일 주소가 검증되었는지 여부 | 아니요 |
phone
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
phone_number | string | 사용자의 전화번호 | 아니요 |
phone_number_verified | boolean | 전화번호가 검증되었는지 여부 | 아니요 |
address
주소 클레임의 세부 사항은 OpenID Connect Core 1.0을 참조하세요.
custom_data
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
custom_data | object | 사용자의 사용자 정의 데이터 | 예 |
identities
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
identities | object | 사용자의 연결된 아이덴티티 | 예 |
sso_identities | array | 사용자의 연결된 SSO 아이덴티티 | 예 |
roles
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
roles | string[] | 사용자의 역할 | 아니요 |
urn:logto:scope:organizations
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
organizations | string[] | 사용자가 속한 조직 ID | 아니요 |
organization_data | object[] | 사용자가 속한 조직 데이터 | 예 |
urn:logto:scope:organization_roles
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
organization_roles | string[] | 사용자가 속한 조직 역할, 형식은 <organization_id>:<role_name> | 아니요 |
성능과 데이터 크기를 고려할 때, "사용자 정보 필요 여부"가 "예"인 경우, 해당 클레임은 ID 토큰에 나타나지 않으며, userinfo 엔드포인트 응답에서 반환됩니다.
추가 사용자 정보를 요청하기 위해 application.properties
파일에 추가 스코프 및 클레임을 추가하세요. 예를 들어, urn:logto:scope:organizations
스코프를 요청하려면 application.properties
파일에 다음 줄을 추가하세요:
spring.security.oauth2.client.registration.logto.scope=openid,profile,offline_access,urn:logto:scope:organizations
사용자 조직 클레임은 인가 토큰에 포함됩니다.