Spring Security OAuth 2 教程 - 7:Spring MVC 客户端应用
在本文中,我们将创建一个名为 messages-webapp
的 Spring MVC + Thymeleaf Web 应用,并使用 Keycloak 进行访问控制,使用 Spring Security OAuth 2.0 进行认证。
你可以在 Github 上找到该项目的完整源码。
使用 Docker Compose 安装 Keycloak
在上一篇文章中,我们已经了解了如何使用 Docker Compose 安装 Keycloak。
创建 docker-compose.yml
文件,内容如下:
version: '3.8'
name: spring-security-oauth2-microservices-demo
services:
keycloak:
image: quay.io/keycloak/keycloak:22.0.3
command: ['start-dev']
container_name: keycloak
hostname: keycloak
environment:
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD=admin1234
ports:
- "9191:8080"
运行以下命令启动 Keycloak 实例:
$ docker compose up -d
现在,你可以访问 Keycloak 管理控制台 http://localhost:9191/
,并使用 admin/admin1234
登录。
创建 Keycloak Realm、客户端和用户
在前面的文章中,我们已经学习了如何创建 Realm、客户端和用户。请按照 前文 中提到的步骤创建新的 Realm、客户端和用户,只需更改 “Valid redirect URIs”。
将有 Valid redirect URIs 值设置为 http://localhost:8080/login/oauth2/code/messages-webapp
。
现在,详细信息应该如下:
- Keycloak Realm: sivalabs
- Client Configuration:
- Client ID: messages-webapp
- Client Secret: O3SVuBs0Z25kpYoRtL5C0FhLwAnIx1CW (you might have different value)
- Root URL:
http://localhost:8080
- Home URL:
http://localhost:8080
- Valid redirect URIs:
http://localhost:8080/login/oauth2/code/messages-webapp
- Valid post logout redirect URIs:
http://localhost:8080/
- Web origins:
http://localhost:8080
- User: siva/siva1234
注意到 Valid redirect URIs 的值了吗?它与我们在前几篇文章中配置的 (
http://localhost:8080/callback
)有所不同。Spring Security 实现了 Authentication Filter 来处理 OAuth 2.0 授权码授权流程。
在 Spring Security OAuth 2.0 实现中,redirect-uri
的默认值是 {baseUrl}/login/oauth2/code/{registrationId}
。我们使用 messages-webapp
作为客户端应用的 registrationId
。因此,我们需要在 Keycloak 中将有 Valid redirect URIs 配置为 http://localhost:8080/login/oauth2/code/messages-webapp
。
注意:
确保你已按上述要求配置了 Root URL、Home URL、Valid redirect URIs、Valid post logout redirect URIs 和 Web origins。在结尾多加一个 "/" 或不加 "/" 可能会导致 invalid_redirect_uri 等错误。
创建 messages-webapp
点击 此链接 可使用 Spring Initializr 生成 messages-webapp
。我们选择了 Web
、Validation
、OAuth2 Client
、Security
和 Thymeleaf
Starter。应用生成后,在 IDE 中打开它。
配置 OAuth 2.0 客户端注册属性
OAuth 2.0 客户端应用可以使用多个身份认证 Provider,如 Google、Facebook、GitHub、Okta、Keycloak 等。在本例中,我们只使用一个身份认证 Provider,即 Keycloak。
我们需要在 application.properties
文件中使用 spring.security.oauth2.client.registration.{registrationId}.*
. 和 spring.security.oauth2.client.provider.{registrationId}.*
属性配置客户端应用的详细信息。我们使用 messages-webapp
作为 registrationId
,并对属性进行如下配置:
spring.security.oauth2.client.registration.messages-webapp.client-id=messages-webapp
spring.security.oauth2.client.registration.messages-webapp.client-secret=O3SVuBs0Z25kpYoRtL5C0FhLwAnIx1CW
spring.security.oauth2.client.registration.messages-webapp.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.messages-webapp.scope=openid, profile
spring.security.oauth2.client.registration.messages-webapp.redirect-uri={baseUrl}/login/oauth2/code/messages-webapp
spring.security.oauth2.client.provider.messages-webapp.issuer-uri=http://localhost:9191/realms/sivalabs
#spring.security.oauth2.client.provider.messages-webapp.authorization-uri=http://localhost:9191/realms/sivalabs/protocol/openid-connect/auth
#spring.security.oauth2.client.provider.messages-webapp.token-uri=http://localhost:9191/realms/sivalabs/protocol/openid-connect/token
#spring.security.oauth2.client.provider.messages-webapp.jwk-set-uri=http://localhost:9191/realms/sivalabs/protocol/openid-connect/certs
#spring.security.oauth2.client.provider.messages-webapp.user-info-uri=http://localhost:9191/realms/sivalabs/protocol/openid-connect/userinfo
观察上述配置,我们注释掉了 authorization-uri
、token-uri
、jwk-set-uri
和 user-info-uri
属性。Spring Security OAuth 2.0 客户端实现将通过调用 {issuer-uri}/.well-known/openid-configuration
端点,自动发现这些端点。
如果访问 http://localhost:9191/realms/sivalabs/.well-known/openid-configuration
,可以看到以下包含所有端点信息的响应:
{
"issuer": "http://localhost:9191/realms/sivalabs",
"authorization_endpoint": "http://localhost:9191/realms/sivalabs/protocol/openid-connect/auth",
"token_endpoint": "http://localhost:9191/realms/sivalabs/protocol/openid-connect/token",
"introspection_endpoint": "http://localhost:9191/realms/sivalabs/protocol/openid-connect/token/introspect",
"userinfo_endpoint": "http://localhost:9191/realms/sivalabs/protocol/openid-connect/userinfo",
"end_session_endpoint": "http://localhost:9191/realms/sivalabs/protocol/openid-connect/logout",
"frontchannel_logout_session_supported": true,
"frontchannel_logout_supported": true,
"jwks_uri": "http://localhost:9191/realms/sivalabs/protocol/openid-connect/certs",
"check_session_iframe": "http://localhost:9191/realms/sivalabs/protocol/openid-connect/login-status-iframe.html",
"grant_types_supported": [
"authorization_code",
"implicit",
"refresh_token",
"password",
"client_credentials",
"urn:ietf:params:oauth:grant-type:device_code",
"urn:openid:params:grant-type:ciba"
],
...,
"response_types_supported": [
"code",
"none",
"id_token",
"token",
"id_token token",
"code id_token",
"code token",
"code id_token token"
],
...
...
}
实现主页
添加 spring-boot-starter-security
依赖后,Spring Security 将自动确保所有端点的安全。我们还添加了 spring-boot-starter-oauth2-client
依赖,它会使用在 application.properties
中配置的属性自动配置 OAuth 2.0 客户端。
创建 HomeController
类,如下:
@Controller
public class HomeController {
@GetMapping("/")
public String home(Model model, @AuthenticationPrincipal OAuth2User principal) {
model.addAttribute("username", principal.getAttribute("name"));
return "home";
}
}
我们使用 @AuthenticationPrincipal
注解注入已通过身份认证的 User Principal Object。OAuth2User
接口代表已通过身份认证的 User Principal。
在 src/main/resources/templates
文件夹下创建 home.html
文件,内容如下:
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
</head>
<body>
<div>
<h1>Welcome <span th:text="${username}">username</span></h1>
</div>
</body>
</html>
现在运行应用并访问 http://localhost:8080/
,就会跳转到 Keycloak 登录页面。使用 siva/siva1234
凭证登录成功后,你将被重定向到主页,并可在主页上看到用户名。
自定义 Security 配置
Spring Security 会自动保护所有端点的安全。但是,我们希望允许未经身份认证的用户访问主页。因此,我们需要自定义 Security 配置,以允许匿名用户访问主页。
创建 SecurityConfig
类,内容如下:
package com.sivalabs.messages.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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.configurers.CorsConfigurer;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(c ->
c.requestMatchers("/").permitAll()
.anyRequest().authenticated()
)
.cors(CorsConfigurer::disable)
.csrf(CsrfConfigurer::disable)
.oauth2Login(Customizer.withDefaults());
return http.build();
}
}
由于所有人都能访问主页,所以 @AuthenticationPrincipal
可能为 null
。更新 HomeController
来处理这种情况。
@Controller
public class HomeController {
@GetMapping("/")
public String home(Model model, @AuthenticationPrincipal OAuth2User principal) {
if(principal != null) {
model.addAttribute("username", principal.getAttribute("name"));
} else {
model.addAttribute("username", "Guest");
}
return "home";
}
}
现在,如果我们重启应用并访问 http://localhost:8080/
,我们将看到主页而无需进行任何身份认证。
现在,我们需要一种登录应用的方法。我们可以在主页上添加一个登录链接,如下所示:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity">
<head>
<title>Home</title>
</head>
<body>
<div >
<p sec:authorize="!isAuthenticated()">
<a href="/oauth2/authorization/messages-webapp">Login</a>
</p>
<h1>Welcome <span th:text="${username}">username</span></h1>
</div>
</body>
</html>
我们检查用户是否已登录,并有条件地显示登录链接。我们使用 Spring Security OAuth 2.0 默认登录 URL /oauth2/authorization/{registrationId}
来启动 OAuth 2.0 授权码授权流程。
现在访问 http://localhost:8080/
,你将看到登录链接。点击 “Login” 链接,就会跳转到 Keycloak 登录页面。登录成功后,你将被重定向到主页,并在主页上看到用户名。
实现注销
默认情况下,Spring Security OAuth 2.0 客户端实现会配置注销功能,这样就可以通过调用 URL /logout
来进行注销。然后,HTTP Session 将失效,SecurityContextHolder
将被清除,然后重定向到配置的 “Valid post logout redirect URIs”。
如果要自定义注销功能,可以按以下步骤更新 SecurityConfig
:
package com.sivalabs.messages.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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.configurers.CorsConfigurer;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final ClientRegistrationRepository clientRegistrationRepository;
public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(c ->
c.requestMatchers("/").permitAll()
.anyRequest().authenticated()
)
.cors(CorsConfigurer::disable)
.csrf(CsrfConfigurer::disable)
.oauth2Login(Customizer.withDefaults())
.logout(logout -> logout
.clearAuthentication(true)
.invalidateHttpSession(true)
.logoutSuccessHandler(oidcLogoutSuccessHandler())
);
return http.build();
}
private LogoutSuccessHandler oidcLogoutSuccessHandler() {
OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository);
oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}/");
return oidcLogoutSuccessHandler;
}
}
总结
在本文中,我们创建了 messages-webapp
客户端应用,并使用 Spring Security OAuth 2.0 “授权码模式”(Authorization Code Flow) 进行访问控制。
在下一篇文章中,我们将创建 messages-service
资源服务器,并使用 Spring Security OAuth 2.0 进行访问控制,然后从 messages-webapp
调用其 API。
参考:https://www.sivalabs.in/spring-security-oauth2-tutorial-securing-springmvc-client-application/