前言
OAuth
是一種授權框架,用于創建權限策略,并允許應用程序對用戶在 HTTP 服務(如 GitHub 和 Google)上的賬戶進行有限訪問。它的工作原理是允許用戶授權第三方應用訪問他們的數據,而無需分享他們的憑證。本文將指導你如何在Spring Boot
應用中使用 Spring Security
實現 OAuth2
,并通過 OAuth2
提供商啟用安全的登錄和用戶數據訪問。
簡而言之,這篇文章的內容是講解如何在 Spring Boot
項目中集成 OAuth2
,使得用戶可以通過 OAuth2
提供商(比如 Google 或 github)進行安全登錄,而不需要直接暴露密碼等敏感信息。
什么是OAuth2?
OAuth2
(開放授權 2.0)是一個授權框架,允許應用程序在不暴露用戶憑證的情況下,獲得對 GitHub 或 Google 等 HTTP 服務上用戶賬戶的有限訪問權限。OAuth2
為用戶提供了一種方法,使他們能夠在不與第三方應用共享密碼的情況下訪問自己的資源。
簡單來說,OAuth2
允許用戶授權第三方應用訪問他們在其他平臺上的數據,但同時避免了密碼泄露的風險,確保了用戶的賬戶安全。
關鍵組件
- Resource Owner (資源所有者):是授權應用訪問其賬戶的用戶。
- Client (客戶端):是請求訪問用戶賬戶的應用程序。
- Authorization Server (授權服務器):是驗證用戶身份并向客戶端頒發訪問令牌的服務器。
- Resource Server (資源服務器):是托管受保護資源并接受訪問令牌以允許應用訪問這些資源的服務器。
OAuth2 授權流程
OAuth2 定義了幾種授權流程,以適應不同的使用場景:
1、Authorization Code Grant (授權碼授權):
-
適用于服務器端應用程序。此流程涉及應用程序用授權碼交換訪問令牌。
-
這是最常見的 OAuth2 流程,安全性較高,適用于需要安全存儲客戶端密鑰的應用程序。
2、Implicit Grant (簡化授權):
- 適用于客戶端應用程序(例如瀏覽器中的單頁應用)。在這種流程中,訪問令牌會直接返回,而不需要進行授權碼交換。
- 該流程較為簡單,但安全性較低,因為令牌直接暴露給客戶端。
3、Resource Owner Password Credentials Grant (資源所有者密碼憑證授權):
- 適用于信任客戶端的應用程序,在這種情況下,客戶端可以直接請求資源所有者的憑證(用戶名和密碼)。
- 該流程不推薦用于不受信任的應用,因為它需要用戶直接向客戶端提供憑證。
4、Client Credentials Grant (客戶端憑證授權):
- 適用于客戶端需要訪問自己的資源,而不是資源所有者的資源的情況。
- 該流程不涉及用戶交互,通常用于機器對機器的授權場景,例如后臺服務的認證。
先決條件
- 1、對 Spring Boot 和 Spring Security 有良好的了解;
- 2、在本地系統中安裝 JDK 和 IntelliJ IDEA
- 3、Google Console 帳戶作為 OAuth2 提供者 或者github
- 4、使用 Maven 進行依賴管理和構建
實現 OAuth2 與 Spring Security 集成
步驟1:創建一個新的Spring Boot項目
使用IntelliJ Idea創建一個新的Spring Boot項目,在創建項目時,選擇以下選項:
- Project Name: oauth2-spring-security
- Language: Java
- Type: Maven
- Packaging: Jar
步驟2:添加依賴項
將以下依賴項添加到Spring項目中。
創建項目后,IDE中的文件夾結構如下圖所示:
步驟3:配置應用程序屬性
打開應用程序。屬性文件,并將谷歌OAuth配置代碼添加到項目中。
spring.application.name=oauth2-spring-security
spring.security.oauth2.client.registration.google.client-id=xxxxxxxxxxxx-hjjujqg0rsjlocde5esmjelr7p6rl5a1.apps.googleusercontent.com
spring.security.oauth2.client.registration.google.client-secret=xxxxxxx-x-_hDO3tFTOsCatT2P7CZQDUKb4l
spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/login/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.google.scope=profile, emailspring.security.oauth2.client.registration.github.client-id=xxxxxxxxBsTQfkTtULzE
spring.security.oauth2.client.registration.github.client-secret=xxxxxxxxxxx816d1d112f411f7f5df3da721a322c
步驟4:創建用戶類
package org.example.model;import lombok.Data;@Data
public class User {private String name;private String email;// Getters and Setterspublic String getName() {return name;}public void setName(String name) {this.name = name;}public String getEmail() {return email;}public void setEmail(String email) {this.email = email;}
}
這個類用name和email屬性定義User模型。它使用Lombok注釋來簡化樣板代碼。
步驟5:創建UserService類
這個服務類負責從OAuth2User數據創建User對象。
package org.example.service;import org.example.model.User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;@Service
public class UserService {public User createUser(OAuth2User oAuth2User) {User user = new User();// Set user attributes from OAuth2Useruser.setName(oAuth2User.getAttribute("name"));user.setEmail(oAuth2User.getAttribute("email"));return user;}
}
步驟6:創建SecurityConfig類
package org.example.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;/*** @author Administrator*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http// Authorize requests.authorizeHttpRequests(authorizeRequests ->authorizeRequests.requestMatchers("/", "/login").permitAll().anyRequest().authenticated())// Configure OAuth2 login.oauth2Login(oauth2Login ->oauth2Login.loginPage("/login").defaultSuccessUrl("/home", true));return http.build();}
}
步驟8:主類
package org.example;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class Oauth2SpringSecurityApplication {public static void main(String[] args) {SpringApplication.run(Oauth2SpringSecurityApplication.class, args);}}
步驟9:創建登錄HTML文件
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org/">
<head><title>Login</title><link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"><style>body {background-color: #f8f9fa;}.navbar {background-color: #81c784; /* Light Green */}.navbar-brand {color: white !important;}.container {margin-top: 100px;}.card {border: none;border-radius: 10px;box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);}.card-header {background-color: #81c784; /* Light Green */color: white;border-top-left-radius: 10px;border-top-right-radius: 10px;}.btn-custom {background-color: #81c784; /* Light Green */color: white;border-radius: 5px;}.btn-custom:hover {background-color: #66bb6a; /* Darker Green */color: white;}</style>
</head>
<body>
<nav class="navbar navbar-expand-lg"><a class="navbar-brand" href="#">My App</a>
</nav>
<div class="container"><div class="row justify-content-center"><div class="col-md-6"><div class="card"><div class="card-header text-center">Login with OAuth2</div><div class="card-body text-center"><p class="card-text">Please login using one of the following options:</p><a class="btn btn-custom" href="/oauth2/authorization/google">Login with Google</a></div></div></div><div class="col-md-6"><div class="card"><div class="card-header text-center">Login with OAuth2</div><div class="card-body text-center"><p class="card-text">Please login using one of the following options:</p><a class="btn btn-custom" href="/oauth2/authorization/github">Login with github</a></div></div></div></div>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>
步驟10:創建主HTML文件
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org/">
<head><title>Home</title><link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"><style>body {background-color: #f8f9fa;}.navbar {background-color: #81c784; /* Light Green */}.navbar-brand, .nav-link {color: white !important;}.container {margin-top: 50px;}.card {border: none;border-radius: 10px;box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);}.card-header {background-color: #81c784; /* Light Green */color: white;border-top-left-radius: 10px;border-top-right-radius: 10px;}</style>
</head>
<body>
<nav class="navbar navbar-expand-lg"><a class="navbar-brand" href="#">My App</a><div class="collapse navbar-collapse"><ul class="navbar-nav ml-auto"><li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li></ul></div>
</nav>
<div class="container"><div class="row justify-content-center"><div class="col-md-8"><div class="card"><div class="card-header">Welcome, <span th:text="${name}"></span>!</div><div class="card-body"><p class="card-text">You are now logged in using OAuth2.</p></div></div></div></div>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>
pom.xml
<?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><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.5.5</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>org.example</groupId><artifactId>oauth2-spring-security</artifactId><version>0.0.1-SNAPSHOT</version><name>oauth2-spring-security</name><description>oauth2-spring-security</description><url/><licenses><license/></licenses><developers><developer/></developers><scm><connection/><developerConnection/><tag/><url/></scm><properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-client</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-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity6</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><annotationProcessorPaths><path><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></path></annotationProcessorPaths></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>
步驟11:運行應用程序
D:\Java\corretto-17.0.5\bin\java.exe -XX:TieredStopAtLevel=1 -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true "-Dmanagement.endpoints.jmx.exposure.include=*" "-javaagent:D:\JetBrains\IntelliJ IDEA 2024.1.4\lib\idea_rt.jar=4012:D:\JetBrains\IntelliJ IDEA 2024.1.4\bin" -Dfile.encoding=UTF-8 -classpath D:\02\oauth2-spring-security\target\classes;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-starter-oauth2-client\3.5.5\spring-boot-starter-oauth2-client-3.5.5.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-starter\3.5.5\spring-boot-starter-3.5.5.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-starter-logging\3.5.5\spring-boot-starter-logging-3.5.5.jar;C:\Users\Administrator\.m2\repository\ch\qos\logback\logback-classic\1.5.18\logback-classic-1.5.18.jar;C:\Users\Administrator\.m2\repository\ch\qos\logback\logback-core\1.5.18\logback-core-1.5.18.jar;C:\Users\Administrator\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.24.3\log4j-to-slf4j-2.24.3.jar;C:\Users\Administrator\.m2\repository\org\apache\logging\log4j\log4j-api\2.24.3\log4j-api-2.24.3.jar;C:\Users\Administrator\.m2\repository\org\slf4j\jul-to-slf4j\2.0.17\jul-to-slf4j-2.0.17.jar;C:\Users\Administrator\.m2\repository\jakarta\annotation\jakarta.annotation-api\2.1.1\jakarta.annotation-api-2.1.1.jar;C:\Users\Administrator\.m2\repository\org\yaml\snakeyaml\2.4\snakeyaml-2.4.jar;C:\Users\Administrator\.m2\repository\org\springframework\security\spring-security-config\6.5.3\spring-security-config-6.5.3.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-beans\6.2.10\spring-beans-6.2.10.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-context\6.2.10\spring-context-6.2.10.jar;C:\Users\Administrator\.m2\repository\org\springframework\security\spring-security-core\6.5.3\spring-security-core-6.5.3.jar;C:\Users\Administrator\.m2\repository\org\springframework\security\spring-security-crypto\6.5.3\spring-security-crypto-6.5.3.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-expression\6.2.10\spring-expression-6.2.10.jar;C:\Users\Administrator\.m2\repository\io\micrometer\micrometer-observation\1.15.3\micrometer-observation-1.15.3.jar;C:\Users\Administrator\.m2\repository\io\micrometer\micrometer-commons\1.15.3\micrometer-commons-1.15.3.jar;C:\Users\Administrator\.m2\repository\org\springframework\security\spring-security-oauth2-client\6.5.3\spring-security-oauth2-client-6.5.3.jar;C:\Users\Administrator\.m2\repository\org\springframework\security\spring-security-oauth2-core\6.5.3\spring-security-oauth2-core-6.5.3.jar;C:\Users\Administrator\.m2\repository\com\nimbusds\oauth2-oidc-sdk\9.43.6\oauth2-oidc-sdk-9.43.6.jar;C:\Users\Administrator\.m2\repository\com\github\stephenc\jcip\jcip-annotations\1.0-1\jcip-annotations-1.0-1.jar;C:\Users\Administrator\.m2\repository\com\nimbusds\content-type\2.2\content-type-2.2.jar;C:\Users\Administrator\.m2\repository\com\nimbusds\lang-tag\1.7\lang-tag-1.7.jar;C:\Users\Administrator\.m2\repository\org\springframework\security\spring-security-oauth2-jose\6.5.3\spring-security-oauth2-jose-6.5.3.jar;C:\Users\Administrator\.m2\repository\com\nimbusds\nimbus-jose-jwt\9.37.3\nimbus-jose-jwt-9.37.3.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-starter-security\3.5.5\spring-boot-starter-security-3.5.5.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-aop\6.2.10\spring-aop-6.2.10.jar;C:\Users\Administrator\.m2\repository\org\springframework\security\spring-security-web\6.5.3\spring-security-web-6.5.3.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-starter-thymeleaf\3.5.5\spring-boot-starter-thymeleaf-3.5.5.jar;C:\Users\Administrator\.m2\repository\org\thymeleaf\thymeleaf-spring6\3.1.3.RELEASE\thymeleaf-spring6-3.1.3.RELEASE.jar;C:\Users\Administrator\.m2\repository\org\thymeleaf\thymeleaf\3.1.3.RELEASE\thymeleaf-3.1.3.RELEASE.jar;C:\Users\Administrator\.m2\repository\org\attoparser\attoparser\2.0.7.RELEASE\attoparser-2.0.7.RELEASE.jar;C:\Users\Administrator\.m2\repository\org\unbescape\unbescape\1.1.6.RELEASE\unbescape-1.1.6.RELEASE.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-starter-web\3.5.5\spring-boot-starter-web-3.5.5.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-starter-json\3.5.5\spring-boot-starter-json-3.5.5.jar;C:\Users\Administrator\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.19.2\jackson-databind-2.19.2.jar;C:\Users\Administrator\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.19.2\jackson-annotations-2.19.2.jar;C:\Users\Administrator\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.19.2\jackson-core-2.19.2.jar;C:\Users\Administrator\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.19.2\jackson-datatype-jdk8-2.19.2.jar;C:\Users\Administrator\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.19.2\jackson-datatype-jsr310-2.19.2.jar;C:\Users\Administrator\.m2\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.19.2\jackson-module-parameter-names-2.19.2.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\3.5.5\spring-boot-starter-tomcat-3.5.5.jar;C:\Users\Administrator\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\10.1.44\tomcat-embed-core-10.1.44.jar;C:\Users\Administrator\.m2\repository\org\apache\tomcat\embed\tomcat-embed-el\10.1.44\tomcat-embed-el-10.1.44.jar;C:\Users\Administrator\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\10.1.44\tomcat-embed-websocket-10.1.44.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-web\6.2.10\spring-web-6.2.10.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-webmvc\6.2.10\spring-webmvc-6.2.10.jar;C:\Users\Administrator\.m2\repository\org\thymeleaf\extras\thymeleaf-extras-springsecurity6\3.1.3.RELEASE\thymeleaf-extras-springsecurity6-3.1.3.RELEASE.jar;C:\Users\Administrator\.m2\repository\org\slf4j\slf4j-api\2.0.17\slf4j-api-2.0.17.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-devtools\3.5.5\spring-boot-devtools-3.5.5.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot\3.5.5\spring-boot-3.5.5.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\3.5.5\spring-boot-autoconfigure-3.5.5.jar;C:\Users\Administrator\.m2\repository\org\projectlombok\lombok\1.18.38\lombok-1.18.38.jar;C:\Users\Administrator\.m2\repository\net\minidev\json-smart\2.5.2\json-smart-2.5.2.jar;C:\Users\Administrator\.m2\repository\net\minidev\accessors-smart\2.5.2\accessors-smart-2.5.2.jar;C:\Users\Administrator\.m2\repository\org\ow2\asm\asm\9.7.1\asm-9.7.1.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-core\6.2.10\spring-core-6.2.10.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-jcl\6.2.10\spring-jcl-6.2.10.jar org.example.Oauth2SpringSecurityApplication. ____ _ __ _ _/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \\\/ ___)| |_)| | | | | || (_| | ) ) ) )' |____| .__|_| |_|_| |_\__, | / / / /=========|_|==============|___/=/_/_/_/:: Spring Boot :: (v3.5.5)2025-09-07T21:35:35.024+08:00 INFO 3272 --- [oauth2-spring-security] [ restartedMain] o.e.Oauth2SpringSecurityApplication : Starting Oauth2SpringSecurityApplication using Java 17.0.5 with PID 3272 (D:\02\oauth2-spring-security\target\classes started by Administrator in D:\02\oauth2-spring-security)
2025-09-07T21:35:35.036+08:00 INFO 3272 --- [oauth2-spring-security] [ restartedMain] o.e.Oauth2SpringSecurityApplication : No active profile set, falling back to 1 default profile: "default"
2025-09-07T21:35:35.200+08:00 INFO 3272 --- [oauth2-spring-security] [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2025-09-07T21:35:35.200+08:00 INFO 3272 --- [oauth2-spring-security] [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2025-09-07T21:35:37.926+08:00 INFO 3272 --- [oauth2-spring-security] [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2025-09-07T21:35:37.963+08:00 INFO 3272 --- [oauth2-spring-security] [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2025-09-07T21:35:37.963+08:00 INFO 3272 --- [oauth2-spring-security] [ restartedMain] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.44]
2025-09-07T21:35:38.074+08:00 INFO 3272 --- [oauth2-spring-security] [ restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2025-09-07T21:35:38.074+08:00 INFO 3272 --- [oauth2-spring-security] [ restartedMain] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2872 ms
2025-09-07T21:35:39.463+08:00 INFO 3272 --- [oauth2-spring-security] [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
2025-09-07T21:35:39.540+08:00 INFO 3272 --- [oauth2-spring-security] [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2025-09-07T21:35:39.562+08:00 INFO 3272 --- [oauth2-spring-security] [ restartedMain] o.e.Oauth2SpringSecurityApplication : Started Oauth2SpringSecurityApplication in 5.87 seconds (process running for 9.315)
步驟12:測試應用程序
登錄頁http://localhost:8080/login
Google OAuth Authentication:
首頁 http://localhost:8080/home
github OAuth Authentication: