各位观众老爷,大家好!我是今天的主讲人,咱们今天聊聊Java Spring Security
OAuth 2.0
/ OpenID Connect
Resource Server
和 Authorization Server
的那些事儿。别害怕,虽然概念听起来挺唬人,但其实也没那么复杂,我会用大白话加上代码,争取让大家听完之后,也能拍着胸脯说一句:“这玩意儿,我会!”
咱们先来明确一下,今天的主题是基于 Spring Security 构建 OAuth 2.0 和 OpenID Connect 的资源服务器(Resource Server)和授权服务器(Authorization Server)。这两个家伙是 OAuth 2.0 和 OpenID Connect 协议中的核心角色,理解它们至关重要。
OAuth 2.0 和 OpenID Connect 的关系
先简单说说 OAuth 2.0 和 OpenID Connect 的关系,OAuth 2.0 是一种授权框架,主要解决的是第三方应用如何安全地访问用户的受保护资源的问题。而 OpenID Connect 是基于 OAuth 2.0 的一个身份验证层,它在 OAuth 2.0 的基础上增加了用户身份信息的传递,让第三方应用不仅可以获得授权,还能知道是谁授的权。可以这么理解:OAuth 2.0 管授权,OpenID Connect 管身份。
角色介绍:Authorization Server vs. Resource Server
- Authorization Server (授权服务器): 顾名思义,它的主要职责是颁发访问令牌(Access Token)。它负责验证用户的身份,并根据用户的授权决定是否颁发令牌。简单来说,它就是个发通行证的,只有它说了算,谁能拿到通行证。
- Resource Server (资源服务器): 资源服务器负责保护用户的受保护资源,例如用户的个人资料、照片等。它会验证请求中携带的访问令牌,只有持有有效令牌的请求才能访问资源。它就像个保安,只有持有有效通行证的人才能进入大楼。
搭建授权服务器 (Authorization Server)
咱们先从授权服务器开始。使用 Spring Security 构建授权服务器,我们需要用到 Spring Authorization Server 项目,它基于 Spring Security 5.8+ 构建,提供了 OAuth 2.0 Authorization Server 的完整功能。
-
添加依赖:
首先,在
pom.xml
文件中添加 Spring Authorization Server 的依赖。<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-authorization-server</artifactId> <version>1.1.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency>
这里还加入了
spring-boot-starter-jdbc
和h2
,是为了方便演示,我们将客户端信息存储在 H2 内存数据库中。实际项目中,建议使用更可靠的数据库。 -
配置授权服务器:
创建一个配置类,用于配置授权服务器。
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; 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.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import java.time.Duration; import java.util.UUID; @Configuration @EnableWebSecurity public class AuthorizationServerConfig { @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .oidc(Customizer.withDefaults()); // Enable OIDC endpoint http // Redirect to the login page when not authenticated from the authorization endpoint .exceptionHandling((exceptions) -> exceptions .authenticationEntryPoint( new LoginUrlAuthenticationEntryPoint("/login")) ) // Accept access tokens for User Info and/or Client Registration .oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults())); return http.build(); } @Bean public UserDetailsService users() { UserDetails user = User.withUsername("user") .password(passwordEncoder().encode("password")) .roles("USER") .build(); return new InMemoryUserDetailsManager(user); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("messaging-client") .clientSecret(passwordEncoder().encode("secret")) .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client") .redirectUri("http://127.0.0.1:8080/authorized") .scope(OidcScopes.OPENID) .scope(OidcScopes.PROFILE) .scope("message.read") .scope("message.write") .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build()) .tokenSettings(TokenSettings.builder() .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) .accessTokenTimeToLive(Duration.ofMinutes(10)) .refreshTokenTimeToLive(Duration.ofHours(8)) .reuseRefreshTokens(true) .build()) .build(); // Save registered client in db as if in-memory JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate); registeredClientRepository.save(registeredClient); return registeredClientRepository; } @Bean public EmbeddedDatabase embeddedDatabase() { return new EmbeddedDatabaseBuilder() .generateUniqueName(true) .setType(EmbeddedDatabaseType.H2) .setScriptEncoding("UTF-8") .addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql") .addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql") .addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql") .build(); } }
代码解释:
@EnableWebSecurity
: 启用 Spring Security 的 Web 安全功能。@Order(Ordered.HIGHEST_PRECEDENCE)
: 设置这个SecurityFilterChain
的优先级最高,确保 OAuth 2.0 授权服务器的配置先生效。authorizationServerSecurityFilterChain
: 配置 OAuth 2.0 授权服务器的安全过滤器链。OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
: 应用默认的 OAuth 2.0 授权服务器安全配置。.oidc(Customizer.withDefaults())
: 启用 OpenID Connect 支持。.exceptionHandling((exceptions) -> ...)
: 配置异常处理,当未认证的用户访问授权端点时,重定向到登录页面。.oauth2ResourceServer((resourceServer) -> ...)
: 配置 OAuth 2.0 资源服务器,允许使用 JWT 访问用户信息和客户端注册端点。
UserDetailsService users()
: 创建一个内存中的用户,用于演示登录。实际项目中,应该从数据库或其他数据源加载用户。PasswordEncoder passwordEncoder()
: 配置密码编码器,用于加密用户密码和客户端密钥。RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate)
: 配置客户端存储库,这里使用JdbcRegisteredClientRepository
将客户端信息存储在数据库中。RegisteredClient.withId(...)
: 创建一个客户端,配置客户端 ID、密钥、授权类型、重定向 URI、Scope 等信息。clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())
: 配置客户端设置,这里设置为不需要用户授权确认。tokenSettings(TokenSettings.builder()...build())
: 配置令牌设置,包括令牌格式(JWT),有效期,刷新令牌等
EmbeddedDatabase embeddedDatabase()
: 创建一个嵌入式 H2 数据库,用于存储客户端信息。
重点说一下
RegisteredClient
的配置:属性 说明 clientId
客户端 ID,用于标识客户端。 clientSecret
客户端密钥,用于客户端进行身份验证。 clientAuthenticationMethod
客户端身份验证方法,常用的有 CLIENT_SECRET_BASIC
(使用 HTTP Basic 认证)和CLIENT_SECRET_POST
(将客户端 ID 和密钥放在请求体中)。authorizationGrantType
授权类型,常用的有 AUTHORIZATION_CODE
(授权码模式)、REFRESH_TOKEN
(刷新令牌模式)、CLIENT_CREDENTIALS
(客户端凭据模式)。redirectUri
重定向 URI,授权服务器在完成授权后会将用户重定向到这个 URI。 scope
权限范围,用于限制客户端可以访问的资源。 clientSettings
客户端设置,例如是否需要用户授权确认。 tokenSettings
令牌设置,例如访问令牌的有效期、刷新令牌的有效期、是否可以重用刷新令牌。 SQL 脚本:
创建资源服务器之前,我们需要创建数据库表。以下是创建数据库表的 SQL 脚本(
oauth2-registered-client-schema.sql
、oauth2-authorization-schema.sql
和oauth2-authorization-consent-schema.sql
):oauth2-registered-client-schema.sql:
CREATE TABLE IF NOT EXISTS oauth2_registered_client ( id varchar(100) NOT NULL, client_id varchar(100) NOT NULL, client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, client_secret varchar(200) DEFAULT NULL, client_secret_expires_at timestamp DEFAULT NULL, client_name varchar(200) NOT NULL, client_authentication_methods varchar(1000) NOT NULL, authorization_grant_types varchar(1000) NOT NULL, redirect_uris varchar(1000) DEFAULT NULL, scopes varchar(1000) NOT NULL, client_settings varchar(2000) NOT NULL, token_settings varchar(2000) NOT NULL, PRIMARY KEY (id) );
oauth2-authorization-schema.sql:
CREATE TABLE IF NOT EXISTS oauth2_authorization ( id varchar(100) NOT NULL, registered_client_id varchar(100) NOT NULL, principal_name varchar(200) NOT NULL, authorization_grant_type varchar(100) NOT NULL, authorized_scopes varchar(1000) DEFAULT NULL, attributes blob DEFAULT NULL, state varchar(500) DEFAULT NULL, authorization_code_value blob DEFAULT NULL, authorization_code_issued_at timestamp DEFAULT NULL, authorization_code_expires_at timestamp DEFAULT NULL, authorization_code_metadata blob DEFAULT NULL, access_token_value blob DEFAULT NULL, access_token_issued_at timestamp DEFAULT NULL, access_token_expires_at timestamp DEFAULT NULL, access_token_metadata blob DEFAULT NULL, access_token_type varchar(100) DEFAULT NULL, access_token_scopes varchar(1000) DEFAULT NULL, refresh_token_value blob DEFAULT NULL, refresh_token_issued_at timestamp DEFAULT NULL, refresh_token_expires_at timestamp DEFAULT NULL, refresh_token_metadata blob DEFAULT NULL, oidc_id_token_value blob DEFAULT NULL, oidc_id_token_issued_at timestamp DEFAULT NULL, oidc_id_token_expires_at timestamp DEFAULT NULL, oidc_id_token_metadata blob DEFAULT NULL, PRIMARY KEY (id) );
oauth2-authorization-consent-schema.sql:
CREATE TABLE IF NOT EXISTS oauth2_authorization_consent ( registered_client_id varchar(100) NOT NULL, principal_name varchar(200) NOT NULL, authorities varchar(1000) NOT NULL, PRIMARY KEY (registered_client_id, principal_name) );
这些脚本会在应用启动时自动执行,创建必要的数据库表。
-
登录页面:
由于我们启用了
OidcConfigurer
,当访问/oauth2/authorize
端点时,如果没有登录,将会被重定向到登录页面。我们需要创建一个简单的登录页面。创建一个
templates/login.html
文件:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Login</title> </head> <body> <h1>Login</h1> <form action="/login" method="post"> <div> <label>Username:</label> <input type="text" name="username" value="user"/> </div> <div> <label>Password:</label> <input type="password" name="password" value="password"/> </div> <button type="submit">Login</button> </form> </body> </html>
这个页面很简单,就是一个用户名和密码的输入框,以及一个登录按钮。
搭建资源服务器 (Resource Server)
接下来,咱们搭建资源服务器。资源服务器的主要任务是保护受保护的资源,并验证访问令牌的有效性。
-
添加依赖:
在
pom.xml
文件中添加 Spring Security 的 OAuth 2.0 资源服务器依赖。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
-
配置资源服务器:
创建一个配置类,用于配置资源服务器。
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import static org.springframework.security.config.Customizer.withDefaults; @Configuration @EnableWebSecurity @EnableMethodSecurity public class ResourceServerConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize .requestMatchers("/public").permitAll() .anyRequest().authenticated() ) .oauth2ResourceServer((oauth2) -> oauth2.jwt(withDefaults())); return http.build(); } }
代码解释:
@EnableWebSecurity
: 启用 Spring Security 的 Web 安全功能。@EnableMethodSecurity
: 启用方法级别的安全控制。securityFilterChain
: 配置资源服务器的安全过滤器链。authorizeHttpRequests((authorize) -> ...)
: 配置请求授权规则。.requestMatchers("/public").permitAll()
: 允许匿名访问/public
路径。.anyRequest().authenticated()
: 其他任何请求都需要进行身份验证。
oauth2ResourceServer((oauth2) -> oauth2.jwt(withDefaults()))
: 配置 OAuth 2.0 资源服务器,使用 JWT 进行身份验证。
-
创建受保护的资源:
创建一个 Controller,用于提供受保护的资源。
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ResourceController { @GetMapping("/resource") @PreAuthorize("hasAuthority('SCOPE_message.read')") public String resource() { return "Hello, Resource Server!"; } @GetMapping("/public") public String publicResource() { return "Hello, Public Resource!"; } }
代码解释:
@RestController
: 声明这是一个 REST Controller。@GetMapping("/resource")
: 将resource()
方法映射到/resource
路径。@PreAuthorize("hasAuthority('SCOPE_message.read')")
: 使用方法级别的安全控制,要求用户必须具有message.read
权限才能访问该资源。@GetMapping("/public")
: 将publicResource()
方法映射到/public
路径。
测试
-
启动授权服务器和资源服务器。
-
获取访问令牌。
-
访问授权服务器的授权端点
/oauth2/authorize
,例如:http://localhost:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid%20profile%20message.read&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client&state=test
(这里授权服务器运行在9000端口,资源服务器运行在8080端口,请替换为你实际的端口)
-
如果未登录,将会重定向到登录页面。输入用户名和密码(我们配置的
user/password
)进行登录。 -
登录成功后,如果客户端需要授权确认,会显示授权确认页面。由于我们配置了
requireAuthorizationConsent(false)
,所以不会显示授权确认页面,而是直接重定向到redirect_uri
。 -
授权服务器会将授权码(Authorization Code)作为参数添加到
redirect_uri
中。 -
使用授权码向授权服务器的令牌端点
/oauth2/token
发送请求,获取访问令牌。curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -H "Authorization: Basic bWVzc2FnaW5nLWNsaWVudDpzZWNyZXQ=" -d "grant_type=authorization_code&code={授权码}&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client" http://localhost:9000/oauth2/token
(将
{授权码}
替换为实际的授权码)Authorization: Basic bWVzc2FnaW5nLWNsaWVudDpzZWNyZXQ=
:使用 HTTP Basic 认证,其中messaging-client:secret
是客户端 ID 和密钥,需要进行 Base64 编码。grant_type=authorization_code
:指定授权类型为授权码模式。code={授权码}
:授权码。redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client
:重定向 URI。
-
授权服务器会返回一个 JSON 响应,包含访问令牌(access_token)、刷新令牌(refresh_token)等信息。
-
-
访问受保护的资源。
-
使用访问令牌向资源服务器的
/resource
端点发送请求。curl -H "Authorization: Bearer {访问令牌}" http://localhost:8080/resource
(将
{访问令牌}
替换为实际的访问令牌)Authorization: Bearer {访问令牌}
:使用 Bearer 认证,将访问令牌放在请求头中。
-
如果访问令牌有效,资源服务器会返回 "Hello, Resource Server!"。
-
-
访问公共资源。
-
直接访问资源服务器的
/public
端点。curl http://localhost:8080/public
-
资源服务器会返回 "Hello, Public Resource!"。
-
总结
咱们今天讲了如何使用 Spring Security 构建 OAuth 2.0 和 OpenID Connect 的授权服务器和资源服务器。虽然涉及到的概念和配置比较多,但只要理解了授权服务器和资源服务器的角色,以及 OAuth 2.0 和 OpenID Connect 的流程,就能轻松应对。
记住,授权服务器是发通行证的,资源服务器是看门的。它们一起保护着咱们的宝贵资源。
希望今天的讲座对大家有所帮助。如果有什么问题,欢迎随时提问。咱们下次再见!
扩展思考:
主题 | 说明 |
---|---|
自定义授权流程 | Spring Authorization Server 提供了丰富的扩展点,可以自定义授权流程,例如自定义用户认证、自定义授权确认页面、自定义令牌生成等。 |
使用 JWT 存储更多信息 | JWT (JSON Web Token) 是一种紧凑、自包含的方式,用于安全地传输信息。可以在 JWT 中存储更多用户信息,例如用户 ID、角色等,方便资源服务器进行权限控制。 |
分布式 Session 管理 | 在分布式系统中,Session 管理是一个挑战。可以使用 Spring Session 将 Session 存储在 Redis 或其他集中式存储中,实现 Session 共享。 |
OAuth 2.0 客户端 | 除了授权服务器和资源服务器,OAuth 2.0 还有一个重要的角色:客户端。客户端是代表用户向授权服务器请求授权,并使用访问令牌访问资源的应用程序。可以使用 Spring Security 的 OAuth 2.0 Client 模块构建 OAuth 2.0 客户端。 |
OpenID Connect 的更多特性 | OpenID Connect 在 OAuth 2.0 的基础上增加了用户身份信息的传递。可以利用 OpenID Connect 的更多特性,例如用户身份声明、会话管理等,构建更安全的身份验证系统。 |