跳至主要內容

為你的 Java Spring Boot 應用程式新增驗證 (Authentication)

本指南將向你展示如何將 Logto 整合到你的 Java Spring Boot 應用程式中。

提示:
  • 你可以在我們的 spring-boot-sample GitHub 儲存庫中找到本指南的範例程式碼。
  • 不需要官方 SDK 即可將 Logto 與你的 Java Spring Boot 應用程式整合。我們將使用 Spring SecuritySpring Security OAuth2 庫來處理與 Logto 的 OIDC 驗證流程。

先決條件

配置你的 Java Spring Boot 應用程式

添加依賴項

對於 gradle 用戶,將以下依賴項添加到你的 build.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 文件中:

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 文件中:

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

實作

在深入細節之前,以下是終端使用者體驗的快速概覽。登入流程可簡化如下:

  1. 你的應用程式呼叫登入方法。
  2. 使用者被重定向至 Logto 登入頁面。對於原生應用程式,系統瀏覽器會被開啟。
  3. 使用者登入後被重定向回你的應用程式(配置為重定向 URI)。

關於基於重導的登入

  1. 此驗證流程遵循 OpenID Connect (OIDC) 協議,Logto 強制執行嚴格的安全措施以保護使用者登入。
  2. 如果你有多個應用程式,可以使用相同的身分提供者 (IdP, Identity provider)(Logto)。一旦使用者登入其中一個應用程式,Logto 將在使用者訪問另一個應用程式時自動完成登入流程。

欲了解更多關於基於重導登入的原理和優勢,請參閱 Logto 登入體驗解析


為了在使用者登入後將其重定向回你的應用程式,你需要在前一步中使用 client.registration.logto.redirect-uri 屬性設置重定向 URI。

配置重定向 URI

切換到 Logto Console 的應用程式詳細資訊頁面。新增一個重定向 URI http://localhost:3000/callback

Logto Console 中的重定向 URI

就像登入一樣,使用者應被重定向到 Logto 以登出共享會話。完成後,將使用者重定向回你的網站會很不錯。例如,將 http://localhost:3000/ 新增為登出後重定向 URI 區段。

然後點擊「儲存」以保存更改。

實作 WebSecurityConfig

在你的專案中創建一個新的 WebSecurityConfig

WebSecurityConfig 類將用於配置應用程式的安全設置。它是處理驗證和授權流程的關鍵類。請查看 Spring Security 文件 以獲取更多詳細資訊。

WebSecurityConfig.java
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 bean

這是必需的,因為 Logto 使用 ES384 作為預設算法,我們需要覆寫預設的 OidcIdTokenDecoderFactory 以使用相同的算法。

WebSecurityConfig.java
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 頁面。

CustomSuccessHandler.java
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 類來處理登出成功事件

清除會話並將使用者重定向到首頁。

CustomLogoutHandler.java
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 以允許訪問首頁,並要求對所有其他請求進行驗證。使用 CustomSuccessHandlerCustomLogoutHandler 來處理登入和登出事件。

WebSecurityConfig.java
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();
}
}

創建首頁

(如果你的專案中已經有首頁,可以跳過此步驟)

HomeController.java
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";
}
}

此控制器將在使用者驗證後將其重定向到使用者頁面,否則將顯示首頁。在首頁添加一個登入連結。

resources/templates/home.html
<body>
<h1>Welcome!</h1>

<p><a th:href="@{/oauth2/authorization/logto}">Login with Logto</a></p>
</body>

創建使用者頁面

創建一個新的控制器來處理使用者頁面:

UserController.java
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";
}
}

一旦使用者驗證成功,我們將從已驗證的 principal 物件中檢索 OAuth2User 資料。請參閱 OAuth2AuthenticationTokenOAuth2User 以獲取更多詳細資訊。

讀取使用者資料並將其傳遞給 user.html 模板。

resources/templates/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>

請求額外的宣告 (Claims)

你可能會發現從 principal (OAuth2AuthenticationToken) 返回的物件中缺少一些使用者資訊。這是因為 OAuth 2.0 和 OpenID Connect (OIDC) 的設計遵循最小權限原則 (PoLP, Principle of Least Privilege),而 Logto 是基於這些標準構建的。

預設情況下,僅返回有限的宣告 (Claims)。如果你需要更多資訊,可以請求額外的權限範圍 (Scopes) 以存取更多宣告。

資訊:

「宣告 (Claim)」是對主體所做的斷言;「權限範圍 (Scope)」是一組宣告。在目前的情況下,宣告是關於使用者的一部分資訊。

以下是權限範圍與宣告關係的非規範性範例:

提示:

「sub」宣告表示「主體 (Subject)」,即使用者的唯一識別符(例如使用者 ID)。

Logto SDK 將始終請求三個權限範圍:openidprofileoffline_access

要檢索額外的使用者資訊,你可以在 application.properties 文件中添加額外的權限範圍 (Scopes)。例如,要請求 emailphoneurn:logto:scope:organizations 權限範圍,請在 application.properties 文件中添加以下行:

application.properties
  spring.security.oauth2.client.registration.logto.scope=openid,profile,offline_access,email,phone,urn:logto:scope:organizations

然後你可以在 OAuth2User 物件中訪問額外的宣告 (Claims)。

運行並測試應用程式

運行應用程式並導航到 http://localhost:8080

  • 你將看到帶有登入連結的首頁。
  • 點擊連結以使用 Logto 登入。
  • 驗證成功後,你將被重定向到包含你的使用者詳細資訊的使用者頁面。
  • 點擊登出按鈕以登出。你將被重定向回首頁。

權限範圍 (Scopes) 和宣告 (Claims)

Logto 使用 OIDC 權限範圍 (Scopes) 和宣告 (Claims) 慣例 來定義從 ID 權杖 (ID token) 和 OIDC userinfo endpoint 獲取使用者資訊的權限範圍和宣告。無論是「權限範圍 (Scope)」還是「宣告 (Claim)」,都是 OAuth 2.0 和 OpenID Connect (OIDC) 規範中的術語。

簡而言之,當你請求一個權限範圍 (Scope) 時,你將獲得使用者資訊中的相應宣告 (Claims)。例如,如果你請求 email 權限範圍 (Scope),你將獲得使用者的 emailemail_verified 資料。

以下是支援的權限範圍 (Scopes) 及其對應的宣告 (Claims):

openid

宣告名稱類型描述需要使用者資訊嗎?
substring使用者的唯一識別符

profile

宣告名稱類型描述需要使用者資訊嗎?
namestring使用者的全名
usernamestring使用者的用戶名
picturestring使用者個人資料圖片的 URL。此 URL 必須指向圖像文件(例如 PNG、JPEG 或 GIF 圖像文件),而不是包含圖像的網頁。請注意,此 URL 應特別參考適合在描述使用者時顯示的個人資料照片,而不是使用者拍攝的任意照片。
created_atnumber使用者創建的時間。時間以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。
updated_atnumber使用者資訊最後更新的時間。時間以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。

其他 標準宣告 包括 family_namegiven_namemiddle_namenicknamepreferred_usernameprofilewebsitegenderbirthdatezoneinfolocale 也將包含在 profile 權限範圍中,無需請求使用者資訊端點。與上述宣告的不同之處在於,這些宣告僅在其值不為空時返回,而上述宣告在值為空時將返回 null

備註:

與標準宣告不同,created_atupdated_at 宣告使用毫秒而非秒。

email

宣告名稱類型描述需要使用者資訊嗎?
emailstring使用者的電子郵件地址
email_verifiedboolean電子郵件地址是否已驗證

phone

宣告名稱類型描述需要使用者資訊嗎?
phone_numberstring使用者的電話號碼
phone_number_verifiedboolean電話號碼是否已驗證

address

請參閱 OpenID Connect Core 1.0 以獲取地址宣告的詳細資訊。

custom_data

宣告名稱類型描述需要使用者資訊嗎?
custom_dataobject使用者的自訂資料

identities

宣告名稱類型描述需要使用者資訊嗎?
identitiesobject使用者的連結身分
sso_identitiesarray使用者的連結 SSO 身分

roles

宣告名稱類型描述需要使用者資訊嗎?
rolesstring[]使用者的角色

urn:logto:scope:organizations

宣告名稱類型描述需要使用者資訊嗎?
organizationsstring[]使用者所屬的組織 ID
organization_dataobject[]使用者所屬的組織資料

urn:logto:scope:organization_roles

宣告名稱類型描述需要使用者資訊嗎?
organization_rolesstring[]使用者所屬的組織角色,格式為 <organization_id>:<role_name>

考慮到效能和資料大小,如果「需要使用者資訊嗎?」為「是」,則表示該宣告不會顯示在 ID 權杖中,而會在 使用者資訊端點 回應中返回。

application.properties 檔案中新增額外的權限範圍 (Scopes) 和宣告 (Claims) 以請求更多使用者資訊。例如,要請求 urn:logto:scope:organizations 權限範圍 (Scope),請在 application.properties 檔案中新增以下行:

application.properties
spring.security.oauth2.client.registration.logto.scope=openid,profile,offline_access,urn:logto:scope:organizations

使用者的組織宣告 (Claims) 將包含在授權權杖 (Authorization token) 中。

進一步閱讀

終端使用者流程:驗證流程、帳戶流程與組織流程 配置連接器 保護你的 API